Skip to content

Commit fb4cfa9

Browse files
authored
Merge pull request #67 from dubinc/iframe-srcdoc
Support outbound-domains for iframes with srcdoc
2 parents 887f973 + 8e9425f commit fb4cfa9

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
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 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>
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,67 @@ test.describe('Outbound domains tracking', () => {
4545
expect(iframeSrc).toContain('dub_id=test-click-id');
4646
});
4747

48+
test('should handle nested iframes inside srcdoc (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 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);
107+
});
108+
48109
test('should not add tracking to links on the same domain', async ({
49110
page,
50111
}) => {

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,33 @@ const initOutboundDomains = () => {
4949

5050
// Get all links and iframes
5151
const elements = document.querySelectorAll('a[href], iframe[src]');
52-
if (!elements || elements.length === 0) return;
5352

54-
elements.forEach((element) => {
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;
77+
78+
allElements.forEach((element) => {
5579
// Skip already processed elements
5680
if (outboundLinksUpdated.has(element)) return;
5781

0 commit comments

Comments
 (0)