From a1111c8762a9f4c35e0261c92675ef13808dd74a Mon Sep 17 00:00:00 2001 From: Matt Bodle Date: Sat, 7 Mar 2026 10:26:39 -0500 Subject: [PATCH] feat: Measure ad-block rate via control domain init signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fire two hidden iframes from the mParticle kit during launcher initialization — one to the existing (potentially-blocked) domain and one to apps.roktecommerce.com (control domain) — to measure ad-block rates by comparing signal counts server-side. - Add createAutoRemovedIframe helper for hidden, self-removing iframes - Add sendAdBlockMeasurementSignals with 10% sampling, fired from initRoktLauncher after the launcher is attached (ensures window.__rokt_li_guid__ is reliably available) - Strip both query params and hash fragments from pageUrl - Expose helpers via testHelpers for unit testing - Add tests for iframe creation, sampling, GUID gating, fragment stripping, and integration-level init behavior Co-Authored-By: Claude Opus 4.6 --- dist/Rokt-Kit.common.js | 60 ++++++++ dist/Rokt-Kit.iife.js | 60 ++++++++ src/Rokt-Kit.js | 60 ++++++++ test/src/tests.js | 304 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 484 insertions(+) diff --git a/dist/Rokt-Kit.common.js b/dist/Rokt-Kit.common.js index bcaa980..51ad00c 100644 --- a/dist/Rokt-Kit.common.js +++ b/dist/Rokt-Kit.common.js @@ -259,6 +259,8 @@ var constructor = function () { ); launcherOptions.integrationName = self.integrationName; + self.domain = domain; + if (testMode) { self.testHelpers = { generateLauncherScript: generateLauncherScript, @@ -268,6 +270,8 @@ var constructor = function () { generateMappedEventLookup: generateMappedEventLookup, generateMappedEventAttributeLookup: generateMappedEventAttributeLookup, + sendAdBlockMeasurementSignals: sendAdBlockMeasurementSignals, + createAutoRemovedIframe: createAutoRemovedIframe, }; attachLauncher(accountId, launcherOptions); return; @@ -565,6 +569,9 @@ var constructor = function () { // Kit must be initialized before attaching to the Rokt manager self.isInitialized = true; + + sendAdBlockMeasurementSignals(self.domain, self.integrationName); + // Attaches the kit to the Rokt manager window.mParticle.Rokt.attachKit(self); processEventQueue(); @@ -659,6 +666,59 @@ var constructor = function () { window.mParticle.captureTiming(metricName); } } + + function createAutoRemovedIframe(src) { + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.src = src; + iframe.onload = function () { + iframe.onload = null; + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + }; + var target = document.body || document.head; + if (target) { + target.appendChild(iframe); + } + } + + var ADBLOCK_CONTROL_DOMAIN = 'apps.roktecommerce.com'; + var INIT_LOG_SAMPLING_RATE = 0.1; + + function sendAdBlockMeasurementSignals(domain, version) { + if (Math.random() >= INIT_LOG_SAMPLING_RATE) { + return; + } + + var guid = window.__rokt_li_guid__; + if (!guid) { + return; + } + + var pageUrl = window.location.href.split('?')[0].split('#')[0]; + var params = + 'version=' + + encodeURIComponent(version) + + '&launcherInstanceGuid=' + + encodeURIComponent(guid) + + '&pageUrl=' + + encodeURIComponent(pageUrl); + + var existingDomain = domain || 'apps.rokt.com'; + createAutoRemovedIframe( + 'https://' + existingDomain + '/v1/wsdk-init/index.html?' + params + ); + + createAutoRemovedIframe( + 'https://' + + ADBLOCK_CONTROL_DOMAIN + + '/v1/wsdk-init/index.html?' + + params + + '&isControl=true' + ); + } }; function generateIntegrationName(customIntegrationName) { diff --git a/dist/Rokt-Kit.iife.js b/dist/Rokt-Kit.iife.js index e0fa8a0..b26d912 100644 --- a/dist/Rokt-Kit.iife.js +++ b/dist/Rokt-Kit.iife.js @@ -258,6 +258,8 @@ var RoktKit = (function (exports) { ); launcherOptions.integrationName = self.integrationName; + self.domain = domain; + if (testMode) { self.testHelpers = { generateLauncherScript: generateLauncherScript, @@ -267,6 +269,8 @@ var RoktKit = (function (exports) { generateMappedEventLookup: generateMappedEventLookup, generateMappedEventAttributeLookup: generateMappedEventAttributeLookup, + sendAdBlockMeasurementSignals: sendAdBlockMeasurementSignals, + createAutoRemovedIframe: createAutoRemovedIframe, }; attachLauncher(accountId, launcherOptions); return; @@ -564,6 +568,9 @@ var RoktKit = (function (exports) { // Kit must be initialized before attaching to the Rokt manager self.isInitialized = true; + + sendAdBlockMeasurementSignals(self.domain, self.integrationName); + // Attaches the kit to the Rokt manager window.mParticle.Rokt.attachKit(self); processEventQueue(); @@ -658,6 +665,59 @@ var RoktKit = (function (exports) { window.mParticle.captureTiming(metricName); } } + + function createAutoRemovedIframe(src) { + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.src = src; + iframe.onload = function () { + iframe.onload = null; + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + }; + var target = document.body || document.head; + if (target) { + target.appendChild(iframe); + } + } + + var ADBLOCK_CONTROL_DOMAIN = 'apps.roktecommerce.com'; + var INIT_LOG_SAMPLING_RATE = 0.1; + + function sendAdBlockMeasurementSignals(domain, version) { + if (Math.random() >= INIT_LOG_SAMPLING_RATE) { + return; + } + + var guid = window.__rokt_li_guid__; + if (!guid) { + return; + } + + var pageUrl = window.location.href.split('?')[0].split('#')[0]; + var params = + 'version=' + + encodeURIComponent(version) + + '&launcherInstanceGuid=' + + encodeURIComponent(guid) + + '&pageUrl=' + + encodeURIComponent(pageUrl); + + var existingDomain = domain || 'apps.rokt.com'; + createAutoRemovedIframe( + 'https://' + existingDomain + '/v1/wsdk-init/index.html?' + params + ); + + createAutoRemovedIframe( + 'https://' + + ADBLOCK_CONTROL_DOMAIN + + '/v1/wsdk-init/index.html?' + + params + + '&isControl=true' + ); + } }; function generateIntegrationName(customIntegrationName) { diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 087fabc..1bee802 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -255,6 +255,8 @@ var constructor = function () { ); launcherOptions.integrationName = self.integrationName; + self.domain = domain; + if (testMode) { self.testHelpers = { generateLauncherScript: generateLauncherScript, @@ -264,6 +266,8 @@ var constructor = function () { generateMappedEventLookup: generateMappedEventLookup, generateMappedEventAttributeLookup: generateMappedEventAttributeLookup, + sendAdBlockMeasurementSignals: sendAdBlockMeasurementSignals, + createAutoRemovedIframe: createAutoRemovedIframe, }; attachLauncher(accountId, launcherOptions); return; @@ -561,6 +565,9 @@ var constructor = function () { // Kit must be initialized before attaching to the Rokt manager self.isInitialized = true; + + sendAdBlockMeasurementSignals(self.domain, self.integrationName); + // Attaches the kit to the Rokt manager window.mParticle.Rokt.attachKit(self); processEventQueue(); @@ -655,6 +662,59 @@ var constructor = function () { window.mParticle.captureTiming(metricName); } } + + function createAutoRemovedIframe(src) { + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.src = src; + iframe.onload = function () { + iframe.onload = null; + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + }; + var target = document.body || document.head; + if (target) { + target.appendChild(iframe); + } + } + + var ADBLOCK_CONTROL_DOMAIN = 'apps.roktecommerce.com'; + var INIT_LOG_SAMPLING_RATE = 0.1; + + function sendAdBlockMeasurementSignals(domain, version) { + if (Math.random() >= INIT_LOG_SAMPLING_RATE) { + return; + } + + var guid = window.__rokt_li_guid__; + if (!guid) { + return; + } + + var pageUrl = window.location.href.split('?')[0].split('#')[0]; + var params = + 'version=' + + encodeURIComponent(version) + + '&launcherInstanceGuid=' + + encodeURIComponent(guid) + + '&pageUrl=' + + encodeURIComponent(pageUrl); + + var existingDomain = domain || 'apps.rokt.com'; + createAutoRemovedIframe( + 'https://' + existingDomain + '/v1/wsdk-init/index.html?' + params + ); + + createAutoRemovedIframe( + 'https://' + + ADBLOCK_CONTROL_DOMAIN + + '/v1/wsdk-init/index.html?' + + params + + '&isControl=true' + ); + } }; function generateIntegrationName(customIntegrationName) { diff --git a/test/src/tests.js b/test/src/tests.js index d1bebee..74307c8 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -4399,4 +4399,308 @@ describe('Rokt Forwarder', () => { resultHash.should.equal('hashed-<48Test Event>-value'); }); }); + + describe('#createAutoRemovedIframe', () => { + beforeEach(() => { + window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true + ); + }); + + it('should create a hidden iframe with the given src and append it to the document', () => { + const src = 'https://example.com/test'; + window.mParticle.forwarder.testHelpers.createAutoRemovedIframe(src); + + const iframe = document.querySelector('iframe[src="' + src + '"]'); + iframe.should.be.ok(); + iframe.style.display.should.equal('none'); + iframe + .getAttribute('sandbox') + .should.equal('allow-scripts allow-same-origin'); + }); + + it('should remove the iframe from the DOM after it loads', (done) => { + const src = 'https://example.com/auto-remove-test'; + window.mParticle.forwarder.testHelpers.createAutoRemovedIframe(src); + + const iframe = document.querySelector('iframe[src="' + src + '"]'); + iframe.should.be.ok(); + + // Simulate load event + iframe.onload(); + + // iframe should be removed + const removed = document.querySelector('iframe[src="' + src + '"]'); + (removed === null).should.be.true(); + done(); + }); + }); + + describe('#sendAdBlockMeasurementSignals', () => { + let originalRandom; + + beforeEach(() => { + originalRandom = Math.random; + window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true + ); + // Clean up any iframes from previous tests + document.querySelectorAll('iframe').forEach((iframe) => { + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + }); + }); + + afterEach(() => { + Math.random = originalRandom; + delete window.__rokt_li_guid__; + // Clean up iframes + document.querySelectorAll('iframe').forEach((iframe) => { + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + }); + }); + + it('should create two iframes with correct URLs when sampled in and guid is set', () => { + Math.random = () => 0.05; // Below 0.1 threshold + window.__rokt_li_guid__ = 'test-guid-123'; + + window.mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'custom.rokt.com', + 'test-version' + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call( + iframes, + (iframe) => iframe.src + ); + + srcs.length.should.be.aboveOrEqual(2); + + const existingDomainIframe = srcs.find( + (src) => + src.indexOf('custom.rokt.com/v1/wsdk-init/index.html') !== + -1 + ); + const controlDomainIframe = srcs.find( + (src) => + src.indexOf( + 'apps.roktecommerce.com/v1/wsdk-init/index.html' + ) !== -1 + ); + + existingDomainIframe.should.be.ok(); + controlDomainIframe.should.be.ok(); + + existingDomainIframe.should.containEql('version=test-version'); + existingDomainIframe.should.containEql( + 'launcherInstanceGuid=test-guid-123' + ); + existingDomainIframe.should.not.containEql('isControl'); + + controlDomainIframe.should.containEql('version=test-version'); + controlDomainIframe.should.containEql( + 'launcherInstanceGuid=test-guid-123' + ); + controlDomainIframe.should.containEql('isControl=true'); + }); + + it('should use apps.rokt.com as the default domain when no domain is provided', () => { + Math.random = () => 0.05; + window.__rokt_li_guid__ = 'test-guid-123'; + + window.mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + undefined, + 'test-version' + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call( + iframes, + (iframe) => iframe.src + ); + + const defaultDomainIframe = srcs.find( + (src) => + src.indexOf('apps.rokt.com/v1/wsdk-init/index.html') !== + -1 && src.indexOf('apps.roktecommerce.com') === -1 + ); + + defaultDomainIframe.should.be.ok(); + }); + + it('should not create iframes when sampled out', () => { + Math.random = () => 0.5; // Above 0.1 threshold + window.__rokt_li_guid__ = 'test-guid-123'; + + const iframeCountBefore = + document.querySelectorAll('iframe').length; + + window.mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'apps.rokt.com', + 'test-version' + ); + + const iframeCountAfter = document.querySelectorAll('iframe').length; + iframeCountAfter.should.equal(iframeCountBefore); + }); + + it('should not create iframes when __rokt_li_guid__ is not set', () => { + Math.random = () => 0.05; + delete window.__rokt_li_guid__; + + const iframeCountBefore = + document.querySelectorAll('iframe').length; + + window.mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'apps.rokt.com', + 'test-version' + ); + + const iframeCountAfter = document.querySelectorAll('iframe').length; + iframeCountAfter.should.equal(iframeCountBefore); + }); + + it('should strip hash fragments from pageUrl', () => { + Math.random = () => 0.05; + window.__rokt_li_guid__ = 'test-guid-123'; + + // window.location.href in test env won't have a fragment, + // but we can verify the pageUrl param does not contain '#' + window.mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'apps.rokt.com', + 'test-version' + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call( + iframes, + (iframe) => iframe.src + ); + + srcs.forEach((src) => { + src.should.containEql('pageUrl='); + // Extract the pageUrl param value + const match = src.match(/pageUrl=([^&]*)/); + match.should.be.ok(); + const decodedPageUrl = decodeURIComponent(match[1]); + decodedPageUrl.should.not.containEql('#'); + decodedPageUrl.should.not.containEql('?'); + }); + }); + + it('should fire measurement signals during initRoktLauncher when guid exists', async () => { + Math.random = () => 0.05; + window.__rokt_li_guid__ = 'init-test-guid'; + + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKitCalled = false; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.attachKitCalled = true; + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.filters = { + userAttributesFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + + await mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call( + iframes, + (iframe) => iframe.src + ); + + const controlIframe = srcs.find( + (src) => + src.indexOf( + 'apps.roktecommerce.com/v1/wsdk-init/index.html' + ) !== -1 + ); + + controlIframe.should.be.ok(); + controlIframe.should.containEql( + 'launcherInstanceGuid=init-test-guid' + ); + }); + + it('should not fire measurement signals during init when guid is absent', async () => { + Math.random = () => 0.05; + delete window.__rokt_li_guid__; + + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKitCalled = false; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.attachKitCalled = true; + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.filters = { + userAttributesFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + + await mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call( + iframes, + (iframe) => iframe.src + ); + + const controlIframe = srcs.find( + (src) => + src.indexOf( + 'apps.roktecommerce.com/v1/wsdk-init/index.html' + ) !== -1 + ); + + (controlIframe === undefined).should.be.true(); + }); + }); });