From 13e8cf5a6d8d8f551555af708968ecd8cbebbc27 Mon Sep 17 00:00:00 2001 From: Ben Booth Date: Wed, 13 May 2026 14:34:27 +1000 Subject: [PATCH 1/2] feat(audience): remove sandbox env routing, add testMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes getBaseUrl() and the pk_imapik-test- prefix check that routed events to api.sandbox.immutable.com, which is being decommissioned. Adds testMode init option to both the pixel and SDK — when true, every outbound event carries a top-level test: true field so the backend can filter test data from production analytics. Co-Authored-By: Claude Sonnet 4.6 --- packages/audience/core/src/config.test.ts | 15 --------------- packages/audience/core/src/config.ts | 8 +------- packages/audience/core/src/consent.ts | 4 ++-- packages/audience/core/src/queue.ts | 4 ++-- packages/audience/core/src/types.ts | 2 ++ packages/audience/pixel/README.md | 17 +++++++++++++++++ packages/audience/pixel/src/pixel.ts | 7 ++++++- packages/audience/sdk/README.md | 12 ++++++++++++ packages/audience/sdk/src/sdk.ts | 4 ++++ packages/audience/sdk/src/types.ts | 2 ++ 10 files changed, 48 insertions(+), 27 deletions(-) delete mode 100644 packages/audience/core/src/config.test.ts diff --git a/packages/audience/core/src/config.test.ts b/packages/audience/core/src/config.test.ts deleted file mode 100644 index f1ef56c1ff..0000000000 --- a/packages/audience/core/src/config.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getBaseUrl } from './config'; - -describe('getBaseUrl', () => { - it('returns sandbox URL for test keys', () => { - expect(getBaseUrl('pk_imapik-test-local')).toBe('https://api.sandbox.immutable.com'); - }); - - it('returns production URL for live keys', () => { - expect(getBaseUrl('pk_imapik-abcdef123')).toBe('https://api.immutable.com'); - }); - - it('returns production URL for empty key', () => { - expect(getBaseUrl('')).toBe('https://api.immutable.com'); - }); -}); diff --git a/packages/audience/core/src/config.ts b/packages/audience/core/src/config.ts index 2105170d3e..d125edacf8 100644 --- a/packages/audience/core/src/config.ts +++ b/packages/audience/core/src/config.ts @@ -1,4 +1,4 @@ -const TEST_KEY_PREFIX = 'pk_imapik-test-'; +export const BASE_URL = 'https://api.immutable.com'; export const INGEST_PATH = '/v1/audience/messages'; export const CONSENT_PATH = '/v1/audience/tracking-consent'; @@ -13,9 +13,3 @@ export const SESSION_MAX_AGE = 30 * 60; // 30 minutes in seconds export const SESSION_START = 'session_start'; export const SESSION_END = 'session_end'; - -export const getBaseUrl = (publishableKey: string): string => ( - publishableKey.startsWith(TEST_KEY_PREFIX) - ? 'https://api.sandbox.immutable.com' - : 'https://api.immutable.com' -); diff --git a/packages/audience/core/src/consent.ts b/packages/audience/core/src/consent.ts index 53f78fdc49..3027591b5c 100644 --- a/packages/audience/core/src/consent.ts +++ b/packages/audience/core/src/consent.ts @@ -4,7 +4,7 @@ import type { import type { MessageQueue } from './queue'; import type { HttpSend } from './transport'; import { type AudienceError, invokeOnError, toAudienceError } from './errors'; -import { CONSENT_PATH, getBaseUrl } from './config'; +import { BASE_URL, CONSENT_PATH } from './config'; export interface ConsentManager { level: ConsentLevel; @@ -59,7 +59,7 @@ export function createConsentManager( const LEVELS: Record = { none: 0, anonymous: 1, full: 2 }; function notifyBackend(level: ConsentLevel): void { - const url = `${baseUrl ?? getBaseUrl(publishableKey)}${CONSENT_PATH}`; + const url = `${baseUrl ?? BASE_URL}${CONSENT_PATH}`; const payload: ConsentUpdatePayload = { anonymousId, status: level, source }; // Fire-and-forget. HttpSend never rejects, so the floating chain is safe. send(url, publishableKey, payload, { method: 'PUT', keepalive: true }) diff --git a/packages/audience/core/src/queue.ts b/packages/audience/core/src/queue.ts index f5eb1a112c..7013182ca7 100644 --- a/packages/audience/core/src/queue.ts +++ b/packages/audience/core/src/queue.ts @@ -2,7 +2,7 @@ import type { Message, BatchPayload } from './types'; import type { HttpSend } from './transport'; import { type AudienceError, invokeOnError, toAudienceError } from './errors'; import { - getBaseUrl, INGEST_PATH, FLUSH_INTERVAL_MS, FLUSH_SIZE, + BASE_URL, INGEST_PATH, FLUSH_INTERVAL_MS, FLUSH_SIZE, } from './config'; import * as storage from './storage'; import { isBrowser } from './utils'; @@ -87,7 +87,7 @@ export class MessageQueue { private readonly publishableKey: string, options?: MessageQueueOptions, ) { - this.endpointUrl = `${options?.baseUrl ?? getBaseUrl(publishableKey)}${INGEST_PATH}`; + this.endpointUrl = `${options?.baseUrl ?? BASE_URL}${INGEST_PATH}`; this.flushIntervalMs = options?.flushIntervalMs ?? FLUSH_INTERVAL_MS; this.flushSize = options?.flushSize ?? FLUSH_SIZE; this.onFlush = options?.onFlush; diff --git a/packages/audience/core/src/types.ts b/packages/audience/core/src/types.ts index 9e9e39aff2..f26bedfcbb 100644 --- a/packages/audience/core/src/types.ts +++ b/packages/audience/core/src/types.ts @@ -28,6 +28,8 @@ interface BaseMessage { anonymousId: string; surface: Surface; context: EventContext; + /** Present when the SDK/pixel is initialised with testMode: true. */ + test?: true; } export interface TrackMessage extends BaseMessage { diff --git a/packages/audience/pixel/README.md b/packages/audience/pixel/README.md index 6ebd349f6b..e7f1ee1102 100644 --- a/packages/audience/pixel/README.md +++ b/packages/audience/pixel/README.md @@ -115,6 +115,23 @@ document.head.appendChild(s); Both cookies are first-party (`SameSite=Lax`, `Secure` on HTTPS). +## Test Mode + +Set `testMode: true` when initialising the pixel in non-production environments (dev, staging). All events will include a top-level `test: true` field so they can be filtered from production analytics. + +```html + +``` + ## Content Security Policy (CSP) If your site uses a Content-Security-Policy header, add these origins to the relevant directives: diff --git a/packages/audience/pixel/src/pixel.ts b/packages/audience/pixel/src/pixel.ts index 3acc92a6cf..dd477fe734 100644 --- a/packages/audience/pixel/src/pixel.ts +++ b/packages/audience/pixel/src/pixel.ts @@ -38,6 +38,8 @@ export interface PixelInitOptions { autocapture?: AutocaptureOptions; /** Override the default API base URL. */ baseUrl?: string; + /** When true, all events are marked test: true and can be filtered from production analytics. */ + testMode?: boolean; } export class Pixel { @@ -55,6 +57,8 @@ export class Pixel { private domain: string | undefined; + private testMode = false; + private initialized = false; private unloadHandler?: () => void; @@ -80,6 +84,7 @@ export class Pixel { this.publishableKey = key; this.domain = domain; + this.testMode = options.testMode ?? false; this.queue = new MessageQueue( httpSend, @@ -320,7 +325,6 @@ export class Pixel { // -- Helpers ------------------------------------------------------------ - // eslint-disable-next-line class-methods-use-this private buildBase() { return { messageId: generateId(), @@ -328,6 +332,7 @@ export class Pixel { anonymousId: this.anonymousId, surface: 'pixel' as const, context: collectContext('@imtbl/pixel', PIXEL_VERSION), + ...(this.testMode && { test: true as const }), }; } diff --git a/packages/audience/sdk/README.md b/packages/audience/sdk/README.md index 0972cab28b..3c248d3adb 100644 --- a/packages/audience/sdk/README.md +++ b/packages/audience/sdk/README.md @@ -61,6 +61,18 @@ audience.shutdown(); ``` +## Test Mode + +Pass `testMode: true` when initialising the SDK in non-production environments (dev, staging). All events will include a top-level `test: true` field so they can be filtered from production analytics. + +```ts +const audience = Audience.init({ + publishableKey: 'YOUR_PUBLISHABLE_KEY', + consent: 'anonymous', + testMode: true, +}); +``` + ## Documentation - [Web SDK](https://docs.immutable.com/docs/products/audience/web-sdk) — API reference, usage, integration walkthrough diff --git a/packages/audience/sdk/src/sdk.ts b/packages/audience/sdk/src/sdk.ts index bb6b031dbb..a7fef1041c 100644 --- a/packages/audience/sdk/src/sdk.ts +++ b/packages/audience/sdk/src/sdk.ts @@ -60,6 +60,8 @@ export class Audience { private readonly cookieDomain?: string; + private readonly testMode: boolean; + private anonymousId: string; private sessionId: string | undefined; @@ -81,6 +83,7 @@ export class Audience { const consentSource = DEFAULT_CONSENT_SOURCE; this.cookieDomain = cookieDomain; + this.testMode = config.testMode ?? false; this.debug = new DebugLogger(config.debug ?? false); let isNewSession = false; @@ -173,6 +176,7 @@ export class Audience { anonymousId: this.anonymousId, surface: 'web' as const, context: collectContext(LIBRARY_NAME, LIBRARY_VERSION), + ...(this.testMode && { test: true as const }), }; } diff --git a/packages/audience/sdk/src/types.ts b/packages/audience/sdk/src/types.ts index 92a85f072c..54653042da 100644 --- a/packages/audience/sdk/src/types.ts +++ b/packages/audience/sdk/src/types.ts @@ -16,6 +16,8 @@ export interface AudienceConfig { flushSize?: number; /** Override the default API base URL. */ baseUrl?: string; + /** When true, all events are marked test: true and can be filtered from production analytics. */ + testMode?: boolean; /** * Called when the SDK fails to reach the backend. Receives a structured * {@link AudienceError} with a machine-readable `code` so studios can From 484a5fd20f908ed8f557d84ad7f816d138937360 Mon Sep 17 00:00:00 2001 From: Ben Booth Date: Wed, 13 May 2026 15:50:25 +1000 Subject: [PATCH 2/2] fix(audience): update tests for prod URL, drop testMode from public READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the 8 stale sandbox URL assertions in consent.test.ts and queue.test.ts that bugbot flagged — they referenced api.sandbox URLs that no longer get produced after the getBaseUrl removal. Drops the Test Mode README sections. The option is for internal/non-prod use, similar to baseUrl which is also undocumented. Still discoverable via TypeScript IntelliSense. Co-Authored-By: Claude Sonnet 4.6 --- packages/audience/core/src/consent.test.ts | 6 +++--- packages/audience/core/src/queue.test.ts | 10 +++++----- packages/audience/pixel/README.md | 17 ----------------- packages/audience/sdk/README.md | 12 ------------ 4 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/audience/core/src/consent.test.ts b/packages/audience/core/src/consent.test.ts index 4d5667d556..8c581e74f4 100644 --- a/packages/audience/core/src/consent.test.ts +++ b/packages/audience/core/src/consent.test.ts @@ -110,7 +110,7 @@ describe('createConsentManager', () => { manager.setLevel('anonymous'); expect(send).toHaveBeenCalledWith( - 'https://api.sandbox.immutable.com/v1/audience/tracking-consent', + 'https://api.immutable.com/v1/audience/tracking-consent', 'pk_imapik-test-local', { anonymousId: 'anon-1', status: 'anonymous', source: 'pixel' }, { method: 'PUT', keepalive: true }, @@ -146,7 +146,7 @@ describe('createConsentManager', () => { ok: false, error: new TransportError({ status: 503, - endpoint: 'https://api.sandbox.immutable.com/v1/audience/tracking-consent', + endpoint: 'https://api.immutable.com/v1/audience/tracking-consent', body: { code: 'SERVICE_UNAVAILABLE' }, }), }); @@ -172,7 +172,7 @@ describe('createConsentManager', () => { ok: false, error: new TransportError({ status: 0, - endpoint: 'https://api.sandbox.immutable.com/v1/audience/tracking-consent', + endpoint: 'https://api.immutable.com/v1/audience/tracking-consent', cause: new TypeError('Failed to fetch'), }), }); diff --git a/packages/audience/core/src/queue.test.ts b/packages/audience/core/src/queue.test.ts index 539d94a570..da9d515fe7 100644 --- a/packages/audience/core/src/queue.test.ts +++ b/packages/audience/core/src/queue.test.ts @@ -206,7 +206,7 @@ describe('MessageQueue', () => { const send = jest.fn, Parameters>().mockResolvedValue({ ok: false, error: new TransportError({ - status: 500, endpoint: 'https://api.sandbox.immutable.com/v1/audience/messages', body: null, + status: 500, endpoint: 'https://api.immutable.com/v1/audience/messages', body: null, }), }); const queue = createQueue(send, { onError }); @@ -228,7 +228,7 @@ describe('MessageQueue', () => { ok: false, error: new TransportError({ status: 0, - endpoint: 'https://api.sandbox.immutable.com/v1/audience/messages', + endpoint: 'https://api.immutable.com/v1/audience/messages', cause: new TypeError('Failed to fetch'), }), }); @@ -278,7 +278,7 @@ describe('MessageQueue', () => { ok: false, error: new TransportError({ status: 200, - endpoint: 'https://api.sandbox.immutable.com/v1/audience/messages', + endpoint: 'https://api.immutable.com/v1/audience/messages', body: { accepted: 1, rejected: 1 }, }), }); @@ -342,7 +342,7 @@ describe('page-unload flush (keepalive)', () => { document.dispatchEvent(new Event('visibilitychange')); expect(send).toHaveBeenCalledWith( - 'https://api.sandbox.immutable.com/v1/audience/messages', + 'https://api.immutable.com/v1/audience/messages', 'pk_imapik-test-local', expect.objectContaining({ messages: expect.any(Array) }), { keepalive: true }, @@ -366,7 +366,7 @@ describe('page-unload flush (keepalive)', () => { window.dispatchEvent(new Event('pagehide')); expect(send).toHaveBeenCalledWith( - 'https://api.sandbox.immutable.com/v1/audience/messages', + 'https://api.immutable.com/v1/audience/messages', 'pk_imapik-test-local', expect.objectContaining({ messages: expect.any(Array) }), { keepalive: true }, diff --git a/packages/audience/pixel/README.md b/packages/audience/pixel/README.md index e7f1ee1102..6ebd349f6b 100644 --- a/packages/audience/pixel/README.md +++ b/packages/audience/pixel/README.md @@ -115,23 +115,6 @@ document.head.appendChild(s); Both cookies are first-party (`SameSite=Lax`, `Secure` on HTTPS). -## Test Mode - -Set `testMode: true` when initialising the pixel in non-production environments (dev, staging). All events will include a top-level `test: true` field so they can be filtered from production analytics. - -```html - -``` - ## Content Security Policy (CSP) If your site uses a Content-Security-Policy header, add these origins to the relevant directives: diff --git a/packages/audience/sdk/README.md b/packages/audience/sdk/README.md index 3c248d3adb..0972cab28b 100644 --- a/packages/audience/sdk/README.md +++ b/packages/audience/sdk/README.md @@ -61,18 +61,6 @@ audience.shutdown(); ``` -## Test Mode - -Pass `testMode: true` when initialising the SDK in non-production environments (dev, staging). All events will include a top-level `test: true` field so they can be filtered from production analytics. - -```ts -const audience = Audience.init({ - publishableKey: 'YOUR_PUBLISHABLE_KEY', - consent: 'anonymous', - testMode: true, -}); -``` - ## Documentation - [Web SDK](https://docs.immutable.com/docs/products/audience/web-sdk) — API reference, usage, integration walkthrough