@faststats/web 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/REPLAY_PAYLOAD.md +64 -0
- package/dist/chunks/api-urls-DaeYkG0_.js +1 -0
- package/dist/chunks/error-Cd9PTS5v.js +2 -0
- package/dist/chunks/{feature-flags-BxZz_lNm.d.ts → feature-flags-BClx56v5.d.ts} +1 -0
- package/dist/chunks/feature-flags-DSOCIZHK.js +1 -0
- package/dist/chunks/{replay-BuX3_0xs.d.ts → replay-DvJYurEC.d.ts} +10 -2
- package/dist/chunks/replay-rTqcjOo2.js +1 -0
- package/dist/chunks/send-data-B2fYGj6v.js +1 -0
- package/dist/chunks/session-manager-Cy63ptPF.js +1 -0
- package/dist/chunks/web-vitals-Be-Cg4Po.js +1 -0
- package/dist/error.d.ts +5 -8
- package/dist/error.js +1 -1
- package/dist/feature-flags.d.ts +1 -1
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +7 -15
- package/dist/web-vitals.js +1 -1
- package/package.json +4 -7
- package/src/analytics.ts +25 -14
- package/src/error.ts +110 -158
- package/src/feature-flags.ts +5 -2
- package/src/replay.ts +179 -52
- package/src/utils/api-urls.ts +7 -20
- package/src/utils/identifiers.ts +30 -72
- package/src/utils/session-manager.ts +416 -0
- package/src/web-vitals.ts +66 -168
- package/tests/analytics.test.ts +8 -4
- package/tests/identifiers.test.ts +15 -32
- package/tests/replay.test.ts +180 -10
- package/tests/session-manager.test.ts +161 -0
- package/dist/chunks/api-urls-BrkcoElX.js +0 -1
- package/dist/chunks/error-CttYL43D.js +0 -2
- package/dist/chunks/feature-flags-CjnLZGxp.js +0 -1
- package/dist/chunks/identifiers-CQeWm7wi.js +0 -1
- package/dist/chunks/replay-BrMLCiBF.js +0 -1
- package/dist/chunks/send-data-DL_GlsQw.js +0 -1
- package/dist/chunks/web-vitals-CjA1bFLG.js +0 -1
- package/wrangler.toml +0 -8
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
export type SessionContext = {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
windowId: string;
|
|
4
|
+
sessionStart: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type SessionRotation = {
|
|
8
|
+
prev: SessionContext;
|
|
9
|
+
next: SessionContext;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
13
|
+
const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
const LS_SESSION_ID = "faststats_session_id";
|
|
16
|
+
const LS_SESSION_ACTIVITY = "faststats_session_activity";
|
|
17
|
+
const LS_SESSION_START = "faststats_session_start";
|
|
18
|
+
|
|
19
|
+
const LEGACY_SESSION_ID = "session_id";
|
|
20
|
+
const LEGACY_SESSION_TIMESTAMP = "session_timestamp";
|
|
21
|
+
const LEGACY_SESSION_START = "session_start";
|
|
22
|
+
|
|
23
|
+
let cookielessMode = false;
|
|
24
|
+
let defaultSiteKey = "";
|
|
25
|
+
let ephemeralSession: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
sessionStart: number;
|
|
28
|
+
activity: number;
|
|
29
|
+
} | null = null;
|
|
30
|
+
|
|
31
|
+
type RotationListener = (prev: SessionContext, next: SessionContext) => void;
|
|
32
|
+
const rotationListeners = new Set<RotationListener>();
|
|
33
|
+
|
|
34
|
+
function storageGet(storage: Storage | undefined, key: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
return storage?.getItem(key) ?? null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function storageSet(
|
|
43
|
+
storage: Storage | undefined,
|
|
44
|
+
key: string,
|
|
45
|
+
value: string,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (!storage) return false;
|
|
48
|
+
try {
|
|
49
|
+
storage.setItem(key, value);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function storageRemove(storage: Storage | undefined, key: string): void {
|
|
57
|
+
try {
|
|
58
|
+
storage?.removeItem(key);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getLocalStorage(): Storage | undefined {
|
|
63
|
+
try {
|
|
64
|
+
return globalThis.localStorage;
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSessionStorage(): Storage | undefined {
|
|
71
|
+
try {
|
|
72
|
+
return globalThis.sessionStorage;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function notifyRotation(prev: SessionContext, next: SessionContext): void {
|
|
79
|
+
for (const listener of rotationListeners) {
|
|
80
|
+
listener(prev, next);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createId(): string {
|
|
85
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
86
|
+
return crypto.randomUUID();
|
|
87
|
+
}
|
|
88
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setCookielessMode(mode: boolean): void {
|
|
92
|
+
cookielessMode = mode;
|
|
93
|
+
if (mode) ephemeralSession = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isCookielessMode(): boolean {
|
|
97
|
+
return cookielessMode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function setDefaultSiteKey(siteKey: string): void {
|
|
101
|
+
defaultSiteKey = siteKey;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function windowIdKey(siteKey: string): string {
|
|
105
|
+
return `faststats_window_id_${siteKey}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseTimestamp(value: string | null): number | null {
|
|
109
|
+
if (!value) return null;
|
|
110
|
+
const parsed = Number.parseInt(value, 10);
|
|
111
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function migrateLegacySession(): void {
|
|
115
|
+
const session = getSessionStorage();
|
|
116
|
+
const local = getLocalStorage();
|
|
117
|
+
const legacyId = storageGet(session, LEGACY_SESSION_ID);
|
|
118
|
+
if (!legacyId || storageGet(local, LS_SESSION_ID)) {
|
|
119
|
+
if (legacyId) {
|
|
120
|
+
storageRemove(session, LEGACY_SESSION_ID);
|
|
121
|
+
storageRemove(session, LEGACY_SESSION_TIMESTAMP);
|
|
122
|
+
storageRemove(session, LEGACY_SESSION_START);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const legacyActivity = parseTimestamp(
|
|
128
|
+
storageGet(session, LEGACY_SESSION_TIMESTAMP),
|
|
129
|
+
);
|
|
130
|
+
const legacyStart = parseTimestamp(storageGet(session, LEGACY_SESSION_START));
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
|
|
133
|
+
const migrated = storageSet(local, LS_SESSION_ID, legacyId);
|
|
134
|
+
storageSet(local, LS_SESSION_ACTIVITY, String(legacyActivity ?? now));
|
|
135
|
+
storageSet(
|
|
136
|
+
local,
|
|
137
|
+
LS_SESSION_START,
|
|
138
|
+
String(legacyStart ?? legacyActivity ?? now),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (migrated) {
|
|
142
|
+
storageRemove(session, LEGACY_SESSION_ID);
|
|
143
|
+
storageRemove(session, LEGACY_SESSION_TIMESTAMP);
|
|
144
|
+
storageRemove(session, LEGACY_SESSION_START);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readPersistedSession(): {
|
|
149
|
+
sessionId: string;
|
|
150
|
+
activity: number;
|
|
151
|
+
sessionStart: number;
|
|
152
|
+
} | null {
|
|
153
|
+
migrateLegacySession();
|
|
154
|
+
|
|
155
|
+
const local = getLocalStorage();
|
|
156
|
+
const sessionId = storageGet(local, LS_SESSION_ID);
|
|
157
|
+
const activity = parseTimestamp(storageGet(local, LS_SESSION_ACTIVITY));
|
|
158
|
+
const sessionStart = parseTimestamp(storageGet(local, LS_SESSION_START));
|
|
159
|
+
|
|
160
|
+
if (!sessionId || activity === null) return null;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
sessionId,
|
|
164
|
+
activity,
|
|
165
|
+
sessionStart: sessionStart ?? activity,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writePersistedSession(
|
|
170
|
+
sessionId: string,
|
|
171
|
+
activity: number,
|
|
172
|
+
sessionStart: number,
|
|
173
|
+
): void {
|
|
174
|
+
const local = getLocalStorage();
|
|
175
|
+
const wroteId = storageSet(local, LS_SESSION_ID, sessionId);
|
|
176
|
+
storageSet(local, LS_SESSION_ACTIVITY, activity.toString());
|
|
177
|
+
storageSet(local, LS_SESSION_START, sessionStart.toString());
|
|
178
|
+
|
|
179
|
+
if (!wroteId) {
|
|
180
|
+
ephemeralSession = { sessionId, activity, sessionStart };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function clearPersistedSession(): void {
|
|
185
|
+
const local = getLocalStorage();
|
|
186
|
+
storageRemove(local, LS_SESSION_ID);
|
|
187
|
+
storageRemove(local, LS_SESSION_ACTIVITY);
|
|
188
|
+
storageRemove(local, LS_SESSION_START);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isSessionExpired(
|
|
192
|
+
activity: number,
|
|
193
|
+
sessionStart: number,
|
|
194
|
+
now: number,
|
|
195
|
+
): boolean {
|
|
196
|
+
if (now - activity >= SESSION_IDLE_TIMEOUT_MS) return true;
|
|
197
|
+
return now - sessionStart >= SESSION_MAX_AGE_MS;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function ephemeralRecord(now: number): {
|
|
201
|
+
sessionId: string;
|
|
202
|
+
sessionStart: number;
|
|
203
|
+
activity: number;
|
|
204
|
+
} {
|
|
205
|
+
if (!ephemeralSession) {
|
|
206
|
+
ephemeralSession = {
|
|
207
|
+
sessionId: createId(),
|
|
208
|
+
sessionStart: now,
|
|
209
|
+
activity: now,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return ephemeralSession;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveSessionRecord(
|
|
216
|
+
cookieless: boolean,
|
|
217
|
+
touch: boolean,
|
|
218
|
+
): {
|
|
219
|
+
sessionId: string;
|
|
220
|
+
sessionStart: number;
|
|
221
|
+
rotated: boolean;
|
|
222
|
+
previous: { sessionId: string; sessionStart: number } | null;
|
|
223
|
+
} {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
|
|
226
|
+
if (cookieless) {
|
|
227
|
+
const record = ephemeralRecord(now);
|
|
228
|
+
if (touch) record.activity = now;
|
|
229
|
+
return {
|
|
230
|
+
sessionId: record.sessionId,
|
|
231
|
+
sessionStart: record.sessionStart,
|
|
232
|
+
rotated: false,
|
|
233
|
+
previous: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const existing = readPersistedSession() ?? ephemeralSession;
|
|
238
|
+
if (
|
|
239
|
+
existing &&
|
|
240
|
+
!isSessionExpired(existing.activity, existing.sessionStart, now)
|
|
241
|
+
) {
|
|
242
|
+
if (touch) {
|
|
243
|
+
writePersistedSession(existing.sessionId, now, existing.sessionStart);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
sessionId: existing.sessionId,
|
|
247
|
+
sessionStart: existing.sessionStart,
|
|
248
|
+
rotated: false,
|
|
249
|
+
previous: null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const previous =
|
|
254
|
+
existing !== null
|
|
255
|
+
? {
|
|
256
|
+
sessionId: existing.sessionId,
|
|
257
|
+
sessionStart: existing.sessionStart,
|
|
258
|
+
}
|
|
259
|
+
: null;
|
|
260
|
+
|
|
261
|
+
const sessionId = createId();
|
|
262
|
+
writePersistedSession(sessionId, now, now);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
sessionId,
|
|
266
|
+
sessionStart: now,
|
|
267
|
+
rotated: previous !== null,
|
|
268
|
+
previous,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getOrCreateWindowId(
|
|
273
|
+
siteKey: string,
|
|
274
|
+
cookieless?: boolean,
|
|
275
|
+
): string {
|
|
276
|
+
if (cookieless ?? cookielessMode) {
|
|
277
|
+
return resolveSessionRecord(true, false).sessionId;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const key = windowIdKey(siteKey);
|
|
281
|
+
const session = getSessionStorage();
|
|
282
|
+
if (!session) return resolveSessionRecord(false, false).sessionId;
|
|
283
|
+
|
|
284
|
+
const existing = storageGet(session, key);
|
|
285
|
+
if (existing) return existing;
|
|
286
|
+
|
|
287
|
+
const windowId = createId();
|
|
288
|
+
storageSet(session, key, windowId);
|
|
289
|
+
return windowId;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clearWindowId(siteKey: string): void {
|
|
293
|
+
storageRemove(getSessionStorage(), windowIdKey(siteKey));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function getSessionContext(
|
|
297
|
+
siteKey: string,
|
|
298
|
+
cookieless?: boolean,
|
|
299
|
+
): SessionContext {
|
|
300
|
+
const useCookieless = cookieless ?? cookielessMode;
|
|
301
|
+
const record = resolveSessionRecord(useCookieless, false);
|
|
302
|
+
const windowId = getOrCreateWindowId(siteKey, useCookieless);
|
|
303
|
+
|
|
304
|
+
const context: SessionContext = {
|
|
305
|
+
sessionId: record.sessionId,
|
|
306
|
+
windowId,
|
|
307
|
+
sessionStart: record.sessionStart,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (record.rotated && record.previous) {
|
|
311
|
+
const prev: SessionContext = {
|
|
312
|
+
sessionId: record.previous.sessionId,
|
|
313
|
+
windowId,
|
|
314
|
+
sessionStart: record.previous.sessionStart,
|
|
315
|
+
};
|
|
316
|
+
notifyRotation(prev, context);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return context;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function getOrCreateSessionId(cookieless?: boolean): string {
|
|
323
|
+
return resolveSessionRecord(cookieless ?? cookielessMode, true).sessionId;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function getSessionStart(cookieless?: boolean): number {
|
|
327
|
+
return resolveSessionRecord(cookieless ?? cookielessMode, false).sessionStart;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function touchActivity(cookieless?: boolean): SessionRotation | null {
|
|
331
|
+
const useCookieless = cookieless ?? cookielessMode;
|
|
332
|
+
const record = resolveSessionRecord(useCookieless, true);
|
|
333
|
+
if (record.rotated && record.previous) {
|
|
334
|
+
const siteKey = defaultSiteKey || "_default";
|
|
335
|
+
const rotation = {
|
|
336
|
+
prev: {
|
|
337
|
+
sessionId: record.previous.sessionId,
|
|
338
|
+
windowId: getOrCreateWindowId(siteKey, useCookieless),
|
|
339
|
+
sessionStart: record.previous.sessionStart,
|
|
340
|
+
},
|
|
341
|
+
next: {
|
|
342
|
+
sessionId: record.sessionId,
|
|
343
|
+
windowId: getOrCreateWindowId(siteKey, useCookieless),
|
|
344
|
+
sessionStart: record.sessionStart,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
notifyRotation(rotation.prev, rotation.next);
|
|
348
|
+
return rotation;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function refreshSessionTimestamp(cookieless?: boolean): void {
|
|
354
|
+
const useCookieless = cookieless ?? cookielessMode;
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
|
|
357
|
+
if (useCookieless) {
|
|
358
|
+
if (ephemeralSession) ephemeralSession.activity = now;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const existing = readPersistedSession() ?? ephemeralSession;
|
|
363
|
+
if (!existing) return;
|
|
364
|
+
|
|
365
|
+
writePersistedSession(existing.sessionId, now, existing.sessionStart);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function resetSession(siteKey?: string): SessionContext {
|
|
369
|
+
const key = siteKey ?? defaultSiteKey ?? "_default";
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
let previous: SessionContext | null = null;
|
|
372
|
+
|
|
373
|
+
if (cookielessMode) {
|
|
374
|
+
if (ephemeralSession) {
|
|
375
|
+
previous = {
|
|
376
|
+
sessionId: ephemeralSession.sessionId,
|
|
377
|
+
windowId: ephemeralSession.sessionId,
|
|
378
|
+
sessionStart: ephemeralSession.sessionStart,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
ephemeralSession = {
|
|
382
|
+
sessionId: createId(),
|
|
383
|
+
sessionStart: now,
|
|
384
|
+
activity: now,
|
|
385
|
+
};
|
|
386
|
+
} else {
|
|
387
|
+
const existing = readPersistedSession();
|
|
388
|
+
if (existing) {
|
|
389
|
+
previous = {
|
|
390
|
+
sessionId: existing.sessionId,
|
|
391
|
+
windowId: getOrCreateWindowId(key, false),
|
|
392
|
+
sessionStart: existing.sessionStart,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
clearPersistedSession();
|
|
396
|
+
ephemeralSession = null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
clearWindowId(key);
|
|
400
|
+
const next = getSessionContext(key, cookielessMode);
|
|
401
|
+
|
|
402
|
+
if (previous && previous.sessionId !== next.sessionId) {
|
|
403
|
+
notifyRotation(previous, next);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return next;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function onSessionRotated(
|
|
410
|
+
listener: (prev: SessionContext, next: SessionContext) => void,
|
|
411
|
+
): () => void {
|
|
412
|
+
rotationListeners.add(listener);
|
|
413
|
+
return () => {
|
|
414
|
+
rotationListeners.delete(listener);
|
|
415
|
+
};
|
|
416
|
+
}
|