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
2 changes: 1 addition & 1 deletion packages/altertable-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ altertable.alias('new_user_id-019aca6a-1e42-71af-81a0-1e14bbe2ccbd');
## Features

- **Automatic page view tracking** – Captures page views automatically
- **Session management** – Handles visitor and session IDs automatically
- **Session management** – Handles anonymous and session IDs automatically
- **Event queuing** – Queues events when offline or consent is pending
- **Privacy compliance** – Built-in tracking consent management
- **Multiple storage options** – localStorage, cookies, or both
Expand Down
2 changes: 1 addition & 1 deletion packages/altertable-js/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const keyBuilder = createKeyBuilder(STORAGE_KEY_PREFIX, '.');
export const STORAGE_KEY_TEST = keyBuilder('check');

export const PREFIX_SESSION_ID = 'session';
export const PREFIX_VISITOR_ID = 'visitor';
export const PREFIX_ANONYMOUS_ID = 'anonymous';
export const PREFIX_DEVICE_ID = 'device';

const MINUTE_IN_MS = 1_000 * 60;
Expand Down
12 changes: 6 additions & 6 deletions packages/altertable-js/src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export function createLogger(prefix: string) {
const [userValueLabel, userValueStyle] = createValueElement(
payload.distinct_id ?? 'Not set'
);
const [visitorLabel, visitorLabelStyle] =
createEventLabelElement('Visitor ID');
const [visitorValueLabel, visitorValueStyle] = createValueElement(
const [anonymousLabel, anonymousLabelStyle] =
createEventLabelElement('Anonymous ID');
const [anonymousValueLabel, anonymousValueStyle] = createValueElement(
payload.anonymous_id ?? 'Not set'
);
const [sessionLabel, sessionLabelStyle] =
Expand All @@ -73,9 +73,9 @@ export function createLogger(prefix: string) {
userValueStyle
);
console.log(
`%c${visitorLabel} %c${visitorValueLabel}`,
visitorLabelStyle,
visitorValueStyle
`%c${anonymousLabel} %c${anonymousValueLabel}`,
anonymousLabelStyle,
anonymousValueStyle
);
console.log(
`%c${sessionLabel} %c${sessionValueLabel}`,
Expand Down
26 changes: 13 additions & 13 deletions packages/altertable-js/src/lib/sessionManager.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {
PREFIX_ANONYMOUS_ID,
PREFIX_DEVICE_ID,
PREFIX_SESSION_ID,
PREFIX_VISITOR_ID,
SESSION_EXPIRATION_TIME_MS,
TrackingConsent,
TrackingConsentType,
} from '../constants';
import type {
AnonymousId,
DeviceId,
DistinctId,
SessionId,
UserId,
VisitorId,
} from '../types';
import { generateId } from './generateId';
import { Logger } from './logger';
Expand All @@ -20,7 +20,7 @@ import { type StorageApi } from './storage';
type SessionData = {
deviceId: DeviceId;
distinctId: DistinctId;
anonymousId: VisitorId | null;
anonymousId: AnonymousId | null;
sessionId: SessionId;
lastEventAt: string | null;
trackingConsent: TrackingConsentType;
Expand Down Expand Up @@ -61,7 +61,7 @@ export class SessionManager {

this._sessionData = {
deviceId: parsedData.deviceId || this._generateDeviceId(),
distinctId: parsedData.distinctId || this._generateVisitorId(),
distinctId: parsedData.distinctId || this._generateAnonymousId(),
sessionId: parsedData.sessionId || this._generateSessionId(),
anonymousId: parsedData.anonymousId || null,
lastEventAt: parsedData.lastEventAt || null,
Expand Down Expand Up @@ -91,7 +91,7 @@ export class SessionManager {
return this._sessionData.distinctId;
}

getAnonymousId(): VisitorId | null {
getAnonymousId(): AnonymousId | null {
return this._sessionData.anonymousId;
}

Expand All @@ -103,13 +103,13 @@ export class SessionManager {
*
* When transitioning from anonymous to identified, we preserve the anonymous ID
* to enable identity merging on the backend. This allows:
* - Linking pre-identification events (anonymous visitor ID) to post-identification events (user ID)
* - Linking pre-identification events (anonymous ID) to post-identification events (user ID)
* - Merging user profiles so anonymous browsing behavior is associated with the identified user
* - Maintaining a complete user journey from first visit through identification
*
* **State Transitions:**
* - **Anonymous:** `anonymousId = null`, `distinctId = visitorId`, `isIdentified() = false`
* - **Identified:** `anonymousId = previous visitorId`, `distinctId = userId`, `isIdentified() = true`
* - **Anonymous:** `anonymousId = null`, `distinctId = anonymousId`, `isIdentified() = false`
* - **Identified:** `anonymousId = previous distinctId`, `distinctId = userId`, `isIdentified() = true`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also fixed the semantics in comments

*/
isIdentified(): boolean {
return Boolean(this._sessionData.anonymousId);
Expand All @@ -124,7 +124,7 @@ export class SessionManager {
}

identify(userId: UserId): void {
this._sessionData.anonymousId = this._sessionData.distinctId as VisitorId;
this._sessionData.anonymousId = this._sessionData.distinctId as AnonymousId;
this._sessionData.distinctId = userId;
this._persistToStorage();
}
Expand Down Expand Up @@ -168,7 +168,7 @@ export class SessionManager {

this._sessionData.sessionId = this._generateSessionId();
this._sessionData.anonymousId = null;
this._sessionData.distinctId = this._generateVisitorId();
this._sessionData.distinctId = this._generateAnonymousId();
this._sessionData.lastEventAt = null;
this._persistToStorage();
}
Expand All @@ -177,7 +177,7 @@ export class SessionManager {
return {
anonymousId: null,
deviceId: this._generateDeviceId(),
distinctId: this._generateVisitorId(),
distinctId: this._generateAnonymousId(),
lastEventAt: null,
sessionId: this._generateSessionId(),
trackingConsent: this._defaultTrackingConsent,
Expand All @@ -192,8 +192,8 @@ export class SessionManager {
return generateId(PREFIX_DEVICE_ID);
}

private _generateVisitorId(): VisitorId {
return generateId(PREFIX_VISITOR_ID);
private _generateAnonymousId(): AnonymousId {
return generateId(PREFIX_ANONYMOUS_ID);
}

private _shouldRenewSession(): boolean {
Expand Down
6 changes: 3 additions & 3 deletions packages/altertable-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export type EventType = 'track' | 'identify' | 'alias';
export type EventProperties = Record<string, unknown>;

export type UserId = string;
export type DistinctId = StringWithAutocomplete<UserId | VisitorId>;
export type DistinctId = StringWithAutocomplete<UserId | AnonymousId>;
export type DeviceId = `device-${string}`;
export type VisitorId = `visitor-${string}`;
export type AnonymousId = `anonymous-${string}`;
export type SessionId = `session-${string}`;
export type Environment = StringWithAutocomplete<
'production' | 'development' | 'staging'
Expand All @@ -21,7 +21,7 @@ export type AltertableContext = {
environment: Environment;
device_id: DeviceId;
distinct_id: DistinctId;
anonymous_id: VisitorId | null;
anonymous_id: AnonymousId | null;
session_id: SessionId;
};

Expand Down
50 changes: 25 additions & 25 deletions packages/altertable-js/test/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
} from '../../../test-utils/networkMode';
import {
EVENT_PAGEVIEW,
PREFIX_ANONYMOUS_ID,
PREFIX_DEVICE_ID,
PREFIX_SESSION_ID,
PREFIX_VISITOR_ID,
PROPERTY_LIB,
PROPERTY_LIB_VERSION,
PROPERTY_REFERER,
Expand All @@ -28,11 +28,11 @@ import { UserId, UserTraits } from '../src/types';
const REGEXP_DATE_ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const REGEXP_SESSION_ID = new RegExp(`^${PREFIX_SESSION_ID}-`);
const REGEXP_DEVICE_ID = new RegExp(`^${PREFIX_DEVICE_ID}-`);
const REGEXP_VISITOR_ID = new RegExp(`^${PREFIX_VISITOR_ID}-`);
const REGEXP_ANONYMOUS_ID = new RegExp(`^${PREFIX_ANONYMOUS_ID}-`);

function createSessionData(overrides: Record<string, any> = {}) {
return JSON.stringify({
distinctId: 'visitor-test-uuid-1-1234567890',
distinctId: 'anonymous-test-uuid-1-1234567890',
anonymousId: null,
sessionId: 'session-test-uuid-2-1234567890',
deviceId: 'device-test-uuid-3-1234567890',
Expand Down Expand Up @@ -134,7 +134,7 @@ describe('Altertable', () => {
event: EVENT_PAGEVIEW,
timestamp: expect.stringMatching(REGEXP_DATE_ISO),
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
distinct_id: expect.stringMatching(REGEXP_VISITOR_ID),
distinct_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
anonymous_id: null,
session_id: expect.stringMatching(REGEXP_SESSION_ID),
environment: 'production',
Expand Down Expand Up @@ -165,7 +165,7 @@ describe('Altertable', () => {
payload: {
event: 'eventName',
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
distinct_id: expect.stringMatching(REGEXP_VISITOR_ID),
distinct_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
timestamp: expect.stringMatching(REGEXP_DATE_ISO),
anonymous_id: null,
session_id: expect.stringMatching(REGEXP_SESSION_ID),
Expand Down Expand Up @@ -611,7 +611,7 @@ describe('Altertable', () => {
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
traits: {},
distinct_id: userId,
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
},
});
});
Expand All @@ -629,7 +629,7 @@ describe('Altertable', () => {
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
traits,
distinct_id: userId,
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
},
});
});
Expand Down Expand Up @@ -747,7 +747,7 @@ describe('Altertable', () => {
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
traits: { email: '[email protected]' },
distinct_id: userId,
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
},
});

Expand All @@ -759,7 +759,7 @@ describe('Altertable', () => {
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
traits: { email: '[email protected]' },
distinct_id: userId,
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
},
});
});
Expand Down Expand Up @@ -799,7 +799,7 @@ describe('Altertable', () => {
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
traits: newTraits,
distinct_id: 'user123',
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
session_id: expect.stringMatching(REGEXP_SESSION_ID),
},
});
Expand Down Expand Up @@ -881,7 +881,7 @@ describe('Altertable', () => {
environment: expect.any(String),
device_id: expect.stringMatching(REGEXP_DEVICE_ID),
new_user_id: 'user456',
distinct_id: expect.stringMatching(REGEXP_VISITOR_ID),
distinct_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
anonymous_id: null,
})
);
Expand Down Expand Up @@ -933,7 +933,7 @@ describe('Altertable', () => {
}).toRequestApi('/alias', {
payload: expect.objectContaining({
new_user_id: 'user456',
distinct_id: expect.stringMatching(REGEXP_VISITOR_ID),
distinct_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
anonymous_id: null,
}),
});
Expand All @@ -954,7 +954,7 @@ describe('Altertable', () => {
payload: expect.objectContaining({
new_user_id: 'user456',
distinct_id: 'user123',
anonymous_id: expect.stringMatching(REGEXP_VISITOR_ID),
anonymous_id: expect.stringMatching(REGEXP_ANONYMOUS_ID),
}),
});
});
Expand All @@ -973,10 +973,10 @@ describe('Altertable', () => {
});

it('persists session ID across page reloads', () => {
const testVisitorId = 'visitor-test-uuid-1-1234567890';
const testAnonymousId = 'anonymous-test-uuid-1-1234567890';
const testSessionId = 'session-test-uuid-2-1234567890';
const existingSessionData = createSessionData({
visitorId: testVisitorId,
anonymousId: testAnonymousId,
sessionId: testSessionId,
});

Expand All @@ -999,13 +999,13 @@ describe('Altertable', () => {
describe('session renewal', () => {
it('regenerates session ID when event sent after 30 minutes', () => {
vi.useFakeTimers();
const testVisitorId = 'visitor-test-uuid-3-1234567890';
const testAnonymousId = 'anonymous-test-uuid-3-1234567890';
const testSessionId = 'session-test-uuid-4-1234567890';
const thirtyMinutesAgo = new Date(
Date.now() - 30 * 60 * 1000 - 1000
).toISOString();
const existingSessionData = createSessionData({
visitorId: testVisitorId,
anonymousId: testAnonymousId,
sessionId: testSessionId,
lastEventAt: thirtyMinutesAgo,
});
Expand Down Expand Up @@ -1045,13 +1045,13 @@ describe('Altertable', () => {

it('does not regenerate session ID when event sent within 30 minutes', () => {
vi.useFakeTimers();
const testVisitorId = 'visitor-test-uuid-5-1234567890';
const testAnonymousId = 'anonymous-test-uuid-5-1234567890';
const testSessionId = 'session-test-uuid-6-1234567890';
const twentyNineMinutesAgo = new Date(
Date.now() - 29 * 60 * 1000
).toISOString();
const existingSessionData = createSessionData({
visitorId: testVisitorId,
anonymousId: testAnonymousId,
sessionId: testSessionId,
lastEventAt: twentyNineMinutesAgo,
});
Expand Down Expand Up @@ -1099,7 +1099,7 @@ describe('Altertable', () => {

altertable.reset();
const distinctId = altertable['_sessionManager'].getDistinctId();
expect(distinctId).toMatch(REGEXP_VISITOR_ID);
expect(distinctId).toMatch(REGEXP_ANONYMOUS_ID);
const anonymousId = altertable['_sessionManager'].getAnonymousId();
expect(anonymousId).toBeNull();
});
Expand All @@ -1120,7 +1120,7 @@ describe('Altertable', () => {
expect(newSessionId).toMatch(REGEXP_SESSION_ID);

const newDistinctId = altertable['_sessionManager'].getDistinctId();
expect(newDistinctId).toMatch(REGEXP_VISITOR_ID);
expect(newDistinctId).toMatch(REGEXP_ANONYMOUS_ID);
const newAnonymousId = altertable['_sessionManager'].getAnonymousId();
expect(newAnonymousId).toBeNull();
});
Expand Down Expand Up @@ -1174,15 +1174,15 @@ describe('Altertable', () => {
];
const storedData = JSON.parse(lastCall[1]);
expect(storedData).toMatchObject({
anonymousId: expect.stringMatching(REGEXP_VISITOR_ID),
anonymousId: expect.stringMatching(REGEXP_ANONYMOUS_ID),
sessionId: expect.stringMatching(REGEXP_SESSION_ID),
distinctId: 'user123',
lastEventAt: null,
});
});

it('recovers storage data on initialization', () => {
const testAnonymousId = 'visitor-test-uuid-3-1234567890';
const testAnonymousId = 'anonymous-test-uuid-3-1234567890';
const testSessionId = 'session-test-uuid-4-1234567890';
const existingData = createSessionData({
anonymousId: testAnonymousId,
Expand Down Expand Up @@ -1233,7 +1233,7 @@ describe('Altertable', () => {
const distinctId = altertable['_sessionManager'].getDistinctId();
const sessionId = altertable['_sessionManager'].getSessionId();
const anonymousId = altertable['_sessionManager'].getAnonymousId();
expect(distinctId).toMatch(REGEXP_VISITOR_ID);
expect(distinctId).toMatch(REGEXP_ANONYMOUS_ID);
expect(sessionId).toMatch(REGEXP_SESSION_ID);
expect(anonymousId).toBeNull();
});
Expand Down Expand Up @@ -1635,7 +1635,7 @@ describe('Altertable', () => {
// 6. Reset (should clear state)
altertable.reset();
expect(altertable['_sessionManager'].getDistinctId()).toMatch(
REGEXP_VISITOR_ID
REGEXP_ANONYMOUS_ID
);
expect(altertable['_sessionManager'].getAnonymousId()).toBeNull();
expect(altertable['_sessionManager'].getSessionId()).toMatch(
Expand Down
Loading