From aa72d25faf7a26ac3e141411c562b276a0321275 Mon Sep 17 00:00:00 2001 From: Matt Bodle Date: Fri, 14 Aug 2026 09:12:26 +1000 Subject: [PATCH 1/2] feat: allow a full origin in domain so the launcher can load from a bundled extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generateBaseUrl assumed domain was a bare host and always prepended https://, so the Kit could only fetch launcher.js (and the thank-you element) from a remote https host. Inside an MV3 browser extension that is remote hosted code, which the platform forbids — the extension must load the launcher from its own package. Treat a domain that already carries a scheme as a full origin and use it verbatim (trailing slashes trimmed), mirroring the existing generateReportingUrl handling. Extensions can now set mp.Rokt.domain to chrome-extension:///rokt and the Kit loads launcher.js from the bundle; http:// origins also work for local development. Bare hosts (CNAMEs) keep the https:// default. The ad-block probe is skipped for scheme-bearing origins where it would be malformed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Rokt-Kit.ts | 9 +++++++++ test/src/tests.spec.ts | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 0ba7f23..decb6af 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -318,6 +318,11 @@ function generateThankYouElementScript(domain: string | undefined) { function generateBaseUrl(domain: string | undefined) { const resolvedDomain = typeof domain !== 'undefined' ? domain : DEFAULT_ROKT_DOMAIN; + + if (resolvedDomain.includes('://')) { + return resolvedDomain.replace(/\/+$/, ''); + } + const protocol = 'https://'; return [protocol, resolvedDomain].join(''); @@ -511,6 +516,10 @@ function sendAdBlockMeasurementSignals(domain: string | undefined, version: stri return; } + if (domain && domain.includes('://')) { + return; + } + const pageUrl = window.location.href.split('?')[0].split('#')[0]; const params = 'version=' + diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 7edfc49..ff24048 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -4117,6 +4117,24 @@ describe('Rokt Forwarder', () => { ); }); + it('should use a chrome-extension origin verbatim so the launcher loads from the bundled extension', () => { + expect( + (window as any).mParticle.forwarder.testHelpers.generateLauncherScript('chrome-extension://abcdef123/rokt'), + ).toBe('chrome-extension://abcdef123/rokt/wsdk/integrations/launcher.js'); + }); + + it('should trim a trailing slash from a full origin so the path join stays clean', () => { + expect( + (window as any).mParticle.forwarder.testHelpers.generateLauncherScript('chrome-extension://abcdef123/rokt/'), + ).toBe('chrome-extension://abcdef123/rokt/wsdk/integrations/launcher.js'); + }); + + it('should preserve an http origin for local development', () => { + expect((window as any).mParticle.forwarder.testHelpers.generateLauncherScript('http://localhost:8001')).toBe( + 'http://localhost:8001/wsdk/integrations/launcher.js', + ); + }); + it('should return base URL when no extensions are provided', () => { const url = (window as any).mParticle.forwarder.testHelpers.generateLauncherScript(); expect(url).toBe(baseUrl); @@ -4165,6 +4183,13 @@ describe('Rokt Forwarder', () => { const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript('cname.rokt.com'); expect(url).toBe('https://cname.rokt.com/rokt-elements/rokt-element-thank-you.js'); }); + + it('should use a full custom origin verbatim', () => { + const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript( + 'chrome-extension://abcdef123/rokt', + ); + expect(url).toBe('chrome-extension://abcdef123/rokt/rokt-elements/rokt-element-thank-you.js'); + }); }); describe('#roktExtensions', () => { From 365c14325514d71869fa1389cad791b7e94004b8 Mon Sep 17 00:00:00 2001 From: Matt Bodle Date: Fri, 14 Aug 2026 14:54:36 +1000 Subject: [PATCH 2/2] fix: keep reporting + ad-block probes on the API domain for extension schemes Addresses review on the launcher-origin change: generateBaseUrl is shared with generateReportingUrl, so a chrome-extension:// domain would have sent logs/errors to chrome-extension:///rokt/v1/log. generateReportingUrl now falls back to the default API domain for non-http(s) schemes, leaving bare-host and http(s) reporting unchanged. The ad-block probe now only skips non-https scheme-bearing domains (extension / local) rather than every scheme, and builds its URL via generateBaseUrl so a full https origin is used verbatim instead of being double-prefixed (https://https://...). Adds tests: https origin still fires the probe, chrome-extension:// does not, and reporting falls back to the API domain for an extension integrationDomain while a full https integrationDomain is used verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Rokt-Kit.ts | 11 ++++++--- test/src/tests.spec.ts | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index decb6af..4010419 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -336,7 +336,10 @@ function generateReportingUrl(configuredUrl: string | undefined, domain: string return 'https://' + configuredUrl; } - return generateBaseUrl(domain) + endpoint; + const hasNonHttpScheme = domain?.includes('://') && !/^https?:\/\//i.test(domain); + const reportingDomain = hasNonHttpScheme ? undefined : domain; + + return generateBaseUrl(reportingDomain) + endpoint; } function loadRoktScript( @@ -516,7 +519,7 @@ function sendAdBlockMeasurementSignals(domain: string | undefined, version: stri return; } - if (domain && domain.includes('://')) { + if (domain && domain.includes('://') && !/^https:\/\//i.test(domain)) { return; } @@ -529,8 +532,8 @@ function sendAdBlockMeasurementSignals(domain: string | undefined, version: stri '&pageUrl=' + encodeURIComponent(pageUrl); - const existingDomain = domain || 'apps.rokt.com'; - createAutoRemovedIframe('https://' + existingDomain + '/v1/wsdk-init/index.html?' + params); + const existingBaseUrl = domain ? generateBaseUrl(domain) : 'https://apps.rokt.com'; + createAutoRemovedIframe(existingBaseUrl + '/v1/wsdk-init/index.html?' + params); createAutoRemovedIframe( 'https://' + ADBLOCK_CONTROL_DOMAIN + '/v1/wsdk-init/index.html?' + params + '&isControl=true', diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index ff24048..6d33747 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -6998,6 +6998,42 @@ describe('Rokt Forwarder', () => { expect(defaultDomainIframe).toBeTruthy(); }); + it('should still create the probe for a full https origin', () => { + Math.random = () => 0.05; + (window as any).__rokt_li_guid__ = 'test-guid-123'; + + (window as any).mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'https://custom.rokt.com', + 'test-version', + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call(iframes, (iframe: any) => iframe.src) as string[]; + + const httpsDomainIframe = srcs.find( + (src) => src.indexOf('https://custom.rokt.com/v1/wsdk-init/index.html') !== -1, + ); + + expect(httpsDomainIframe).toBeTruthy(); + }); + + it('should not create the probe for a chrome-extension origin', () => { + Math.random = () => 0.05; + (window as any).__rokt_li_guid__ = 'test-guid-123'; + + (window as any).mParticle.forwarder.testHelpers.sendAdBlockMeasurementSignals( + 'chrome-extension://abcdef123/rokt', + 'test-version', + ); + + const iframes = document.querySelectorAll('iframe'); + const srcs = Array.prototype.map.call(iframes, (iframe: any) => iframe.src) as string[]; + + const anyProbe = srcs.find((src) => src.indexOf('/v1/wsdk-init/index.html') !== -1); + + expect(anyProbe).toBeUndefined(); + }); + it('should not create iframes when sampled out', () => { Math.random = () => 0.5; // Above 0.1 threshold (window as any).__rokt_li_guid__ = 'test-guid-123'; @@ -7287,6 +7323,26 @@ describe('Rokt Forwarder', () => { expect(fetchCalls[0].url).toBe('https://apps.rokt-api.com/v1/errors'); }); + it('should use a full https integration domain for the error URL', () => { + const service = new ErrorReportingServiceClass( + { isLoggingEnabled: true, integrationDomain: 'https://custom.rokt.com' }, + '1.0.0', + 'test-guid', + ); + service.report({ message: 'test error', severity: WSDKErrorSeverityConst.ERROR }); + expect(fetchCalls[0].url).toBe('https://custom.rokt.com/v1/errors'); + }); + + it('should fall back to the default error URL for a chrome-extension integration domain', () => { + const service = new ErrorReportingServiceClass( + { isLoggingEnabled: true, integrationDomain: 'chrome-extension://abcdef123/rokt' }, + '1.0.0', + 'test-guid', + ); + service.report({ message: 'test error', severity: WSDKErrorSeverityConst.ERROR }); + expect(fetchCalls[0].url).toBe('https://apps.rokt-api.com/v1/errors'); + }); + it('should include all required fields in the log request body', () => { const service = new ErrorReportingServiceClass({ isLoggingEnabled: true }, 'test-integration', 'test-guid'); service.report({