Skip to content
Open
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
26 changes: 7 additions & 19 deletions packages/audience/core/src/consent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,19 @@ describe('createConsentManager', () => {
Object.defineProperty(navigator, 'globalPrivacyControl', { value: undefined, configurable: true });
});

it('tracks gpc_consent_override metric with signal and configured level when GPC fires', () => {
it('tracks gpc_consent_overridden when GPC fires at init', () => {
Object.defineProperty(navigator, 'globalPrivacyControl', { value: true, configurable: true });
const send = createMockSend();
createConsentManager(send, 'pk_imapik-test-local', 'anon-1', 'pixel', 'full');
const expected = {
signal: 'gpc', requestedLevel: 'full', context: 'init', publishableKey: 'pk_imapik-test-local',
};
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden', expected);
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden');
Object.defineProperty(navigator, 'globalPrivacyControl', { value: undefined, configurable: true });
});

it('tracks gpc_consent_override metric with dnt signal when DNT fires', () => {
it('tracks gpc_consent_overridden when DNT fires at init', () => {
Object.defineProperty(navigator, 'doNotTrack', { value: '1', configurable: true });
const send = createMockSend();
createConsentManager(send, 'pk_imapik-test-local', 'anon-1', 'pixel', 'anonymous');
const expected = {
signal: 'dnt', requestedLevel: 'anonymous', context: 'init', publishableKey: 'pk_imapik-test-local',
};
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden', expected);
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden');
Object.defineProperty(navigator, 'doNotTrack', { value: '0', configurable: true });
});

Expand All @@ -176,23 +170,17 @@ describe('createConsentManager', () => {
const manager = createConsentManager(send, 'pk_imapik-test-local', 'anon-1', 'pixel', 'none');
(track as jest.Mock).mockClear();
manager.setLevel('full');
const expected = {
signal: 'gpc', requestedLevel: 'full', context: 'runtime', publishableKey: 'pk_imapik-test-local',
};
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden', expected);
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden');
Object.defineProperty(navigator, 'globalPrivacyControl', { value: undefined, configurable: true });
});

it('tracks gpc_consent_overridden with dnt signal when blocked by DNT at runtime', () => {
it('tracks gpc_consent_overridden when setLevel is blocked by DNT', () => {
Object.defineProperty(navigator, 'doNotTrack', { value: '1', configurable: true });
const send = createMockSend();
const manager = createConsentManager(send, 'pk_imapik-test-local', 'anon-1', 'pixel', 'none');
(track as jest.Mock).mockClear();
manager.setLevel('full');
const expected = {
signal: 'dnt', requestedLevel: 'full', context: 'runtime', publishableKey: 'pk_imapik-test-local',
};
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden', expected);
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden');
Object.defineProperty(navigator, 'doNotTrack', { value: '0', configurable: true });
});

Expand Down
14 changes: 2 additions & 12 deletions packages/audience/core/src/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,7 @@ export function createConsentManager(
): ConsentManager {
const privacySignalActive = detectDoNotTrack();
if (privacySignalActive) {
track('audience', 'gpc_consent_overridden', {
signal: resolvePrivacySignal(),
requestedLevel: initialLevel ?? 'none',
context: 'init',
publishableKey,
});
track('audience', 'gpc_consent_overridden');
}
let current: ConsentLevel = privacySignalActive ? 'none' : (initialLevel ?? 'none');

Expand All @@ -101,12 +96,7 @@ export function createConsentManager(
const effective = signalActive ? 'none' : next;

if (signalActive && effective !== next) {
track('audience', 'gpc_consent_overridden', {
signal: resolvePrivacySignal(),
requestedLevel: next,
context: 'runtime',
publishableKey,
});
track('audience', 'gpc_consent_overridden');
}

if (effective === current) return;
Expand Down
55 changes: 11 additions & 44 deletions packages/audience/core/src/transport.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { track, trackError } from '@imtbl/metrics';
import { track } from '@imtbl/metrics';
import { httpSend } from './transport';
import { TransportError } from './errors';
import type { BatchPayload } from './types';

jest.mock('@imtbl/metrics', () => ({
track: jest.fn(),
trackError: jest.fn(),
}));

const payload: BatchPayload = {
Expand All @@ -24,15 +23,13 @@ const payload: BatchPayload = {
};

const mockTrack = track as jest.Mock;
const mockTrackError = trackError as jest.Mock;

describe('httpSend', () => {
const originalFetch = global.fetch;

afterEach(() => {
global.fetch = originalFetch;
mockTrack.mockClear();
mockTrackError.mockClear();
});

it('sends POST with correct headers and body', async () => {
Expand Down Expand Up @@ -281,15 +278,10 @@ describe('httpSend', () => {

await httpSend('https://example.com', 'pk', payload);

expect(mockTrackError).toHaveBeenCalledWith(
expect(mockTrack).toHaveBeenCalledWith(
'audience',
'transport_send',
expect.any(TransportError),
expect.objectContaining({
errorName: 'TypeError',
online: true,
timeToFailureMs: expect.any(Number),
}),
{ error: expect.any(TransportError) },
);
});

Expand All @@ -307,15 +299,10 @@ describe('httpSend', () => {
jest.advanceTimersByTime(30_000);
await sendPromise;

expect(mockTrackError).toHaveBeenCalledWith(
expect(mockTrack).toHaveBeenCalledWith(
'audience',
'transport_send',
expect.any(TransportError),
expect.objectContaining({
errorName: 'AbortError',
online: true,
timeToFailureMs: expect.any(Number),
}),
{ error: expect.any(TransportError) },
);

jest.useRealTimers();
Expand All @@ -327,18 +314,14 @@ describe('httpSend', () => {

await httpSend('https://example.com', 'pk', payload);

expect(mockTrackError).toHaveBeenCalledWith(
expect(mockTrack).toHaveBeenCalledWith(
'audience',
'transport_send',
expect.any(TransportError),
expect.objectContaining({
online: false,
timeToFailureMs: expect.any(Number),
}),
{ error: expect.any(TransportError) },
);
});

it('attaches online and timeToFailureMs to transport_send_failed on HTTP error', async () => {
it('tracks transport_send_failed on HTTP error', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 500,
Expand All @@ -348,18 +331,10 @@ describe('httpSend', () => {

await httpSend('https://example.com', 'pk', payload);

expect(mockTrack).toHaveBeenCalledWith(
'audience',
'transport_send_failed',
expect.objectContaining({
status: 500,
online: true,
timeToFailureMs: expect.any(Number),
}),
);
expect(mockTrack).toHaveBeenCalledWith('audience', 'transport_send_failed');
});

it('attaches online and timeToFailureMs to transport_send_failed on 429', async () => {
it('tracks transport_send_failed on 429', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 429,
Expand All @@ -368,15 +343,7 @@ describe('httpSend', () => {

await httpSend('https://example.com', 'pk', payload);

expect(mockTrack).toHaveBeenCalledWith(
'audience',
'transport_send_failed',
expect.objectContaining({
status: 429,
online: true,
timeToFailureMs: expect.any(Number),
}),
);
expect(mockTrack).toHaveBeenCalledWith('audience', 'transport_send_failed');
});
});
});
35 changes: 5 additions & 30 deletions packages/audience/core/src/transport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { track, trackError } from '@imtbl/metrics';
import { track } from '@imtbl/metrics';
import type { BatchPayload, ConsentUpdatePayload } from './types';
import { TransportError, type TransportResult } from './errors';
import { isBrowser } from './utils';

export interface TransportOptions {
method?: string;
Expand Down Expand Up @@ -44,14 +43,6 @@ function parseRetryAfterMs(headers: Headers): number | null {
return null;
}

function safeOnline(): boolean | undefined {
try {
return isBrowser() ? navigator.onLine : undefined;
} catch {
return undefined;
}
}

async function parseBody(response: Response): Promise<unknown> {
const contentType = response.headers?.get?.('content-type') ?? '';
try {
Expand All @@ -72,7 +63,6 @@ export const httpSend: HttpSend = async (
) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
const startTime = Date.now();

try {
const hasBody = payload !== undefined;
Expand All @@ -88,11 +78,7 @@ export const httpSend: HttpSend = async (
});

if (response.status === 429) {
track('audience', 'transport_send_failed', {
status: 429,
online: safeOnline(),
timeToFailureMs: Date.now() - startTime,
});
track('audience', 'transport_send_failed');
const retryAfterMs = parseRetryAfterMs(response.headers);
return {
ok: false,
Expand All @@ -106,11 +92,7 @@ export const httpSend: HttpSend = async (

if (!response.ok) {
const body = await parseBody(response);
track('audience', 'transport_send_failed', {
status: response.status,
online: safeOnline(),
timeToFailureMs: Date.now() - startTime,
});
track('audience', 'transport_send_failed');
return {
ok: false,
error: new TransportError({
Expand Down Expand Up @@ -138,10 +120,7 @@ export const httpSend: HttpSend = async (
) {
const rejected = (body as { rejected?: number }).rejected ?? 0;
if (rejected > 0) {
track('audience', 'transport_partial_rejected', {
status: response.status,
rejected,
});
track('audience', 'transport_partial_rejected');
return {
ok: false,
error: new TransportError({
Expand All @@ -160,11 +139,7 @@ export const httpSend: HttpSend = async (
endpoint: url,
cause: err,
});
trackError('audience', 'transport_send', error, {
errorName: err instanceof Error ? err.name : undefined,
online: safeOnline(),
timeToFailureMs: Date.now() - startTime,
});
track('audience', 'transport_send', { error });
return { ok: false, error };
} finally {
clearTimeout(timeoutId);
Expand Down
4 changes: 1 addition & 3 deletions packages/audience/pixel/src/stubs/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const track = (..._args: unknown[]): void => {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const trackError = (..._args: unknown[]): void => {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const trackDuration = (..._args: unknown[]): void => {};
export const configure = (..._args: unknown[]): void => {};
6 changes: 1 addition & 5 deletions packages/audience/sdk/src/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { LIBRARY_NAME } from './config';

jest.mock('@imtbl/metrics', () => ({
track: jest.fn(),
trackError: jest.fn(),
}));

const INGEST_PATH = '/v1/audience/messages';
Expand Down Expand Up @@ -1790,10 +1789,7 @@ describe('Audience', () => {
// Consent stays at none — no events should be sent
sdk.page();
expect(sentMessages()).toHaveLength(0);
const expected = {
signal: 'gpc', requestedLevel: 'anonymous', context: 'runtime', publishableKey: 'pk_imapik-test-local',
};
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden', expected);
expect(track).toHaveBeenCalledWith('audience', 'gpc_consent_overridden');

sdk.shutdown();
});
Expand Down
9 changes: 2 additions & 7 deletions packages/audience/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
isBrowser,
setupAutocapture,
} from '@imtbl/audience-core';
import { adoptAnonymousId, resolvePrivacySignal } from '@imtbl/audience-core/internal';
import { adoptAnonymousId } from '@imtbl/audience-core/internal';
import { track } from '@imtbl/metrics';
import { DebugLogger } from './debug';
import { REQUIRED_EVENT_PROPS, type AudienceEventName, type PropsFor } from './events';
Expand Down Expand Up @@ -450,12 +450,7 @@ export class Audience {
const effective: ConsentLevel = privacySignalActive ? 'none' : level;

if (privacySignalActive && effective !== level) {
track('audience', 'gpc_consent_overridden', {
signal: resolvePrivacySignal(),
requestedLevel: level,
context: 'runtime',
publishableKey: this.publishableKey,
});
track('audience', 'gpc_consent_overridden');
this.debug.logWarning('GPC or DNT signal active: consent upgrade blocked.');
}

Expand Down
Loading