Fix identified session ID generation

This commit is contained in:
Mike Cao committed 2026-07-19 10:43:21 -07:00
1 parent 4691a344c5
commit cb801230cc
3 files changed
+72 -2

No files matched your search

+3 -2
View File
@@ -10,6 +10,7 @@ import { fetchWebsite } from '@/lib/load';
import { parseRequest } from '@/lib/request';
import { badRequest, forbidden, json, serverError } from '@/lib/response';
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
import { getSessionId } from '@/lib/session';
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
import { createSession, saveEvent, saveSessionData, saveSessionLink } from '@/queries/sql';
@@ -155,7 +156,7 @@ export async function POST(request: Request) {
const sessionSalt = getSalt(saltRotation, createdAt);
const visitSalt = hash(startOfHour(createdAt).toUTCString());
const sessionId = id ? uuid(sourceId, id) : uuid(sourceId, ip, userAgent, sessionSalt);
const sessionId = getSessionId(sourceId, ip, userAgent, sessionSalt, id);
// Create a session if not found
if (!clickhouse.enabled && !cache?.sessionId) {
@@ -284,7 +285,7 @@ export async function POST(request: Request) {
if (websiteId && id) {
// Persist both memberships so stitched activity can be resolved from
// either the anonymous session or the identified session.
const anonymousSessionId = uuid(sourceId, ip, userAgent, sessionSalt);
const anonymousSessionId = getSessionId(sourceId, ip, userAgent, sessionSalt);
const linkedSessionIds = [...new Set([anonymousSessionId, sessionId])];
const newLinkId = hash(...linkedSessionIds, id);
+56
View File
@@ -0,0 +1,56 @@
import { expect, test } from 'vitest';
import { uuid } from './crypto';
import { getSessionId } from './session';
const sourceId = '2e2fc91f-77a7-4e5d-a7b1-18a74c67b4e0';
const sessionSalt = 'monthly-session-salt';
test('keeps identified sessions stable on the same device', () => {
const args = [sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt, 'user-123'] as const;
expect(getSessionId(...args)).toBe(getSessionId(...args));
});
test('creates separate sessions for the same distinct ID on different devices', () => {
const desktopSessionId = getSessionId(
sourceId,
'192.0.2.1',
'Desktop Browser',
sessionSalt,
'user-123',
);
const mobileSessionId = getSessionId(
sourceId,
'198.51.100.2',
'Mobile Browser',
sessionSalt,
'user-123',
);
expect(desktopSessionId).not.toBe(mobileSessionId);
});
test('does not merge different identified users on the same device', () => {
const firstUserSessionId = getSessionId(
sourceId,
'192.0.2.1',
'Desktop Browser',
sessionSalt,
'user-123',
);
const secondUserSessionId = getSessionId(
sourceId,
'192.0.2.1',
'Desktop Browser',
sessionSalt,
'user-456',
);
expect(firstUserSessionId).not.toBe(secondUserSessionId);
});
test('preserves anonymous session IDs', () => {
expect(getSessionId(sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt)).toBe(
uuid(sourceId, '192.0.2.1', 'Desktop Browser', sessionSalt),
);
});
+13
View File
@@ -0,0 +1,13 @@
import { uuid } from './crypto';
export function getSessionId(
sourceId: string,
ip: string,
userAgent: string,
sessionSalt: string,
distinctId?: string,
) {
return distinctId
? uuid(sourceId, ip, userAgent, sessionSalt, distinctId)
: uuid(sourceId, ip, userAgent, sessionSalt);
}