Skip to content

Commit 7c60c31

Browse files
committed
Support outbound-domains for iframes with srcdoc
Extend outbound domain tracking to handle iframes with srcdoc attributes, such as those used by Cal.com. The script now detects and updates URLs within srcdoc content to append the tracking parameter. Corresponding tests and example usage have been added to ensure correct behavior.
1 parent 887f973 commit 7c60c31

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed

apps/nextjs/app/outbound/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export default function Outbound() {
88
<iframe src="https://example.com/embed"></iframe>
99
<iframe src="https://www.example.com/embed"></iframe>
1010

11+
{/* Cal.com style iframe with srcdoc */}
12+
<iframe srcdoc='<html><body><h1>Cal.com Embed</h1><a href="https://example.com/booking">Book Now</a><script src="https://other.com/widget.js"></script></body></html>'></iframe>
13+
<iframe srcdoc='<div>Another srcdoc iframe with <a href="https://wildcard.com/test">link</a></div>'></iframe>
14+
1115
<a href="https://getacme.link/about">Internal Link</a>
1216
<div id="container"></div>
1317

apps/nextjs/tests/outbound-domains.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,40 @@ test.describe('Outbound domains tracking', () => {
4545
expect(iframeSrc).toContain('dub_id=test-click-id');
4646
});
4747

48+
test('should handle iframe srcdoc attributes (Cal.com style)', async ({
49+
page,
50+
}) => {
51+
await page.goto('/outbound?dub_id=test-click-id');
52+
53+
await page.waitForFunction(() => window._dubAnalytics !== undefined);
54+
55+
await page.waitForTimeout(2500);
56+
57+
// Check the first srcdoc iframe
58+
const srcdocIframes = await page.$$('iframe[srcdoc]');
59+
expect(srcdocIframes.length).toBeGreaterThan(0);
60+
61+
const firstSrcdocIframe = srcdocIframes[0];
62+
const srcdocContent = await firstSrcdocIframe?.getAttribute('srcdoc');
63+
64+
// Should contain the tracking parameter in the URLs within srcdoc
65+
expect(srcdocContent).toContain('dub_id=test-click-id');
66+
67+
// Check that both example.com and other.com URLs got the tracking parameter
68+
expect(srcdocContent).toContain('example.com/booking?dub_id=test-click-id');
69+
expect(srcdocContent).toContain('other.com/widget.js?dub_id=test-click-id');
70+
71+
// Check the second srcdoc iframe
72+
if (srcdocIframes.length > 1) {
73+
const secondSrcdocIframe = srcdocIframes[1];
74+
const secondSrcdocContent =
75+
await secondSrcdocIframe?.getAttribute('srcdoc');
76+
expect(secondSrcdocContent).toContain(
77+
'wildcard.com/test?dub_id=test-click-id',
78+
);
79+
}
80+
});
81+
4882
test('should not add tracking to links on the same domain', async ({
4983
page,
5084
}) => {

packages/script/src/extensions/outbound-domains.js

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,37 @@ const initOutboundDomains = () => {
3131
}
3232
}
3333

34+
function extractUrlFromSrcdoc(srcdoc) {
35+
if (!srcdoc) return null;
36+
37+
// Look for URLs in the srcdoc content
38+
// This is a basic implementation - may need refinement based on actual Cal.com usage
39+
const urlRegex = /https?:\/\/[^\s"'<>]+/g;
40+
const matches = srcdoc.match(urlRegex);
41+
42+
if (matches && matches.length > 0) {
43+
// Return the first URL found - this might need to be more sophisticated
44+
// depending on how Cal.com structures their srcdoc content
45+
return matches[0];
46+
}
47+
48+
return null;
49+
}
50+
51+
function updateSrcdocWithTracking(element, originalUrl, newUrl) {
52+
if (!element.srcdoc) return false;
53+
54+
try {
55+
// Replace the original URL with the new URL in the srcdoc content
56+
const updatedSrcdoc = element.srcdoc.replace(originalUrl, newUrl);
57+
element.srcdoc = updatedSrcdoc;
58+
return true;
59+
} catch (e) {
60+
console.error('Error updating srcdoc:', e);
61+
return false;
62+
}
63+
}
64+
3465
function addOutboundTracking(clickId) {
3566
// Handle both string and array configurations for outbound domains
3667
const outboundDomains = Array.isArray(DOMAINS_CONFIG.outbound)
@@ -47,16 +78,19 @@ const initOutboundDomains = () => {
4778
const existingCookie = clickId || cookieManager.get(DUB_ID_VAR);
4879
if (!existingCookie) return;
4980

50-
// Get all links and iframes
51-
const elements = document.querySelectorAll('a[href], iframe[src]');
81+
// Get all links and iframes (including those with srcdoc)
82+
const elements = document.querySelectorAll(
83+
'a[href], iframe[src], iframe[srcdoc]',
84+
);
5285
if (!elements || elements.length === 0) return;
5386

5487
elements.forEach((element) => {
5588
// Skip already processed elements
5689
if (outboundLinksUpdated.has(element)) return;
5790

5891
try {
59-
const urlString = element.href || element.src;
92+
const urlString =
93+
element.href || element.src || extractUrlFromSrcdoc(element.srcdoc);
6094
if (!urlString) return;
6195

6296
// Check if the URL matches any of our outbound domains
@@ -70,15 +104,29 @@ const initOutboundDomains = () => {
70104
// Only add the tracking parameter if it's not already present
71105
if (!url.searchParams.has(DUB_ID_VAR)) {
72106
url.searchParams.set(DUB_ID_VAR, existingCookie);
107+
const newUrlString = url.toString();
73108

74109
// Update the appropriate attribute based on element type
75110
if (element.tagName.toLowerCase() === 'a') {
76-
element.href = url.toString();
111+
element.href = newUrlString;
112+
outboundLinksUpdated.add(element);
77113
} else if (element.tagName.toLowerCase() === 'iframe') {
78-
element.src = url.toString();
114+
if (element.src) {
115+
// Standard iframe with src attribute
116+
element.src = newUrlString;
117+
outboundLinksUpdated.add(element);
118+
} else if (element.srcdoc) {
119+
// Iframe with srcdoc attribute (like Cal.com)
120+
const updated = updateSrcdocWithTracking(
121+
element,
122+
urlString,
123+
newUrlString,
124+
);
125+
if (updated) {
126+
outboundLinksUpdated.add(element);
127+
}
128+
}
79129
}
80-
81-
outboundLinksUpdated.add(element);
82130
}
83131
} catch (e) {
84132
console.error('Error processing element:', e);

0 commit comments

Comments
 (0)