Files
calendar/packages/embeds/embed-core/routing-playground.html
T
Hariom BalharaandGitHub 6f0c09d2be feat: add booker lifecycle SDK events for embed tracking (#25569)
* feat: add embed prerendering support and enhance event handling

- Introduced `useIsEmbedPrerendering` hook to determine if the embed is in prerender mode.
- Updated `useAvailabilityEvents` to prevent firing events during prerendering.
- Added `bookerLoadedEvent` and `availabilityRefreshed` types to `EventDataMap`.
- Implemented `useFirebookerLoadedEvent` to manage firing the booker loaded event conditionally.
- Refactored event firing logic in `useSchedule` and `BookerWebWrapper` components to utilize new hooks.

* feat: add embed prerendering support and enhance event handling

- Introduced `useIsEmbedPrerendering` hook to determine if the embed is in prerender mode.
- Updated `useAvailabilityEvents` to prevent firing events during prerendering.
- Added `bookerLoadedEvent` and `availabilityRefreshed` types to `EventDataMap`.
- Implemented `useFirebookerLoadedEvent` to manage firing the booker loaded event conditionally.
- Refactored event firing logic in `useSchedule` and `BookerWebWrapper` components to utilize new hooks.

* feat: enhance embed event handling and introduce link reopening detection

- Added `useEmbedReopened` hook to track when the embed is reopened.
- Updated `BookerWebWrapper` to reset event firing state upon embed reopening.
- Refactored event firing logic to use `bookerViewed` instead of `bookerLoadedEvent`.
- Introduced scheduling for event firing in `useSchedule` to ensure correct order of events during prerendering.

* feat: add lifecycle diagrams for inline and modal embeds

- Introduced `inline-embed-lifecycle.mermaid` and `modal-embed-lifecycle.mermaid` files to visualize the lifecycle events and states of inline and modal embeds.
- Updated `LIFECYCLE.md` to reference the new diagrams and provide a clearer explanation of the embed lifecycle processes.
- Added `modal-prerendering-flow.mermaid` to illustrate the prerendering flow for modal embeds.
- Enhanced the routing playground with new features and improved event handling for availability and booking events.

* refactor: update event handling for booker lifecycle events

- Replaced `availabilityLoaded` event with `bookerReady` to better reflect the state when the booker view is fully loaded and ready for interaction.
- Updated related documentation and diagrams to reflect changes in event triggers and descriptions.
- Adjusted internal state management to track `viewId` instead of `reopenCount` for distinguishing between initial views and reopens.
- Added tests for new event handling logic to ensure correct firing of `bookerViewed`, `bookerReopened`, and `bookerReady` events.

* refactor: enhance embed event handling and state management

- Updated event handling for booker lifecycle events, replacing `resetViewVariables` with `resetPageData` to manage page-specific state.
- Introduced new utility functions for managing event firing states and reload initiation.
- Refactored `fireBookerViewedEvent` and `fireBookerReadyEvent` to utilize the new state management functions.
- Added comprehensive tests for the updated event handling logic and state resets to ensure correct functionality across various scenarios.

* refactor: update embed iframe configuration and utility functions

- Reduced `slotsStaleTimeMs` from 30 seconds to 10 seconds and `iframeForceReloadThresholdMs` from 100 seconds to 30 seconds for improved responsiveness.
- Refactored utility functions to use `isBrowser` for client-side checks instead of `isClientSide`.
- Removed unused `isPrerendering` function and updated related documentation for clarity.
- Enhanced event handling by exporting `useBookerEmbedEvents` from the appropriate module for better accessibility.

* refactor: update embed iframe configuration and utility functions

- Reduced `slotsStaleTimeMs` from 30 seconds to 10 seconds and `iframeForceReloadThresholdMs` from 100 seconds to 30 seconds for improved responsiveness.
- Refactored utility functions to use `isBrowser` for client-side checks instead of `isClientSide`.
- Removed unused `isPrerendering` function and updated related documentation for clarity.
- Enhanced event handling by exporting `useBookerEmbedEvents` from the appropriate module for better accessibility.

* fix cubic feedback
2025-12-11 09:11:38 -03:00

323 lines
15 KiB
HTML

<html>
<head>
<title>Embed Playground - Routing Form</title>
<script type="text/javascript" src="./playground/lib/playground-init.ts"></script>
<script>
if (!location.search.includes("nonResponsive")) {
document.write('<meta name="viewport" content="width=device-width"/>');
}
(function addParamsAutomatically() {
const newSearchParams = new URLSearchParams(location.search);
let paramsChanged = false;
if (!newSearchParams.has("only")) {
newSearchParams.set("only", "ns:routingFormFullPrerender");
paramsChanged = true;
}
if (!newSearchParams.has("calOrigin")) {
newSearchParams.set("calOrigin", "http://acme.cal.local:3000");
paramsChanged = true;
}
if (!newSearchParams.has("cal.embed.logging")) {
newSearchParams.set("cal.embed.logging", "1");
paramsChanged = true;
}
if (paramsChanged) {
location.href = "?" + newSearchParams.toString();
}
})();
</script>
<script>
function embedSnippet() {
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
if (typeof namespace === "string") {
// Make sure that even after re-execution of the snippet, the namespace is not overridden
cal.ns[namespace] = cal.ns[namespace] || api;
p(cal.ns[namespace], ar);
p(cal, ['initNamespace', namespace])
} else p(cal, ar);
return;
}
p(cal, ar);
};
})(window, window.calOrigin + "/embed/embed.js", "init");
}
embedSnippet();
</script>
<style>
body {
background: linear-gradient(90deg,
rgba(120, 116, 186, 1) 0%,
rgba(221, 221, 255, 1) 41%,
rgba(148, 232, 249, 1) 100%);
}
.inline-embed-container {
/* border: 1px solid black; */
margin-bottom: 5px;
border-bottom: 1px solid;
}
.loader {
color: green;
}
* {
--cal-brand: gray;
}
</style>
</head>
<body>
<div style="display: flex; flex-direction: column; gap: 10px;">
<div id="cal-booking-routingFormFullPrerender">
<hr/>
<a style="display: block;" href="?only=ns:routingFormFullPrerender&debug=1&param.email=test@example.com">Routing Form - Prerender Headless Router itself queuing the form response</a>
NOTE: Pass query param param.formId=FORM_UID_HERE to test with a particular routing form. Easiest is to run seed-insights.ts script to create a seeded form and then use the form id here.
<p>1. As page is loading, we prerender the headless router for the email passed as param.email</p>
<p>2. Whenever email changes and onblur happens, the prerender is triggered for the new email and previous prerendered modal is removed</p>
<p>3. The prerender of the headless router queues the form response and doesn't really record it. When the actual booking is made, the form response is recorded from the queue</p>
<p>4. If the CTA click happens before the slots are considered stale(configurable via options.slotsStaleTimeMs), then it will open the prerendered modal without fetching the slots, otherwise it will fetch the slots(for the routedTeamMembers/contactOwner only) and till then skeleton will be shown</p>
<p>5. If the CTA click happens after iframeForceReloadThresholdMs has passed, then fresh headless router request is sent which could be really slow. It is important to do force reload after a certain time because the Routing Form itself could have changed in the meantime or Salesforce ownership might be available or some other change might have occured in Cal.com's side</p>
<p>6. slotsStaleTimeMs is set to 10 seconds(default is 1 minute) and iframeForceReloadThresholdMs is set to 30 seconds(default is 15 minutes) and they are considered from the time when the prerender/modal call was made</p>
<p>7. To avoid reaching the iframeForceReloadThresholdMs, user could prerender the router again and again judiciously</p>
<p>8. You can disable prerendering by passing the query param param.disablePrerender=true</p>
</p>
<div class="inline-embed-container">
<script>
(()=>{
if (window.only !== "ns:routingFormFullPrerender") {
return;
}
Cal("init", "routingFormFullPrerender", {
debug: true,
calOrigin: window.calOrigin,
});
function trackLog(message) {
console.log(`${performance.now().toFixed(2)}ms [Tracking] ${message}`);
}
let listenersAdded = false;
let isAvailabilityLoaded = false;
// Track API start
let apiStartTime = null;
const linkPrerenderedCallback = (e) => {
trackLog("Link Prerendered");
};
Cal.ns["routingFormFullPrerender"]("on", { action: "linkPrerendered", callback: linkPrerenderedCallback });
// Track latency b/w API execution and availability loading
const trackLatencyInShowingSlots = (namespace) => {
isAvailabilityLoaded = false;
apiStartTime = performance.now();
const trackAvailabilityLoaded = (source) => {
if (!apiStartTime) {
trackLog(`API start time not set - ${source}`);
return;
}
if (isAvailabilityLoaded) {
trackLog(`Availability already loaded - triggering again by: ${source}`);
return;
}
isAvailabilityLoaded = true;
const endTime = performance.now();
const timeDiff = endTime - apiStartTime;
trackLog(`Embed Modal API to Booker Ready: ${timeDiff.toFixed(2)}ms - triggered by: ${source}`);
};
trackLog(`Embed Modal API started at: ${apiStartTime.toFixed(2)}ms`);
// Setup availability tracking
isAvailabilityLoaded = false;
const api = namespace ? Cal.ns[namespace] : Cal;
const bookerViewedCallback = (e) => {
const data = e.detail.data;
if (data.slotsLoaded === true) {
trackAvailabilityLoaded("bookerViewed (slotsLoaded: true)");
} else {
trackLog("bookerViewed(slotsLoaded: false). Booker is ready but waiting for slots to load");
}
};
const bookerReopenedCallback = (e) => {
const data = e.detail.data;
if (data.slotsLoaded === true) {
trackAvailabilityLoaded("bookerReopened (slotsLoaded: true)");
} else {
trackLog("Booker was reopened and waiting for slots to load");
}
};
const bookerReadyCallback = () => {
trackAvailabilityLoaded("bookerReady event");
};
const bookerReloadedCallback = (e) => {
const data = e.detail.data;
if (data.slotsLoaded === true) {
trackAvailabilityLoaded("bookerReloaded (slotsLoaded: true)");
} else {
trackLog("Booker was reloaded and waiting for slots to load");
}
};
if (!listenersAdded) {
api("on", { action: "bookerViewed", callback: bookerViewedCallback });
api("on", { action: "bookerReady", callback: bookerReadyCallback });
api("on", { action: "bookerReopened", callback: bookerReopenedCallback });
api("on", { action: "bookerReloaded", callback: bookerReloadedCallback });
listenersAdded = true;
}
};
window.routingFormFullPrerender = {
buildRouterUrl: ({skills, location, name, email, formId}) => {
// Send `email` as well as `Email` because E2e Tests currently use `Email` and `email` is needed by contact owner lookup. It doesn't accept Uppercase Email
return `router?form=${formId}&Manager=fggfg&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`;
},
onFormSubmit: (e) => {
e.preventDefault();
if(!window.params.formId) {
alert("Form ID is not set. Please set the form ID using the param.formId query parameter. Easiest is to run seed-insights.ts script to create a seeded form and then use the form id here. ");
return false;
}
e.preventDefault();
const namespace = "routingFormFullPrerender";
trackLatencyInShowingSlots(namespace);
const skills = document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').value;
const location = document.getElementById('cal-booking-place-routingFormFullPrerender-select-location').value;
const name = document.getElementById('cal-booking-place-routingFormFullPrerender-input-name').value;
const email = document.getElementById('cal-booking-place-routingFormFullPrerender-input-email').value;
const routerUrl = window.routingFormFullPrerender.buildRouterUrl({skills, location, name, email, formId: window.params.formId});
Cal.ns[namespace]("modal", {
calLink: routerUrl,
calOrigin: window.calOrigin,
config: {
"cal.embed.pageType": "team.event.booking.slots",
},
});
}
}
})();
</script>
<div id="cal-booking-place-routingFormFullPrerender">
<div class="place"></div>
<form id="cal-booking-place-routingFormFullPrerender-form" onsubmit="window.routingFormFullPrerender.onFormSubmit(event)" style="display: flex; flex-direction: column; gap: 16px; max-width: 500px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<div style="display: flex; flex-direction: column; gap: 6px;">
<label for="cal-booking-place-routingFormFullPrerender-input-name" style="font-weight: 500; color: #333; font-size: 14px;">Name</label>
<input required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;" type="text" id="cal-booking-place-routingFormFullPrerender-input-name" placeholder="John Doe" />
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<label for="cal-booking-place-routingFormFullPrerender-input-email" style="font-weight: 500; color: #333; font-size: 14px;">Email</label>
<input required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;" type="email" id="cal-booking-place-routingFormFullPrerender-input-email" placeholder="john@example.com" />
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<label for="cal-booking-place-routingFormFullPrerender-select-skills" style="font-weight: 500; color: #333; font-size: 14px;">Skills</label>
<select required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: white;" id="cal-booking-place-routingFormFullPrerender-select-skills">
<option value="">Select a skill</option>
<option value="JavaScript">JavaScript</option>
<option value="React">React</option>
<option value="Node.js">Node.js</option>
<option value="Python">Python</option>
<option value="Sales">Sales</option>
</select>
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<label for="cal-booking-place-routingFormFullPrerender-select-location" style="font-weight: 500; color: #333; font-size: 14px;">Location</label>
<select required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: white;" id="cal-booking-place-routingFormFullPrerender-select-location">
<option value="">Select a location</option>
<option value="New York">New York</option>
<option value="London">London</option>
<option value="Tokyo">Tokyo</option>
<option value="Berlin">Berlin</option>
<option value="Remote">Remote</option>
</select>
</div>
<button type="submit" id="cta-routingFormFullPrerender" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 16px; font-weight: 500; cursor: pointer; margin-top: 8px; transition: background 0.2s;">Submit</button>
</form>
</div>
</div>
<script>
(()=>{
const prerender = ({skills, location, email, formId}) => {
Cal.ns.routingFormFullPrerender("prerender", {
// Email will be automatically passed as query param
calLink: window.routingFormFullPrerender.buildRouterUrl({skills, location, email, formId}),
type: "modal",
pageType: "team.event.booking.slots",
options: {
slotsStaleTimeMs: 10 * 1000,
iframeForceReloadThresholdMs: 100 * 1000,
},
});
}
function onSelectChange(e) {
const skills = document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').value;
const location = document.getElementById('cal-booking-place-routingFormFullPrerender-select-location').value;
const email = document.getElementById('cal-booking-place-routingFormFullPrerender-input-email').value;
function areFieldRequiredbyRoutingFormAvailable() {
return skills && location;
}
if (!areFieldRequiredbyRoutingFormAvailable()) {
return;
}
if (window.params.disablePrerender) {
return;
}
prerender({skills, location, email, formId: window.params.formId});
}
document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').onchange = onSelectChange;
document.getElementById('cal-booking-place-routingFormFullPrerender-select-location').onchange = onSelectChange;
})();
</script>
</div>
</div>
<script type="module" src="./playground/lib/playground.ts"></script>
</script>
</body>
</html>