Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions packages/audience/core/src/config.test.ts

This file was deleted.

8 changes: 1 addition & 7 deletions packages/audience/core/src/config.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'
);
6 changes: 3 additions & 3 deletions packages/audience/core/src/consent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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' },
}),
});
Expand All @@ -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'),
}),
});
Expand Down
4 changes: 2 additions & 2 deletions packages/audience/core/src/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +59,7 @@ export function createConsentManager(
const LEVELS: Record<ConsentLevel, number> = { 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}`;
Comment thread
cursor[bot] marked this conversation as resolved.
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 })
Expand Down
10 changes: 5 additions & 5 deletions packages/audience/core/src/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('MessageQueue', () => {
const send = jest.fn<ReturnType<HttpSend>, Parameters<HttpSend>>().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 });
Expand All @@ -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'),
}),
});
Expand Down Expand Up @@ -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 },
}),
});
Expand Down Expand Up @@ -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 },
Expand All @@ -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 },
Expand Down
4 changes: 2 additions & 2 deletions packages/audience/core/src/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/audience/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 23 additions & 12 deletions packages/audience/pixel/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @imtbl/pixel — Immutable Tracking Pixel
# @imtbl/pixel

A drop-in JavaScript snippet that captures device signals, page views, and attribution data for Immutable's events pipeline. Use it to measure campaign performance and attribute player acquisition across your marketing sites, landing pages, and web shops. Zero configuration beyond a publishable key.

Expand All @@ -21,7 +21,7 @@ document.head.appendChild(s);

Replace `YOUR_PUBLISHABLE_KEY` with your project's publishable key from [Immutable Hub](https://hub.immutable.com/).

The script loads asynchronously and does not block page rendering. The default consent level is `none` the pixel loads but does not collect until consent is explicitly set (see [Consent Modes](#consent-modes)). To start collecting anonymous device signals immediately, add `"consent":"anonymous"` to the init options:
The script loads asynchronously and does not block page rendering. The default consent level is `none`: the pixel loads but does not collect until consent is explicitly set (see [Consent Modes](#consent-modes)). To start collecting anonymous device signals immediately, add `"consent":"anonymous"` to the init options:

```diff
- w[i].push(["init",{"key":"YOUR_PUBLISHABLE_KEY"}]);
Expand All @@ -34,7 +34,7 @@ The `consent` option controls what the pixel collects. **Default is `none`** (no

| Level | What's collected | Cookies set | Use case |
|-------|-----------------|-------------|----------|
| `none` | Nothing pixel loads but is inert | None | Before consent banner interaction |
| `none` | Nothing (pixel loads but is inert) | None | Before consent banner interaction |
| `anonymous` | Device signals, attribution, page views, form submissions, link clicks (no PII) | `imtbl_anon_id`, `_imtbl_sid` | Anonymous analytics without PII |
| `full` | Everything in `anonymous` + hashed email capture from form submissions (for identity matching) | `imtbl_anon_id`, `_imtbl_sid` | After explicit user consent for marketing/ads |

Expand All @@ -47,12 +47,12 @@ If your site uses a Consent Management Platform (CMP), the pixel can auto-detect
+ w[i].push(["init",{"key":"YOUR_KEY","consentMode":"auto"}]);
```

> **Note:** `consentMode` and `consent` are mutually exclusive — do not set both.
> **Note:** `consentMode` and `consent` are mutually exclusive. Do not set both.

The pixel starts in `none` and checks for these CMP standards (in priority order):

1. [**Google Consent Mode v2**](https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced) reads `analytics_storage` and `ad_storage` from `window.dataLayer`
2. [**IAB TCF v2**](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md) reads purpose consents via `window.__tcfapi`
1. [**Google Consent Mode v2**](https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced): reads `analytics_storage` and `ad_storage` from `window.dataLayer`
2. [**IAB TCF v2**](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md): reads purpose consents via `window.__tcfapi`

Once a CMP is detected, the pixel upgrades consent automatically and continues listening for changes (e.g. when a user updates their cookie preferences). If no CMP is detected after ~2.5 seconds, the pixel remains in `none` silently (there is no failure callback). If your CMP may not be present on every page, push a manual fallback on your own timeout:

Expand All @@ -67,7 +67,7 @@ setTimeout(function() {
If you are not using `consentMode: 'auto'`, you can set consent manually at any time:

```javascript
// After cookie banner interaction upgrade to full
// After cookie banner interaction, upgrade to full
window.__imtbl.push(['consent', 'full']);

// Or downgrade (purges PII from queue)
Expand Down Expand Up @@ -106,12 +106,23 @@ document.head.appendChild(s);
</script>
```

## Test mode

Set `"testMode": true` in the init options to mark every event with a
top-level `test: true` flag. Test events still flow through the
production endpoint, but can be filtered out of production analytics.

```diff
- w[i].push(["init",{"key":"YOUR_KEY","consent":"anonymous"}]);
+ w[i].push(["init",{"key":"YOUR_KEY","consent":"anonymous","testMode":true}]);
```

## Cookies

| Cookie | Lifetime | Purpose |
|--------|----------|---------|
| `imtbl_anon_id` | 2 years | Anonymous device ID (shared with web SDK) |
| `_imtbl_sid` | 30 minutes (rolling) | Session ID resets on inactivity |
| `_imtbl_sid` | 30 minutes (rolling) | Session ID (resets on inactivity) |

Both cookies are first-party (`SameSite=Lax`, `Secure` on HTTPS).

Expand Down Expand Up @@ -154,7 +165,7 @@ Note: the nonce covers the inline snippet only. The CDN-loaded script (`imtbl.js

## Documentation

- [Tracking Pixel](https://docs.immutable.com/docs/products/audience/tracking-pixel) this package (setup, consent modes, auto-tracked events)
- [Web SDK](https://docs.immutable.com/docs/products/audience/web-sdk) sibling `@imtbl/audience` package for typed event tracking and identity management
- [REST API](https://docs.immutable.com/docs/products/audience/rest-api) backend reference for direct integration
- [Data dictionary](https://docs.immutable.com/docs/products/audience/data-dictionary) predefined event names and property schemas
- [Tracking Pixel](https://docs.immutable.com/docs/products/audience/tracking-pixel): this package (setup, consent modes, auto-tracked events)
- [Web SDK](https://docs.immutable.com/docs/products/audience/web-sdk): sibling `@imtbl/audience` package for typed event tracking and identity management
- [REST API](https://docs.immutable.com/docs/products/audience/rest-api): backend reference for direct integration
- [Data dictionary](https://docs.immutable.com/docs/products/audience/data-dictionary): predefined event names and property schemas
7 changes: 6 additions & 1 deletion packages/audience/pixel/src/pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -55,6 +57,8 @@ export class Pixel {

private domain: string | undefined;

private testMode = false;

private initialized = false;

private unloadHandler?: () => void;
Expand All @@ -80,6 +84,7 @@ export class Pixel {

this.publishableKey = key;
this.domain = domain;
this.testMode = options.testMode ?? false;

this.queue = new MessageQueue(
httpSend,
Expand Down Expand Up @@ -320,14 +325,14 @@ export class Pixel {

// -- Helpers ------------------------------------------------------------

// eslint-disable-next-line class-methods-use-this
private buildBase() {
return {
messageId: generateId(),
eventTimestamp: getTimestamp(),
anonymousId: this.anonymousId,
surface: 'pixel' as const,
context: collectContext('@imtbl/pixel', PIXEL_VERSION),
...(this.testMode && { test: true as const }),
};
}

Expand Down
7 changes: 5 additions & 2 deletions packages/audience/sdk-sample-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src https://api.dev.immutable.com https://api.sandbox.immutable.com https://api.immutable.com">
content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src https://api.dev.immutable.com https://api.immutable.com">
<title>Immutable Audience SDK — Sample App</title>
<link rel="stylesheet" href="sample-app.css">
</head>
Expand Down Expand Up @@ -58,7 +58,6 @@ <h1>
<label for="environment">Environment</label>
<select id="environment">
<option value="https://api.dev.immutable.com" selected>Dev</option>
<option value="https://api.sandbox.immutable.com">Sandbox</option>
<option value="https://api.immutable.com">Production</option>
</select>
</div>
Expand All @@ -78,6 +77,10 @@ <h1>
<label><input id="debug" type="checkbox" checked> debug</label>
<p class="helper-text">Mirror SDK internal log output into the in-page event log below.</p>
</div>
<div class="field">
<label><input id="test-mode" type="checkbox"> testMode</label>
<p class="helper-text">Mark all events with <code>test: true</code> so they can be filtered from production analytics.</p>
</div>

<div class="advanced-grid">
<div class="field">
Expand Down
2 changes: 1 addition & 1 deletion packages/audience/sdk-sample-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@imtbl/audience-sdk-sample-app",
"description": "Interactive sample app for @imtbl/audience. Exercises every public method on the Audience class, every typed track() event, and every reachable AudienceErrorCode against the sandbox backend.",
"description": "Interactive sample app for @imtbl/audience. Exercises every public method on the Audience class, every typed track() event, and every reachable AudienceErrorCode.",
"version": "0.0.0",
"author": "Immutable",
"private": true,
Expand Down
5 changes: 4 additions & 1 deletion packages/audience/sdk-sample-app/sample-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
['pk', 'value', 'pk'],
['initial-consent', 'value', 'initialConsent'],
['debug', 'checked', 'debug'],
['test-mode', 'checked', 'testMode'],
['cookie-domain', 'value', 'cookieDomain'],
['flush-interval', 'value', 'flushInterval'],
['flush-size', 'value', 'flushSize'],
Expand Down Expand Up @@ -329,6 +330,7 @@
debug: $('debug').checked,
onError: handleError,
};
if ($('test-mode').checked) config.testMode = true;
var cd = $('cookie-domain').value.trim(); if (cd) config.cookieDomain = cd;
config.baseUrl = $('environment').value;
var fi = parseInt($('flush-interval').value, 10); if (!Number.isNaN(fi)) config.flushInterval = fi;
Expand Down Expand Up @@ -417,6 +419,7 @@
log('INIT', {
consent: config.consent,
debug: config.debug,
testMode: config.testMode || false,
cookieDomain: config.cookieDomain,
flushInterval: config.flushInterval,
flushSize: config.flushSize,
Expand Down Expand Up @@ -545,7 +548,7 @@
});
$('environment').addEventListener('change', function () { updateStatus(); saveUiState(); });
$('initial-consent').addEventListener('change', function () { saveUiState(); updateStatus(); });
['debug', 'cookie-domain', 'flush-interval', 'flush-size'].forEach(function (id) {
['debug', 'test-mode', 'cookie-domain', 'flush-interval', 'flush-size'].forEach(function (id) {
var el = $(id);
if (!el) return;
el.addEventListener('input', saveUiState);
Expand Down
4 changes: 4 additions & 0 deletions packages/audience/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export class Audience {

private readonly cookieDomain?: string;

private readonly testMode: boolean;

private anonymousId: string;

private sessionId: string | undefined;
Expand All @@ -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;
Expand Down Expand Up @@ -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 }),
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/audience/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading