Skip to content

Commit 6f0c09d

Browse files
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
1 parent f6642c1 commit 6f0c09d

21 files changed

+2074
-578
lines changed

packages/embeds/LIFECYCLE.md

Lines changed: 185 additions & 161 deletions
Large diffs are not rendered by default.

packages/embeds/embed-core/playground/lib/playground-init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const emailQueryParam = url.searchParams.get("param.email");
77
window.params = {
88
email: emailQueryParam,
99
formId: url.searchParams.get("param.formId"),
10+
disablePrerender: url.searchParams.get("param.disablePrerender") === "true",
1011
};
1112

1213
window.generateRandomHexColor = function generateRandomHexColor() {

packages/embeds/embed-core/playground/lib/playground.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Cal.config.forwardQueryParams = true;
77
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88
const callback = function (e: any) {
99
const detail = e.detail;
10-
console.log("Event: ", e.type, detail);
1110
};
1211

1312
// @ts-expect-error window.calOrigin is set in index.html
@@ -693,22 +692,22 @@ Cal("on", {
693692
callback: bookingSuccessfulV2Callback,
694693
});
695694

696-
const availabilityLoadedCallback = (e: EmbedEvent<"availabilityLoaded">) => {
695+
const bookerReadyCallback = (e: EmbedEvent<"bookerReady">) => {
697696
const data = e.detail.data;
698-
console.log("availabilityLoaded", {
697+
console.log("bookerReady", {
699698
eventId: data.eventId,
700699
eventSlug: data.eventSlug,
701700
});
702701

703702
Cal("off", {
704-
action: "availabilityLoaded",
705-
callback: availabilityLoadedCallback,
703+
action: "bookerReady",
704+
callback: bookerReadyCallback,
706705
});
707706
};
708707

709708
Cal("on", {
710-
action: "availabilityLoaded",
711-
callback: availabilityLoadedCallback,
709+
action: "bookerReady",
710+
callback: bookerReadyCallback,
712711
});
713712

714713
if (only === "all" || only === "ns:skeletonDemo") {

packages/embeds/embed-core/routing-playground.html

Lines changed: 142 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -97,42 +97,18 @@
9797

9898
<body>
9999
<div style="display: flex; flex-direction: column; gap: 10px;">
100-
<div id="cal-booking-place-routingFormWithoutPrerender">
101-
<a href="?only=ns:routingFormWithoutPrerender">Routing Form Without Prerender Demo</a>
102-
<form id="cal-booking-place-routingFormWithoutPrerender-form">
103-
<input type="text" name="name" placeholder="John Doe" />
104-
<input type="email" name="email" placeholder="[email protected]" />
105-
<select name="skills" placeholder="JavaScript, Node.js">
106-
<option value="JavaScript">JavaScript</option>
107-
<option value="Sales">Sales</option>
108-
</select>
109-
</form>
110-
<button id="cal-booking-place-routingFormWithoutPrerender-submit" data-cal-namespace="routingFormWithoutPrerender" data-cal-config='{"cal.embed.pageType":"team.event.booking.slots", "guests":["[email protected]", "[email protected]"]}'>Submit</button>
111-
<script>
112-
requestAnimationFrame(function updateSubmitButtonLink() {
113-
const seededFormAcmeId = "948ae412-d995-4865-885a-48302588de03";
114-
const form = document.getElementById("cal-booking-place-routingFormWithoutPrerender-form");
115-
const name = form.querySelector("input[name='name']").value;
116-
const email = form.querySelector("input[name='email']").value;
117-
const skills = form.querySelector("select[name='skills']").value;
118-
if (name && email) {
119-
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}`);
120-
}
121-
requestAnimationFrame(updateSubmitButtonLink);
122-
});
123-
</script>
124-
</div>
125100
<div id="cal-booking-routingFormFullPrerender">
126101
<hr/>
127-
<a href="?only=ns:routingFormFullPrerender&debug=1&[email protected]">Routing Form - Prerender Headless Router itself queuing the form response</a>
128-
NOTE: Pass query param param.formId=FORM_UID_HERE to test with a particular routing form
102+
<a style="display: block;" href="?only=ns:routingFormFullPrerender&debug=1&[email protected]">Routing Form - Prerender Headless Router itself queuing the form response</a>
103+
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.
129104
<p>1. As page is loading, we prerender the headless router for the email passed as param.email</p>
130105
<p>2. Whenever email changes and onblur happens, the prerender is triggered for the new email and previous prerendered modal is removed</p>
131106
<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>
132107
<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>
133108
<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>
134109
<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>
135110
<p>7. To avoid reaching the iframeForceReloadThresholdMs, user could prerender the router again and again judiciously</p>
111+
<p>8. You can disable prerendering by passing the query param param.disablePrerender=true</p>
136112
</p>
137113
<div class="inline-embed-container">
138114
<script>
@@ -144,55 +120,160 @@
144120
debug: true,
145121
calOrigin: window.calOrigin,
146122
});
123+
124+
function trackLog(message) {
125+
console.log(`${performance.now().toFixed(2)}ms [Tracking] ${message}`);
126+
}
127+
128+
let listenersAdded = false;
129+
let isAvailabilityLoaded = false;
130+
// Track API start
131+
let apiStartTime = null;
132+
133+
const linkPrerenderedCallback = (e) => {
134+
trackLog("Link Prerendered");
135+
};
136+
Cal.ns["routingFormFullPrerender"]("on", { action: "linkPrerendered", callback: linkPrerenderedCallback });
137+
138+
// Track latency b/w API execution and availability loading
139+
const trackLatencyInShowingSlots = (namespace) => {
140+
isAvailabilityLoaded = false;
141+
apiStartTime = performance.now();
142+
const trackAvailabilityLoaded = (source) => {
143+
if (!apiStartTime) {
144+
trackLog(`API start time not set - ${source}`);
145+
return;
146+
}
147+
if (isAvailabilityLoaded) {
148+
trackLog(`Availability already loaded - triggering again by: ${source}`);
149+
return;
150+
}
151+
152+
isAvailabilityLoaded = true;
153+
const endTime = performance.now();
154+
const timeDiff = endTime - apiStartTime;
155+
trackLog(`Embed Modal API to Booker Ready: ${timeDiff.toFixed(2)}ms - triggered by: ${source}`);
156+
};
157+
trackLog(`Embed Modal API started at: ${apiStartTime.toFixed(2)}ms`);
158+
159+
// Setup availability tracking
160+
isAvailabilityLoaded = false;
161+
const api = namespace ? Cal.ns[namespace] : Cal;
162+
163+
const bookerViewedCallback = (e) => {
164+
const data = e.detail.data;
165+
if (data.slotsLoaded === true) {
166+
trackAvailabilityLoaded("bookerViewed (slotsLoaded: true)");
167+
} else {
168+
trackLog("bookerViewed(slotsLoaded: false). Booker is ready but waiting for slots to load");
169+
}
170+
};
171+
172+
const bookerReopenedCallback = (e) => {
173+
const data = e.detail.data;
174+
if (data.slotsLoaded === true) {
175+
trackAvailabilityLoaded("bookerReopened (slotsLoaded: true)");
176+
} else {
177+
trackLog("Booker was reopened and waiting for slots to load");
178+
}
179+
};
180+
181+
const bookerReadyCallback = () => {
182+
trackAvailabilityLoaded("bookerReady event");
183+
};
184+
185+
const bookerReloadedCallback = (e) => {
186+
const data = e.detail.data;
187+
if (data.slotsLoaded === true) {
188+
trackAvailabilityLoaded("bookerReloaded (slotsLoaded: true)");
189+
} else {
190+
trackLog("Booker was reloaded and waiting for slots to load");
191+
}
192+
};
193+
194+
if (!listenersAdded) {
195+
api("on", { action: "bookerViewed", callback: bookerViewedCallback });
196+
api("on", { action: "bookerReady", callback: bookerReadyCallback });
197+
api("on", { action: "bookerReopened", callback: bookerReopenedCallback });
198+
api("on", { action: "bookerReloaded", callback: bookerReloadedCallback });
199+
listenersAdded = true;
200+
}
201+
};
202+
147203
window.routingFormFullPrerender = {
148204
buildRouterUrl: ({skills, location, name, email, formId}) => {
149205
// 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
150-
return `router?form=${formId}&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`;
206+
return `router?form=${formId}&Manager=fggfg&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`;
151207
},
152208
onFormSubmit: (e) => {
153209
e.preventDefault();
210+
211+
if(!window.params.formId) {
212+
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. ");
213+
return false;
214+
}
215+
e.preventDefault();
216+
217+
const namespace = "routingFormFullPrerender";
218+
trackLatencyInShowingSlots(namespace);
219+
154220
const skills = document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').value;
155221
const location = document.getElementById('cal-booking-place-routingFormFullPrerender-select-location').value;
156222
const name = document.getElementById('cal-booking-place-routingFormFullPrerender-input-name').value;
157223
const email = document.getElementById('cal-booking-place-routingFormFullPrerender-input-email').value;
158224
const routerUrl = window.routingFormFullPrerender.buildRouterUrl({skills, location, name, email, formId: window.params.formId});
159-
Cal.ns.routingFormFullPrerender("modal", {
225+
226+
Cal.ns[namespace]("modal", {
160227
calLink: routerUrl,
161228
calOrigin: window.calOrigin,
229+
config: {
230+
"cal.embed.pageType": "team.event.booking.slots",
231+
},
162232
});
163233
}
164234
}
165235
})();
166236
</script>
167237
<div id="cal-booking-place-routingFormFullPrerender">
168238
<div class="place"></div>
169-
<form id="cal-booking-place-routingFormFullPrerender-form" onsubmit="window.routingFormFullPrerender.onFormSubmit(event)">
170-
<label for="cal-booking-place-routingFormFullPrerender-input-name">Name</label>
171-
<input required style="width: 400px" type="text" id="cal-booking-place-routingFormFullPrerender-input-name" placeholder="John Doe" />
172-
<label for="cal-booking-place-routingFormFullPrerender-input-email">Email</label>
173-
<input required style="width: 400px" type="email" id="cal-booking-place-routingFormFullPrerender-input-email" placeholder="[email protected]" />
174-
<label for="cal-booking-place-routingFormFullPrerender-select-skills">Skills</label>
175-
<select required style="width: 400px" id="cal-booking-place-routingFormFullPrerender-select-skills">
176-
<option value="">Select a skill</option>
177-
<option value="JavaScript">JavaScript</option>
178-
<option value="React">React</option>
179-
<option value="Node.js">Node.js</option>
180-
<option value="Python">Python</option>
181-
<option value="Sales">Sales</option>
182-
</select>
183-
184-
<label for="cal-booking-place-routingFormFullPrerender-select-location">Location</label>
185-
<select required style="width: 400px" id="cal-booking-place-routingFormFullPrerender-select-location">
186-
<option value="">Select a location</option>
187-
<option value="New York">New York</option>
188-
<option value="London">London</option>
189-
<option value="Tokyo">Tokyo</option>
190-
<option value="Berlin">Berlin</option>
191-
<option value="Remote">Remote</option>
192-
</select>
193-
<button type="submit" id="cta-routingFormFullPrerender">Submit</button>
194-
</form>
195-
</div>
239+
<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);">
240+
<div style="display: flex; flex-direction: column; gap: 6px;">
241+
<label for="cal-booking-place-routingFormFullPrerender-input-name" style="font-weight: 500; color: #333; font-size: 14px;">Name</label>
242+
<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" />
243+
</div>
244+
245+
<div style="display: flex; flex-direction: column; gap: 6px;">
246+
<label for="cal-booking-place-routingFormFullPrerender-input-email" style="font-weight: 500; color: #333; font-size: 14px;">Email</label>
247+
<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="[email protected]" />
248+
</div>
249+
250+
<div style="display: flex; flex-direction: column; gap: 6px;">
251+
<label for="cal-booking-place-routingFormFullPrerender-select-skills" style="font-weight: 500; color: #333; font-size: 14px;">Skills</label>
252+
<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">
253+
<option value="">Select a skill</option>
254+
<option value="JavaScript">JavaScript</option>
255+
<option value="React">React</option>
256+
<option value="Node.js">Node.js</option>
257+
<option value="Python">Python</option>
258+
<option value="Sales">Sales</option>
259+
</select>
260+
</div>
261+
262+
<div style="display: flex; flex-direction: column; gap: 6px;">
263+
<label for="cal-booking-place-routingFormFullPrerender-select-location" style="font-weight: 500; color: #333; font-size: 14px;">Location</label>
264+
<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">
265+
<option value="">Select a location</option>
266+
<option value="New York">New York</option>
267+
<option value="London">London</option>
268+
<option value="Tokyo">Tokyo</option>
269+
<option value="Berlin">Berlin</option>
270+
<option value="Remote">Remote</option>
271+
</select>
272+
</div>
273+
274+
<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>
275+
</form>
276+
</div>
196277

197278
</div>
198279

@@ -206,7 +287,7 @@
206287
pageType: "team.event.booking.slots",
207288
options: {
208289
slotsStaleTimeMs: 10 * 1000,
209-
iframeForceReloadThresholdMs: 30 * 1000,
290+
iframeForceReloadThresholdMs: 100 * 1000,
210291
},
211292
});
212293
}
@@ -223,6 +304,10 @@
223304
return;
224305
}
225306

307+
if (window.params.disablePrerender) {
308+
return;
309+
}
310+
226311
prerender({skills, location, email, formId: window.params.formId});
227312
}
228313
document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').onchange = onSelectChange;

0 commit comments

Comments
 (0)