Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/nextjs/app/outbound/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default function Outbound() {
<iframe src="https://example.com/embed"></iframe>
<iframe src="https://www.example.com/embed"></iframe>

{/* Cal.com style iframe with srcdoc containing nested iframes */}
<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>
<iframe srcdoc='<div>Another srcdoc with nested content<iframe src="https://wildcard.com/scheduler"></iframe></div>'></iframe>

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

Expand Down
61 changes: 61 additions & 0 deletions apps/nextjs/tests/outbound-domains.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,67 @@ test.describe('Outbound domains tracking', () => {
expect(iframeSrc).toContain('dub_id=test-click-id');
});

test('should handle nested iframes inside srcdoc (Cal.com style)', async ({
page,
}) => {
await page.goto('/outbound?dub_id=test-click-id');

await page.waitForFunction(() => window._dubAnalytics !== undefined);

await page.waitForTimeout(2500);
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a fixed timeout can introduce flakiness and slows the test unnecessarily. Prefer waiting on a concrete condition (e.g., await page.waitForFunction(() => document.querySelector('iframe[srcdoc]')?.getAttribute('srcdoc')?.includes('dub_id='))) instead of an arbitrary delay.

Suggested change
await page.waitForTimeout(2500);
await page.waitForFunction(() => {
const iframes = Array.from(document.querySelectorAll('iframe[srcdoc]'));
return iframes.some(iframe =>
iframe.getAttribute('srcdoc')?.includes('dub_id=test-click-id')
);
});

Copilot uses AI. Check for mistakes.

// Check that nested iframes inside srcdoc get tracking parameters
// This tests the contentDocument access functionality
const nestedIframeCheck = await page.evaluate(() => {
const srcdocIframes = document.querySelectorAll('iframe[srcdoc]');
const results = [];

srcdocIframes.forEach((srcdocIframe, index) => {
try {
const contentDoc = srcdocIframe.contentDocument;
if (contentDoc) {
const nestedIframes = contentDoc.querySelectorAll('iframe[src]');
nestedIframes.forEach((nestedIframe) => {
results.push({
index,
src: nestedIframe.src,
hasTracking: nestedIframe.src.includes('dub_id=test-click-id'),
});
});
}
} catch (e) {
results.push({ index, error: e.message });
}
});

return results;
});

// Verify that nested iframes were found and have tracking parameters
expect(nestedIframeCheck.length).toBeGreaterThan(0);

// Check that at least some nested iframes have tracking
const trackedIframes = nestedIframeCheck.filter(
(result) => result.hasTracking,
);
expect(trackedIframes.length).toBeGreaterThan(0);

// Verify specific URLs got tracking
const exampleTracked = nestedIframeCheck.some(
(result) =>
result.src &&
result.src.includes('example.com/booking-widget?dub_id=test-click-id'),
);
const otherTracked = nestedIframeCheck.some(
(result) =>
result.src &&
result.src.includes('other.com/calendar?dub_id=test-click-id'),
);

expect(exampleTracked).toBe(true);
expect(otherTracked).toBe(true);
});

test('should not add tracking to links on the same domain', async ({
page,
}) => {
Expand Down
28 changes: 26 additions & 2 deletions packages/script/src/extensions/outbound-domains.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,33 @@ const initOutboundDomains = () => {

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

elements.forEach((element) => {
// Also get nested iframes inside srcdoc iframes
const srcdocIframes = document.querySelectorAll('iframe[srcdoc]');
const nestedElements = [];

srcdocIframes.forEach((srcdocIframe) => {
try {
// Access the content document of the srcdoc iframe
const contentDoc = srcdocIframe.contentDocument;
if (contentDoc) {
// Find iframes and links inside the srcdoc content
const nestedIframes = contentDoc.querySelectorAll('iframe[src]');
const nestedLinks = contentDoc.querySelectorAll('a[href]');

nestedElements.push(...nestedIframes, ...nestedLinks);
}
} catch (e) {
// contentDocument access might fail due to CORS or other security restrictions
console.warn('Could not access contentDocument of srcdoc iframe:', e);
}
});
Comment thread
devkiran marked this conversation as resolved.

// Combine all elements
const allElements = [...elements, ...nestedElements];
if (!allElements || allElements.length === 0) return;

allElements.forEach((element) => {
// Skip already processed elements
if (outboundLinksUpdated.has(element)) return;

Expand Down