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
1 change: 1 addition & 0 deletions packages/audience/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface BaseMessage {
surface: Surface;
context: EventContext;
consentLevel?: ConsentLevel;
sessionId?: string;
/** Present when the SDK/pixel is initialised with testMode: true. */
test?: true;
}
Expand Down
28 changes: 22 additions & 6 deletions packages/audience/pixel/src/pixel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('Pixel', () => {
expect((pageCall!.properties as Record<string, unknown>).utm_source).toBe('google');

expect(sessionStartCall).toBeDefined();
expect((sessionStartCall!.properties as Record<string, unknown>).session_id).toBe('session-abc');
expect(sessionStartCall!.sessionId).toBe('session-abc');
});

it('does not fire page view or session_start when consent is none', () => {
Expand Down Expand Up @@ -169,9 +169,9 @@ describe('Pixel', () => {
expect.objectContaining({
type: 'page',
surface: 'pixel',
sessionId: 'session-abc',
properties: expect.objectContaining({
utm_source: 'google',
session_id: 'session-abc',
custom: 'prop',
}),
}),
Expand Down Expand Up @@ -268,6 +268,24 @@ describe('Pixel', () => {
});
});

describe('session id', () => {
it('stamps sessionId at the top level on both page and track (session_start) messages', () => {
// Default mock (isNew: true) means init() fires both a page view and a session_start track event.
const pixel = new Pixel();
activePixel = pixel;
pixel.init({ key: 'pk_imapik-test-local', consent: 'anonymous' });

const calls = mockEnqueue.mock.calls.map((c: unknown[]) => (c[0] as Record<string, unknown>));
const pageCall = calls.find((c) => c.type === 'page');
const trackCall = calls.find((c) => c.type === 'track');

expect(pageCall).toBeDefined();
expect(trackCall).toBeDefined();
expect(pageCall!.sessionId).toBe('session-abc');
expect(trackCall!.sessionId).toBe('session-abc');
});
});

describe('session_end', () => {
it('fires session_end on pagehide when session is active', () => {
const pixel = new Pixel();
Expand All @@ -282,9 +300,7 @@ describe('Pixel', () => {
expect.objectContaining({
type: 'track',
eventName: 'session_end',
properties: expect.objectContaining({
session_id: 'session-abc',
}),
sessionId: 'session-abc',
}),
);
});
Expand Down Expand Up @@ -605,9 +621,9 @@ describe('Pixel', () => {
type: 'track',
eventName: 'form_submitted',
surface: 'pixel',
sessionId: 'session-xyz',
properties: expect.objectContaining({
form_action: '/signup',
session_id: 'session-xyz',
}),
}),
);
Expand Down
14 changes: 5 additions & 9 deletions packages/audience/pixel/src/pixel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ export class Pixel {
properties: {
...attribution,
...thirdPartyIds,
session_id: sessionId,
...properties,
},
userId: undefined,
Expand Down Expand Up @@ -230,7 +229,7 @@ export class Pixel {
...this.buildBase(),
type: 'track',
eventName,
properties: { ...properties, session_id: sessionId },
properties,
userId: undefined,
};

Expand All @@ -243,18 +242,17 @@ export class Pixel {
this.sessionId = sessionId;
if (isNew) {
this.sessionStartTime = Date.now();
this.fireSessionStart(sessionId);
this.fireSessionStart();
}
}

private fireSessionStart(sessionId: string): void {
private fireSessionStart(): void {
if (!this.isTrackingAllowed()) return;

const message: TrackMessage = {
...this.buildBase(),
type: 'track',
eventName: 'session_start',
properties: { session_id: sessionId },
userId: undefined,
};

Expand All @@ -272,10 +270,7 @@ export class Pixel {
...this.buildBase(),
type: 'track',
eventName: 'session_end',
properties: {
session_id: this.sessionId,
duration,
},
properties: { duration },
userId: undefined,
};

Expand Down Expand Up @@ -333,6 +328,7 @@ export class Pixel {
surface: 'pixel' as const,
context: collectContext('@imtbl/pixel', PIXEL_VERSION),
consentLevel: this.consent!.level,
sessionId: this.sessionId,
...(this.testMode && { test: true as const }),
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/audience/sdk-sample-app/sample-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@
currentAnonId = payload.anonymousId;
changed = true;
}
var sid = payload.properties && payload.properties.session_id;
var sid = payload.sessionId;
if (typeof sid === 'string' && sid && currentSessionId !== sid) {
currentSessionId = sid;
changed = true;
Expand Down
90 changes: 72 additions & 18 deletions packages/audience/sdk/src/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === SESSION_START,
);
expect(msg).toBeDefined();
expect(msg.properties).toHaveProperty('session_id');
expect(msg).toHaveProperty('sessionId');

sdk.shutdown();
});
Expand All @@ -300,7 +300,7 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === SESSION_START,
);
expect(msg).toBeDefined();
expect(msg.properties).toHaveProperty('session_id');
expect(msg).toHaveProperty('sessionId');
expect(msg.properties).toHaveProperty('utm_source', 'youtube');
expect(msg.properties).toHaveProperty('utm_campaign', 'launch');

Expand Down Expand Up @@ -347,8 +347,8 @@ describe('Audience', () => {
);

expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
currency: 'USD',
value: 9.99,
item_id: 'sword_01',
Expand Down Expand Up @@ -410,8 +410,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'sign_up',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
method: 'email',
});

Expand All @@ -432,8 +432,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'game_launch',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
platform: 'webgl',
version: '1.2.0',
build_id: 'ci-42',
Expand All @@ -459,8 +459,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'progression',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
status: 'complete',
world: 'forest',
level: '5',
Expand All @@ -485,8 +485,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'achievement_unlocked',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
achievement_id: 'first_win',
achievement_name: 'First Win',
});
Expand All @@ -508,8 +508,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'achievement_unlocked',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
achievement_id: 'tutorial_done',
achievement_name: 'Tutorial Complete',
achievement_type: 'onboarding',
Expand All @@ -534,8 +534,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'resource',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
flow: 'source',
currency: 'gold',
amount: 50,
Expand All @@ -560,8 +560,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'wishlist_add',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
game_id: 'devilfish',
source: 'game_page',
platform: 'steam',
Expand Down Expand Up @@ -592,8 +592,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'sign_up',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
utm_source: 'google',
utm_campaign: 'spring',
touchpoint_type: 'click',
Expand Down Expand Up @@ -625,8 +625,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'link_clicked',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
utm_source: 'twitter',
touchpoint_type: 'click',
url: 'https://store.com',
Expand Down Expand Up @@ -658,8 +658,8 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'purchase',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
currency: 'USD',
value: 9.99,
});
Expand All @@ -677,13 +677,43 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === 'game_launch',
);
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
platform: 'webgl',
});

