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
This commit is contained in:
Hariom Balhara
2025-12-11 09:11:38 -03:00
committed by GitHub
parent f6642c17f8
commit 6f0c09d2be
21 changed files with 2105 additions and 609 deletions
@@ -97,35 +97,10 @@
<body>
<div style="display: flex; flex-direction: column; gap: 10px;">
<div id="cal-booking-place-routingFormWithoutPrerender">
<a href="?only=ns:routingFormWithoutPrerender">Routing Form Without Prerender Demo</a>
<form id="cal-booking-place-routingFormWithoutPrerender-form">
<input type="text" name="name" placeholder="John Doe" />
<input type="email" name="email" placeholder="john@example.com" />
<select name="skills" placeholder="JavaScript, Node.js">
<option value="JavaScript">JavaScript</option>
<option value="Sales">Sales</option>
</select>
</form>
<button id="cal-booking-place-routingFormWithoutPrerender-submit" data-cal-namespace="routingFormWithoutPrerender" data-cal-config='{"cal.embed.pageType":"team.event.booking.slots", "guests":["guest1@example.com", "guest2@example.com"]}'>Submit</button>
<script>
requestAnimationFrame(function updateSubmitButtonLink() {
const seededFormAcmeId = "948ae412-d995-4865-885a-48302588de03";
const form = document.getElementById("cal-booking-place-routingFormWithoutPrerender-form");
const name = form.querySelector("input[name='name']").value;
const email = form.querySelector("input[name='email']").value;
const skills = form.querySelector("select[name='skills']").value;
if (name && email) {
document.getElementById("cal-booking-place-routingFormWithoutPrerender-submit").setAttribute("data-cal-link", `router?form=${seededFormAcmeId}&email=${email}&name=${name}&Location=London&Department=Engineering&Rating=5&skills=${skills}&Email=${email}`);
}
requestAnimationFrame(updateSubmitButtonLink);
});
</script>
</div>
<div id="cal-booking-routingFormFullPrerender">
<hr/>
<a 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
<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>
@@ -133,6 +108,7 @@
<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>
@@ -144,21 +120,115 @@
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}&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`;
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.routingFormFullPrerender("modal", {
Cal.ns[namespace]("modal", {
calLink: routerUrl,
calOrigin: window.calOrigin,
config: {
"cal.embed.pageType": "team.event.booking.slots",
},
});
}
}
@@ -166,33 +236,44 @@
</script>
<div id="cal-booking-place-routingFormFullPrerender">
<div class="place"></div>
<form id="cal-booking-place-routingFormFullPrerender-form" onsubmit="window.routingFormFullPrerender.onFormSubmit(event)">
<label for="cal-booking-place-routingFormFullPrerender-input-name">Name</label>
<input required style="width: 400px" type="text" id="cal-booking-place-routingFormFullPrerender-input-name" placeholder="John Doe" />
<label for="cal-booking-place-routingFormFullPrerender-input-email">Email</label>
<input required style="width: 400px" type="email" id="cal-booking-place-routingFormFullPrerender-input-email" placeholder="john@example.com" />
<label for="cal-booking-place-routingFormFullPrerender-select-skills">Skills</label>
<select required style="width: 400px" 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>
<label for="cal-booking-place-routingFormFullPrerender-select-location">Location</label>
<select required style="width: 400px" 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>
<button type="submit" id="cta-routingFormFullPrerender">Submit</button>
</form>
</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>
@@ -206,7 +287,7 @@
pageType: "team.event.booking.slots",
options: {
slotsStaleTimeMs: 10 * 1000,
iframeForceReloadThresholdMs: 30 * 1000,
iframeForceReloadThresholdMs: 100 * 1000,
},
});
}
@@ -223,6 +304,10 @@
return;
}
if (window.params.disablePrerender) {
return;
}
prerender({skills, location, email, formId: window.params.formId});
}
document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').onchange = onSelectChange;