From 7c60c31de1ee790b2765680de20b49058d51fe91 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Oct 2025 12:43:19 -0700 Subject: [PATCH 1/2] 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. --- apps/nextjs/app/outbound/page.tsx | 4 ++ apps/nextjs/tests/outbound-domains.spec.ts | 34 ++++++++++ .../script/src/extensions/outbound-domains.js | 62 ++++++++++++++++--- 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/apps/nextjs/app/outbound/page.tsx b/apps/nextjs/app/outbound/page.tsx index 00071a6..4e2ec25 100644 --- a/apps/nextjs/app/outbound/page.tsx +++ b/apps/nextjs/app/outbound/page.tsx @@ -8,6 +8,10 @@ export default function Outbound() { + {/* Cal.com style iframe with srcdoc */} + + + Internal Link
diff --git a/apps/nextjs/tests/outbound-domains.spec.ts b/apps/nextjs/tests/outbound-domains.spec.ts index 286f869..e97b12f 100644 --- a/apps/nextjs/tests/outbound-domains.spec.ts +++ b/apps/nextjs/tests/outbound-domains.spec.ts @@ -45,6 +45,40 @@ test.describe('Outbound domains tracking', () => { expect(iframeSrc).toContain('dub_id=test-click-id'); }); + test('should handle iframe srcdoc attributes (Cal.com style)', async ({ + page, + }) => { + await page.goto('/outbound?dub_id=test-click-id'); + + await page.waitForFunction(() => window._dubAnalytics !== undefined); + + await page.waitForTimeout(2500); + + // Check the first srcdoc iframe + const srcdocIframes = await page.$$('iframe[srcdoc]'); + expect(srcdocIframes.length).toBeGreaterThan(0); + + const firstSrcdocIframe = srcdocIframes[0]; + const srcdocContent = await firstSrcdocIframe?.getAttribute('srcdoc'); + + // Should contain the tracking parameter in the URLs within srcdoc + expect(srcdocContent).toContain('dub_id=test-click-id'); + + // Check that both example.com and other.com URLs got the tracking parameter + expect(srcdocContent).toContain('example.com/booking?dub_id=test-click-id'); + expect(srcdocContent).toContain('other.com/widget.js?dub_id=test-click-id'); + + // Check the second srcdoc iframe + if (srcdocIframes.length > 1) { + const secondSrcdocIframe = srcdocIframes[1]; + const secondSrcdocContent = + await secondSrcdocIframe?.getAttribute('srcdoc'); + expect(secondSrcdocContent).toContain( + 'wildcard.com/test?dub_id=test-click-id', + ); + } + }); + test('should not add tracking to links on the same domain', async ({ page, }) => { diff --git a/packages/script/src/extensions/outbound-domains.js b/packages/script/src/extensions/outbound-domains.js index b0119f1..0e686c2 100644 --- a/packages/script/src/extensions/outbound-domains.js +++ b/packages/script/src/extensions/outbound-domains.js @@ -31,6 +31,37 @@ const initOutboundDomains = () => { } } + function extractUrlFromSrcdoc(srcdoc) { + if (!srcdoc) return null; + + // Look for URLs in the srcdoc content + // This is a basic implementation - may need refinement based on actual Cal.com usage + const urlRegex = /https?:\/\/[^\s"'<>]+/g; + const matches = srcdoc.match(urlRegex); + + if (matches && matches.length > 0) { + // Return the first URL found - this might need to be more sophisticated + // depending on how Cal.com structures their srcdoc content + return matches[0]; + } + + return null; + } + + function updateSrcdocWithTracking(element, originalUrl, newUrl) { + if (!element.srcdoc) return false; + + try { + // Replace the original URL with the new URL in the srcdoc content + const updatedSrcdoc = element.srcdoc.replace(originalUrl, newUrl); + element.srcdoc = updatedSrcdoc; + return true; + } catch (e) { + console.error('Error updating srcdoc:', e); + return false; + } + } + function addOutboundTracking(clickId) { // Handle both string and array configurations for outbound domains const outboundDomains = Array.isArray(DOMAINS_CONFIG.outbound) @@ -47,8 +78,10 @@ const initOutboundDomains = () => { const existingCookie = clickId || cookieManager.get(DUB_ID_VAR); if (!existingCookie) return; - // Get all links and iframes - const elements = document.querySelectorAll('a[href], iframe[src]'); + // Get all links and iframes (including those with srcdoc) + const elements = document.querySelectorAll( + 'a[href], iframe[src], iframe[srcdoc]', + ); if (!elements || elements.length === 0) return; elements.forEach((element) => { @@ -56,7 +89,8 @@ const initOutboundDomains = () => { if (outboundLinksUpdated.has(element)) return; try { - const urlString = element.href || element.src; + const urlString = + element.href || element.src || extractUrlFromSrcdoc(element.srcdoc); if (!urlString) return; // Check if the URL matches any of our outbound domains @@ -70,15 +104,29 @@ const initOutboundDomains = () => { // Only add the tracking parameter if it's not already present if (!url.searchParams.has(DUB_ID_VAR)) { url.searchParams.set(DUB_ID_VAR, existingCookie); + const newUrlString = url.toString(); // Update the appropriate attribute based on element type if (element.tagName.toLowerCase() === 'a') { - element.href = url.toString(); + element.href = newUrlString; + outboundLinksUpdated.add(element); } else if (element.tagName.toLowerCase() === 'iframe') { - element.src = url.toString(); + if (element.src) { + // Standard iframe with src attribute + element.src = newUrlString; + outboundLinksUpdated.add(element); + } else if (element.srcdoc) { + // Iframe with srcdoc attribute (like Cal.com) + const updated = updateSrcdocWithTracking( + element, + urlString, + newUrlString, + ); + if (updated) { + outboundLinksUpdated.add(element); + } + } } - - outboundLinksUpdated.add(element); } } catch (e) { console.error('Error processing element:', e); From 8e9425f81d1ad3e34e2bdfd462b27746aaa32efc Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Oct 2025 13:32:04 -0700 Subject: [PATCH 2/2] update to use document.querySelector('iframe[srcdoc]').contentDocument.querySelectorAll('iframe[src]') approach --- apps/nextjs/app/outbound/page.tsx | 6 +- apps/nextjs/tests/outbound-domains.spec.ts | 75 +++++++++++----- .../script/src/extensions/outbound-domains.js | 90 +++++++------------ 3 files changed, 87 insertions(+), 84 deletions(-) diff --git a/apps/nextjs/app/outbound/page.tsx b/apps/nextjs/app/outbound/page.tsx index 4e2ec25..f2e5aa9 100644 --- a/apps/nextjs/app/outbound/page.tsx +++ b/apps/nextjs/app/outbound/page.tsx @@ -8,9 +8,9 @@ export default function Outbound() { - {/* Cal.com style iframe with srcdoc */} - - + {/* Cal.com style iframe with srcdoc containing nested iframes */} + + Internal Link
diff --git a/apps/nextjs/tests/outbound-domains.spec.ts b/apps/nextjs/tests/outbound-domains.spec.ts index e97b12f..06b12d2 100644 --- a/apps/nextjs/tests/outbound-domains.spec.ts +++ b/apps/nextjs/tests/outbound-domains.spec.ts @@ -45,7 +45,7 @@ test.describe('Outbound domains tracking', () => { expect(iframeSrc).toContain('dub_id=test-click-id'); }); - test('should handle iframe srcdoc attributes (Cal.com style)', async ({ + test('should handle nested iframes inside srcdoc (Cal.com style)', async ({ page, }) => { await page.goto('/outbound?dub_id=test-click-id'); @@ -54,29 +54,56 @@ test.describe('Outbound domains tracking', () => { await page.waitForTimeout(2500); - // Check the first srcdoc iframe - const srcdocIframes = await page.$$('iframe[srcdoc]'); - expect(srcdocIframes.length).toBeGreaterThan(0); - - const firstSrcdocIframe = srcdocIframes[0]; - const srcdocContent = await firstSrcdocIframe?.getAttribute('srcdoc'); - - // Should contain the tracking parameter in the URLs within srcdoc - expect(srcdocContent).toContain('dub_id=test-click-id'); - - // Check that both example.com and other.com URLs got the tracking parameter - expect(srcdocContent).toContain('example.com/booking?dub_id=test-click-id'); - expect(srcdocContent).toContain('other.com/widget.js?dub_id=test-click-id'); - - // Check the second srcdoc iframe - if (srcdocIframes.length > 1) { - const secondSrcdocIframe = srcdocIframes[1]; - const secondSrcdocContent = - await secondSrcdocIframe?.getAttribute('srcdoc'); - expect(secondSrcdocContent).toContain( - 'wildcard.com/test?dub_id=test-click-id', - ); - } + // 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 ({ diff --git a/packages/script/src/extensions/outbound-domains.js b/packages/script/src/extensions/outbound-domains.js index 0e686c2..8b3bd9b 100644 --- a/packages/script/src/extensions/outbound-domains.js +++ b/packages/script/src/extensions/outbound-domains.js @@ -31,37 +31,6 @@ const initOutboundDomains = () => { } } - function extractUrlFromSrcdoc(srcdoc) { - if (!srcdoc) return null; - - // Look for URLs in the srcdoc content - // This is a basic implementation - may need refinement based on actual Cal.com usage - const urlRegex = /https?:\/\/[^\s"'<>]+/g; - const matches = srcdoc.match(urlRegex); - - if (matches && matches.length > 0) { - // Return the first URL found - this might need to be more sophisticated - // depending on how Cal.com structures their srcdoc content - return matches[0]; - } - - return null; - } - - function updateSrcdocWithTracking(element, originalUrl, newUrl) { - if (!element.srcdoc) return false; - - try { - // Replace the original URL with the new URL in the srcdoc content - const updatedSrcdoc = element.srcdoc.replace(originalUrl, newUrl); - element.srcdoc = updatedSrcdoc; - return true; - } catch (e) { - console.error('Error updating srcdoc:', e); - return false; - } - } - function addOutboundTracking(clickId) { // Handle both string and array configurations for outbound domains const outboundDomains = Array.isArray(DOMAINS_CONFIG.outbound) @@ -78,19 +47,40 @@ const initOutboundDomains = () => { const existingCookie = clickId || cookieManager.get(DUB_ID_VAR); if (!existingCookie) return; - // Get all links and iframes (including those with srcdoc) - const elements = document.querySelectorAll( - 'a[href], iframe[src], iframe[srcdoc]', - ); - if (!elements || elements.length === 0) return; + // Get all links and iframes + const elements = document.querySelectorAll('a[href], iframe[src]'); + + // 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); + } + }); + + // Combine all elements + const allElements = [...elements, ...nestedElements]; + if (!allElements || allElements.length === 0) return; - elements.forEach((element) => { + allElements.forEach((element) => { // Skip already processed elements if (outboundLinksUpdated.has(element)) return; try { - const urlString = - element.href || element.src || extractUrlFromSrcdoc(element.srcdoc); + const urlString = element.href || element.src; if (!urlString) return; // Check if the URL matches any of our outbound domains @@ -104,29 +94,15 @@ const initOutboundDomains = () => { // Only add the tracking parameter if it's not already present if (!url.searchParams.has(DUB_ID_VAR)) { url.searchParams.set(DUB_ID_VAR, existingCookie); - const newUrlString = url.toString(); // Update the appropriate attribute based on element type if (element.tagName.toLowerCase() === 'a') { - element.href = newUrlString; - outboundLinksUpdated.add(element); + element.href = url.toString(); } else if (element.tagName.toLowerCase() === 'iframe') { - if (element.src) { - // Standard iframe with src attribute - element.src = newUrlString; - outboundLinksUpdated.add(element); - } else if (element.srcdoc) { - // Iframe with srcdoc attribute (like Cal.com) - const updated = updateSrcdocWithTracking( - element, - urlString, - newUrlString, - ); - if (updated) { - outboundLinksUpdated.add(element); - } - } + element.src = url.toString(); } + + outboundLinksUpdated.add(element); } } catch (e) { console.error('Error processing element:', e);