sdk.shutdown();
});

it('picks up a session rollover on the next call instead of keeping a stale id', async () => {
const sdk = createSDK();

sdk.track('purchase', { currency: 'USD', value: 1 });
await sdk.flush();
const firstSessionId = sentMessages().find(
(m: any) => m.type === 'track' && m.eventName === 'purchase',
).sessionId;

// Simulate the session cookie expiring (30 min idle) before the next call.
document.cookie = `${SESSION_COOKIE}=;max-age=0;path=/`;

sdk.track('purchase', { currency: 'USD', value: 2 });
await sdk.flush();

const msgs = sentMessages();
const sessionStarts = msgs.filter(
(m: any) => m.type === 'track' && m.eventName === SESSION_START,
);
const purchases = msgs.filter(
(m: any) => m.type === 'track' && m.eventName === 'purchase',
);

expect(sessionStarts).toHaveLength(2);
expect(purchases[1].sessionId).not.toBe(firstSessionId);
expect(purchases[1].sessionId).toBe(sessionStarts[1].sessionId);

sdk.shutdown();
});
});

describe('page', () => {
Expand All @@ -696,8 +726,8 @@ describe('Audience', () => {
const msg = sentMessages().find((m: any) => m.type === 'page');
expect(msg).toBeDefined();
// First page merges session-cached attribution (includes landing_page)
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
landing_page: expect.any(String),
section: 'shop',
});
Expand Down Expand Up @@ -785,8 +815,8 @@ describe('Audience', () => {

const msg = sentMessages().find((m: any) => m.type === 'page');
expect(msg).toBeDefined();
expect(msg.sessionId).toEqual(expect.any(String));
expect(msg.properties).toEqual({
session_id: expect.any(String),
landing_page: expect.any(String),
});

Expand Down Expand Up @@ -844,6 +874,30 @@ describe('Audience', () => {
});
});

describe('session id', () => {
it('stamps sessionId at the top level on every message type, including identify and alias', async () => {
const sdk = createSDK({ consent: 'full' });
sdk.page();
sdk.track('purchase', { currency: 'USD', value: 1 });
sdk.identify(TEST_USER.id, TEST_USER.identityType);
sdk.alias(TEST_STEAM, TEST_USER);
await sdk.flush();

const msgs = sentMessages();
expect(msgs.length).toBeGreaterThan(0);
msgs.forEach((m: any) => expect(m.sessionId).toEqual(expect.any(String)));

const pageMsg = msgs.find((m: any) => m.type === 'page');
const idMsg = msgs.find((m: any) => m.type === 'identify');
const aliasMsg = msgs.find((m: any) => m.type === 'alias');
// identify/alias have no `properties` field, so sessionId is only ever visible at the top level.
expect(idMsg.sessionId).toBe(pageMsg.sessionId);
expect(aliasMsg.sessionId).toBe(pageMsg.sessionId);

sdk.shutdown();
});
});

describe('identify', () => {
it('sends an identify message at full consent', async () => {
const sdk = createSDK({ consent: 'full' });
Expand Down Expand Up @@ -1038,7 +1092,7 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === SESSION_START,
);
expect(sessionStart).toBeDefined();
expect(sessionStart.properties).toHaveProperty('session_id');
expect(sessionStart).toHaveProperty('sessionId');

const signUp = msgs.find(
(m: any) => m.type === 'track' && m.eventName === 'sign_up',
Expand Down Expand Up @@ -1159,7 +1213,7 @@ describe('Audience', () => {
(m: any) => m.type === 'track' && m.eventName === SESSION_END,
);
expect(msg).toBeDefined();
expect(msg.properties).toHaveProperty('session_id');
expect(msg).toHaveProperty('sessionId');
expect(msg.properties.duration).toBe(5);
});

Expand Down
Loading