Skip to content

Commit 8e9425f

Browse files
committed
update to use document.querySelector('iframe[srcdoc]').contentDocument.querySelectorAll('iframe[src]') approach
1 parent 7c60c31 commit 8e9425f

File tree

3 files changed

+87
-84
lines changed

3 files changed

+87
-84
lines changed

apps/nextjs/app/outbound/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ 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>
11+
{/* Cal.com style iframe with srcdoc containing nested iframes */}
12+
<iframe srcdoc='<html><body><h1>Cal.com Embed</h1><iframe src="https://example.com/booking-widget"></iframe><iframe src="https://other.com/calendar"></iframe></body></html>'></iframe>
13+
<iframe srcdoc='<div>Another srcdoc with nested content<iframe src="https://wildcard.com/scheduler"></iframe></div>'></iframe>
1414

1515
<a href="https://getacme.link/about">Internal Link</a>
1616
<div id="container"></div>

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

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ 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 ({
48+
test('should handle nested iframes inside srcdoc (Cal.com style)', async ({
4949
page,
5050
}) => {
5151
await page.goto('/outbound?dub_id=test-click-id');
@@ -54,29 +54,56 @@ test.describe('Outbound domains tracking', () => {
5454

5555
await page.waitForTimeout(2500);
5656

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-
}
57+
// Check that nested iframes inside srcdoc get tracking parameters
58+
// This tests the contentDocument access functionality
59+
const nestedIframeCheck = await page.evaluate(() => {
60+
const srcdocIframes = document.querySelectorAll('iframe[srcdoc]');
61+
const results = [];
62+
63+
srcdocIframes.forEach((srcdocIframe, index) => {
64+
try {
65+
const contentDoc = srcdocIframe.contentDocument;
66+
if (contentDoc) {
67+
const nestedIframes = contentDoc.querySelectorAll('iframe[src]');
68+
nestedIframes.forEach((nestedIframe) => {
69+
results.push({
70+
index,
71+
src: nestedIframe.src,
72+
hasTracking: nestedIframe.src.includes('dub_id=test-click-id'),
73+
});
74+
});
75+
}
76+
} catch (e) {
77+
results.push({ index, error: e.message });
78+
}
79+
});
80+
81+
return results;
82+
});
83+
84+
// Verify that nested iframes were found and have tracking parameters
85+
expect(nestedIframeCheck.length).toBeGreaterThan(0);
86+
87+
// Check that at least some nested iframes have tracking
88+
const trackedIframes = nestedIframeCheck.filter(
89+
(result) => result.hasTracking,
90+
);
91+
expect(trackedIframes.length).toBeGreaterThan(0);
92+
93+
// Verify specific URLs got tracking
94+
const exampleTracked = nestedIframeCheck.some(
95+
(result) =>
96+
result.src &&
97+
result.src.includes('example.com/booking-widget?dub_id=test-click-id'),
98+
);
99+
const otherTracked = nestedIframeCheck.some(
100+
(result) =>
101+
result.src &&
102+
result.src.includes('other.com/calendar?dub_id=test-click-id'),
103+
);
104+
105+
expect(exampleTracked).toBe(true);
106+
expect(otherTracked).toBe(true);
80107
});
81108

82109
test('should not add tracking to links on the same domain', async ({

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

Lines changed: 33 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,6 @@ 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-
6534
function addOutboundTracking(clickId) {
6635
// Handle both string and array configurations for outbound domains
6736
const outboundDomains = Array.isArray(DOMAINS_CONFIG.outbound)
@@ -78,19 +47,40 @@ const initOutboundDomains = () => {
7847
const existingCookie = clickId || cookieManager.get(DUB_ID_VAR);
7948
if (!existingCookie) return;
8049

81-
// Get all links and iframes (including those with srcdoc)
82-
const elements = document.querySelectorAll(
83-
'a[href], iframe[src], iframe[srcdoc]',
84-
);
85-
if (!elements || elements.length === 0) return;
50+
// Get all links and iframes
51+
const elements = document.querySelectorAll('a[href], iframe[src]');
52+
53+
// Also get nested iframes inside srcdoc iframes
54+
const srcdocIframes = document.querySelectorAll('iframe[srcdoc]');
55+
const nestedElements = [];
56+
57+
srcdocIframes.forEach((srcdocIframe) => {
58+
try {
59+
// Access the content document of the srcdoc iframe
60+
const contentDoc = srcdocIframe.contentDocument;
61+
if (contentDoc) {
62+
// Find iframes and links inside the srcdoc content
63+
const nestedIframes = contentDoc.querySelectorAll('iframe[src]');
64+
const nestedLinks = contentDoc.querySelectorAll('a[href]');
65+
66+
nestedElements.push(...nestedIframes, ...nestedLinks);
67+
}
68+
} catch (e) {
69+
// contentDocument access might fail due to CORS or other security restrictions
70+
console.warn('Could not access contentDocument of srcdoc iframe:', e);
71+
}
72+
});
73+
74+
// Combine all elements
75+
const allElements = [...elements, ...nestedElements];
76+
if (!allElements || allElements.length === 0) return;
8677

87-
elements.forEach((element) => {
78+
allElements.forEach((element) => {
8879
// Skip already processed elements
8980
if (outboundLinksUpdated.has(element)) return;
9081

9182
try {
92-
const urlString =
93-
element.href || element.src || extractUrlFromSrcdoc(element.srcdoc);
83+
const urlString = element.href || element.src;
9484
if (!urlString) return;
9585

9686
// Check if the URL matches any of our outbound domains
@@ -104,29 +94,15 @@ const initOutboundDomains = () => {
10494
// Only add the tracking parameter if it's not already present
10595
if (!url.searchParams.has(DUB_ID_VAR)) {
10696
url.searchParams.set(DUB_ID_VAR, existingCookie);
107-
const newUrlString = url.toString();
10897

10998
// Update the appropriate attribute based on element type
11099
if (element.tagName.toLowerCase() === 'a') {
111-
element.href = newUrlString;
112-
outboundLinksUpdated.add(element);
100+
element.href = url.toString();
113101
} else if (element.tagName.toLowerCase() === 'iframe') {
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-
}
102+
element.src = url.toString();
129103
}
104+
105+
outboundLinksUpdated.add(element);
130106
}
131107
} catch (e) {
132108
console.error('Error processing element:', e);

0 commit comments

Comments
 (0)