Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/audience/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export const FLUSH_SIZE = 20;
export const COOKIE_NAME = 'imtbl_anon_id';
export const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60 * 2; // 2 years

export const SESSION_TIMEOUT_MS = 30 * 60 * 1_000; // 30 minutes

export const getBaseUrl = (environment: Environment): string => BASE_URLS[environment];
2 changes: 2 additions & 0 deletions packages/audience/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { EventContext } from './types';
import { isBrowser } from './utils';
import { getSessionId } from './session';

// WARNING: DO NOT CHANGE THE STRING BELOW. IT GETS REPLACED AT BUILD TIME.
const SDK_VERSION = '__SDK_VERSION__';
Expand All @@ -20,6 +21,7 @@ export function collectContext(): EventContext {
context.pagePath = window.location.pathname;
context.pageReferrer = document.referrer;
context.pageTitle = document.title;
context.sessionId = getSessionId();

return context;
}
2 changes: 2 additions & 0 deletions packages/audience/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
FLUSH_INTERVAL_MS,
FLUSH_SIZE,
COOKIE_NAME,
SESSION_TIMEOUT_MS,
} from './config';

export { generateId, getTimestamp, isBrowser } from './utils';
Expand All @@ -31,3 +32,4 @@ export type { Transport } from './transport';
export { httpTransport, httpSend } from './transport';
export { MessageQueue } from './queue';
export { collectContext } from './context';
export { getSessionId, resetSession } from './session';
61 changes: 61 additions & 0 deletions packages/audience/core/src/session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getSessionId, resetSession } from './session';
import { SESSION_TIMEOUT_MS } from './config';

afterEach(() => {
resetSession();
jest.restoreAllMocks();
});

describe('session', () => {
it('returns a session ID', () => {
const id = getSessionId();
expect(id).toBeDefined();
expect(typeof id).toBe('string');
expect(id.length).toBeGreaterThan(0);
});

it('returns the same session ID within the timeout window', () => {
const id1 = getSessionId();
const id2 = getSessionId();
expect(id2).toBe(id1);
});

it('generates a new session ID after timeout expires', () => {
const id1 = getSessionId();

jest.spyOn(Date, 'now').mockReturnValue(Date.now() + SESSION_TIMEOUT_MS + 1);

const id2 = getSessionId();
expect(id2).not.toBe(id1);
});

it('refreshes the timeout on each call (rolling window)', () => {
const baseTime = Date.now();
const nowSpy = jest.spyOn(Date, 'now');

nowSpy.mockReturnValue(baseTime);
const id1 = getSessionId();

// 20 minutes later — still within window
nowSpy.mockReturnValue(baseTime + 20 * 60 * 1_000);
const id2 = getSessionId();
expect(id2).toBe(id1);

// Another 20 minutes (40 min total, but only 20 since last activity)
nowSpy.mockReturnValue(baseTime + 40 * 60 * 1_000);
const id3 = getSessionId();
expect(id3).toBe(id1);

// 31 minutes after last activity → new session
nowSpy.mockReturnValue(baseTime + 40 * 60 * 1_000 + SESSION_TIMEOUT_MS + 1);
const id4 = getSessionId();
expect(id4).not.toBe(id1);
});

it('resetSession forces a new session', () => {
const id1 = getSessionId();
resetSession();
const id2 = getSessionId();
expect(id2).not.toBe(id1);
});
});
35 changes: 35 additions & 0 deletions packages/audience/core/src/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as storage from './storage';
import { generateId } from './utils';
import { SESSION_TIMEOUT_MS } from './config';

const SESSION_ID_KEY = 'session_id';
const SESSION_LAST_ACTIVITY_KEY = 'session_last_activity';

/**
* Returns the current session ID, creating a new one if:
* - No session exists yet
* - The last activity was more than 30 minutes ago
*
* Each call refreshes the last-activity timestamp (rolling window).
*/
export function getSessionId(): string {
const now = Date.now();
const lastActivity = storage.getItem(SESSION_LAST_ACTIVITY_KEY) as number | undefined;
const existingId = storage.getItem(SESSION_ID_KEY) as string | undefined;

if (existingId && lastActivity && now - lastActivity < SESSION_TIMEOUT_MS) {
storage.setItem(SESSION_LAST_ACTIVITY_KEY, now);
return existingId;
}

const newId = generateId();
storage.setItem(SESSION_ID_KEY, newId);
storage.setItem(SESSION_LAST_ACTIVITY_KEY, now);
return newId;
}

/** Force-starts a new session. Useful for testing or explicit session boundaries. */
export function resetSession(): void {
storage.removeItem(SESSION_ID_KEY);
storage.removeItem(SESSION_LAST_ACTIVITY_KEY);
}
1 change: 1 addition & 0 deletions packages/audience/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface EventContext {
pagePath?: string;
pageReferrer?: string;
pageTitle?: string;
sessionId?: string;
}

export interface UserTraits {
Expand Down
Loading