From 4d6c4933222fb0a50f6abe0d3f9a9d43fb7b5705 Mon Sep 17 00:00:00 2001 From: Matt Bodle Date: Tue, 10 Mar 2026 11:36:46 -0400 Subject: [PATCH] feat: Measure ad-block rate via control domain init signal Fire two hidden iframes from the mParticle kit during launcher initialization to compare signal counts between the existing (potentially blocked) domain and a control domain (apps.roktecommerce.com). - Add createAutoRemovedIframe helper and sendAdBlockMeasurementSignals - Call from initRoktLauncher after GUID is reliably available - 10% sampling rate, strips query/hash from pageUrl - 9 new tests covering iframe creation, sampling, GUID gating, and integration Co-Authored-By: Claude Opus 4.6 --- src/Rokt-Kit.js | 78 +++++++++++ test/src/tests.js | 344 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 087fabc..2f5e621 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,12 @@ var constructor = function () { generateMappedEventLookup: generateMappedEventLookup, generateMappedEventAttributeLookup: generateMappedEventAttributeLookup, + sendAdBlockMeasurementSignals: sendAdBlockMeasurementSignals, + createAutoRemovedIframe: createAutoRemovedIframe, + djb2: djb2, + setAllowedOriginHash: function (hash) { + _allowedOriginHash = hash; + }, }; attachLauncher(accountId, launcherOptions); return; @@ -561,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(); @@ -655,6 +666,73 @@ 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; + var _allowedOriginHash = 1445747545; + + function djb2(str) { + var hash = 5381; + for (var i = 0; i < str.length; i++) { + hash = (hash << 5) + hash + str.charCodeAt(i); + hash = hash & hash; + } + return hash; + } + + function sendAdBlockMeasurementSignals(domain, version) { + if (djb2(window.location.origin) !== _allowedOriginHash) { + return; + } + + 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..6fa9269 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -4399,4 +4399,348 @@ 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 + ); + // Set allowed origin hash to match the test environment origin + const testOriginHash = window.mParticle.forwarder.testHelpers.djb2( + window.location.origin + ); + window.mParticle.forwarder.testHelpers.setAllowedOriginHash( + testOriginHash + ); + // 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 not create iframes when origin does not match allowed hash', () => { + Math.random = () => 0.05; + window.__rokt_li_guid__ = 'test-guid-123'; + + // Set to a hash that won't match any real origin + window.mParticle.forwarder.testHelpers.setAllowedOriginHash(0); + + 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'; + // Ensure origin hash matches test environment + const testOriginHash = window.mParticle.forwarder.testHelpers.djb2( + window.location.origin + ); + window.mParticle.forwarder.testHelpers.setAllowedOriginHash( + testOriginHash + ); + + 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__; + // Ensure origin hash matches test environment + const testOriginHash = window.mParticle.forwarder.testHelpers.djb2( + window.location.origin + ); + window.mParticle.forwarder.testHelpers.setAllowedOriginHash( + testOriginHash + ); + + 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(); + }); + }); });