@faststats/web 0.2.13 → 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 +7 -0
- package/REPLAY_PAYLOAD.md +64 -0
- package/dist/chunks/{error-CDVOMiI9.js → error-Cd9PTS5v.js} +2 -2
- 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.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +1 -1
- package/dist/replay.js +1 -1
- package/dist/web-vitals.js +1 -1
- package/package.json +4 -7
- package/src/analytics.ts +21 -10
- package/src/error.ts +6 -1
- package/src/replay.ts +177 -50
- package/src/utils/identifiers.ts +30 -72
- package/src/utils/session-manager.ts +416 -0
- package/src/web-vitals.ts +1 -1
- package/tests/analytics.test.ts +7 -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/identifiers-CQeWm7wi.js +0 -1
- package/dist/chunks/replay-CHFW2q4E.js +0 -1
- package/dist/chunks/send-data-DL_GlsQw.js +0 -1
- package/dist/chunks/web-vitals-GwPlL7Zy.js +0 -1
- package/wrangler.toml +0 -8
package/tests/replay.test.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { gunzipSync } from "node:zlib";
|
|
3
3
|
import ReplayTracker from "../src/replay";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getOrCreateSessionId,
|
|
6
|
+
type SessionContext,
|
|
7
|
+
setDefaultSiteKey,
|
|
8
|
+
} from "../src/utils/session-manager";
|
|
5
9
|
|
|
6
10
|
function decodeBody(data: string | Uint8Array): string {
|
|
7
11
|
if (typeof data === "string") return data;
|
|
@@ -32,12 +36,16 @@ type ReplayEvent = {
|
|
|
32
36
|
|
|
33
37
|
type ReplayBatch = {
|
|
34
38
|
token: string;
|
|
35
|
-
sessionId
|
|
39
|
+
sessionId: string;
|
|
40
|
+
windowId: string;
|
|
41
|
+
viewId: string;
|
|
42
|
+
sessionStart: number;
|
|
36
43
|
identifier?: string;
|
|
37
44
|
batchId: string;
|
|
38
45
|
sequence: number;
|
|
39
46
|
timestamp: number;
|
|
40
47
|
url: string;
|
|
48
|
+
isFinal?: boolean;
|
|
41
49
|
events: ReplayEvent[];
|
|
42
50
|
};
|
|
43
51
|
|
|
@@ -49,7 +57,12 @@ type ReplayTrackerInternals = {
|
|
|
49
57
|
retryTask: ReturnType<typeof setTimeout> | null;
|
|
50
58
|
startTime: number;
|
|
51
59
|
onEvent: (event: ReplayEvent, isCheckout?: boolean) => void;
|
|
52
|
-
|
|
60
|
+
trackPageChange: (url?: string) => void;
|
|
61
|
+
flush: (
|
|
62
|
+
lowLatency: boolean,
|
|
63
|
+
contextOverride?: SessionContext,
|
|
64
|
+
isFinal?: boolean,
|
|
65
|
+
) => Promise<void>;
|
|
53
66
|
send: (
|
|
54
67
|
data: string | Uint8Array,
|
|
55
68
|
isCompressed: boolean,
|
|
@@ -89,6 +102,7 @@ beforeEach(() => {
|
|
|
89
102
|
setGlobal("location", { href: "https://example.com/" } as Location);
|
|
90
103
|
setGlobal("localStorage", new MockStorage());
|
|
91
104
|
setGlobal("sessionStorage", new MockStorage());
|
|
105
|
+
setDefaultSiteKey("site_test");
|
|
92
106
|
setGlobal("navigator", { sendBeacon: () => false });
|
|
93
107
|
setGlobal(
|
|
94
108
|
"fetch",
|
|
@@ -111,10 +125,25 @@ function createTracker(
|
|
|
111
125
|
): ReplayTrackerInternals {
|
|
112
126
|
return new ReplayTracker({
|
|
113
127
|
siteKey: "site_test",
|
|
128
|
+
samplingPercentage: 100,
|
|
114
129
|
...options,
|
|
115
130
|
}) as unknown as ReplayTrackerInternals;
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
async function captureReplayBatch(
|
|
134
|
+
tracker: ReplayTrackerInternals,
|
|
135
|
+
): Promise<ReplayBatch> {
|
|
136
|
+
const payloads: ReplayBatch[] = [];
|
|
137
|
+
tracker.send = async (data) => {
|
|
138
|
+
payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
|
|
139
|
+
return true;
|
|
140
|
+
};
|
|
141
|
+
tracker.startTime = Date.now() - 1000;
|
|
142
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
143
|
+
await tracker.flush(false);
|
|
144
|
+
return payloads[0] as ReplayBatch;
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
describe("ReplayTracker", () => {
|
|
119
148
|
test("buffers replay events", () => {
|
|
120
149
|
const tracker = createTracker({
|
|
@@ -192,6 +221,9 @@ describe("ReplayTracker", () => {
|
|
|
192
221
|
expect(captured.payload).not.toBeNull();
|
|
193
222
|
expect(captured.payload?.token).toBe("site_test");
|
|
194
223
|
expect(captured.payload?.sessionId).toBeTruthy();
|
|
224
|
+
expect(captured.payload?.windowId).toBeTruthy();
|
|
225
|
+
expect(captured.payload?.viewId).toBeTruthy();
|
|
226
|
+
expect(captured.payload?.sessionStart).toBeGreaterThan(0);
|
|
195
227
|
expect(captured.payload?.batchId).toBeTruthy();
|
|
196
228
|
expect(captured.payload?.events).toHaveLength(1);
|
|
197
229
|
expect(captured.lowLatency).toBe(true);
|
|
@@ -232,15 +264,10 @@ describe("ReplayTracker", () => {
|
|
|
232
264
|
return true;
|
|
233
265
|
};
|
|
234
266
|
|
|
235
|
-
(
|
|
236
|
-
getOrCreateSessionId();
|
|
267
|
+
getOrCreateSessionId();
|
|
237
268
|
tracker.startTime = Date.now() - 1000;
|
|
238
269
|
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
239
270
|
await tracker.flush(false);
|
|
240
|
-
|
|
241
|
-
const storage = globalThis.sessionStorage as unknown as MockStorage;
|
|
242
|
-
storage.setItem("session_timestamp", "0");
|
|
243
|
-
|
|
244
271
|
tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
|
|
245
272
|
await tracker.flush(false);
|
|
246
273
|
|
|
@@ -249,6 +276,149 @@ describe("ReplayTracker", () => {
|
|
|
249
276
|
expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
|
|
250
277
|
});
|
|
251
278
|
|
|
279
|
+
test("uses a new session id after idle timeout", async () => {
|
|
280
|
+
const tracker = createTracker({
|
|
281
|
+
minReplayLengthMs: 0,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const payloads: ReplayBatch[] = [];
|
|
285
|
+
tracker.send = async (data) => {
|
|
286
|
+
payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
|
|
287
|
+
return true;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
getOrCreateSessionId();
|
|
291
|
+
tracker.startTime = Date.now() - 1000;
|
|
292
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
293
|
+
await tracker.flush(false);
|
|
294
|
+
|
|
295
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
296
|
+
local.setItem(
|
|
297
|
+
"faststats_session_activity",
|
|
298
|
+
(Date.now() - 31 * 60 * 1000).toString(),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
|
|
302
|
+
await tracker.flush(false);
|
|
303
|
+
|
|
304
|
+
tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
|
|
305
|
+
await tracker.flush(false);
|
|
306
|
+
|
|
307
|
+
expect(payloads).toHaveLength(3);
|
|
308
|
+
expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
|
|
309
|
+
expect(payloads[1]?.isFinal).toBe(true);
|
|
310
|
+
expect(payloads[2]?.sessionId).not.toBe(payloads[0]?.sessionId);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("finalizes queued events under previous session after idle timeout", async () => {
|
|
314
|
+
const tracker = createTracker({
|
|
315
|
+
minReplayLengthMs: 0,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const payloads: ReplayBatch[] = [];
|
|
319
|
+
tracker.send = async (data) => {
|
|
320
|
+
payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
|
|
321
|
+
return true;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const previousSessionId = getOrCreateSessionId();
|
|
325
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
326
|
+
local.setItem(
|
|
327
|
+
"faststats_session_activity",
|
|
328
|
+
(Date.now() - 31 * 60 * 1000).toString(),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
tracker.startTime = Date.now() - 1000;
|
|
332
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
333
|
+
await tracker.flush(false);
|
|
334
|
+
|
|
335
|
+
expect(payloads).toHaveLength(1);
|
|
336
|
+
expect(payloads[0]?.sessionId).toBe(previousSessionId);
|
|
337
|
+
expect(payloads[0]?.isFinal).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("keeps the same window and view ids across replay batches", async () => {
|
|
341
|
+
const tracker = createTracker({
|
|
342
|
+
minReplayLengthMs: 0,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const payloads: ReplayBatch[] = [];
|
|
346
|
+
tracker.send = async (data) => {
|
|
347
|
+
payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
|
|
348
|
+
return true;
|
|
349
|
+
};
|
|
350
|
+
getOrCreateSessionId();
|
|
351
|
+
tracker.startTime = Date.now() - 1000;
|
|
352
|
+
|
|
353
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
354
|
+
await tracker.flush(false);
|
|
355
|
+
tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
|
|
356
|
+
await tracker.flush(false);
|
|
357
|
+
|
|
358
|
+
expect(payloads).toHaveLength(2);
|
|
359
|
+
expect(payloads[1]?.windowId).toBe(payloads[0]?.windowId);
|
|
360
|
+
expect(payloads[1]?.viewId).toBe(payloads[0]?.viewId);
|
|
361
|
+
expect(payloads[1]?.sequence).toBe((payloads[0]?.sequence ?? 0) + 1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("window id is stable across replay tracker remounts", async () => {
|
|
365
|
+
const sharedSession = new MockStorage();
|
|
366
|
+
sharedSession.setItem("faststats_window_id_site_test", "stable-window");
|
|
367
|
+
|
|
368
|
+
setGlobal("sessionStorage", sharedSession);
|
|
369
|
+
|
|
370
|
+
const first = createTracker({ minReplayLengthMs: 0 });
|
|
371
|
+
const second = createTracker({ minReplayLengthMs: 0 });
|
|
372
|
+
|
|
373
|
+
const firstBatch = await captureReplayBatch(first);
|
|
374
|
+
const secondBatch = await captureReplayBatch(second);
|
|
375
|
+
|
|
376
|
+
expect(firstBatch.windowId).toBe("stable-window");
|
|
377
|
+
expect(secondBatch.windowId).toBe("stable-window");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("same session across tabs with different window ids", async () => {
|
|
381
|
+
const sharedLocal = new MockStorage();
|
|
382
|
+
sharedLocal.setItem("faststats_session_id", "shared-session");
|
|
383
|
+
sharedLocal.setItem("faststats_session_activity", Date.now().toString());
|
|
384
|
+
sharedLocal.setItem("faststats_session_start", Date.now().toString());
|
|
385
|
+
|
|
386
|
+
setGlobal("localStorage", sharedLocal);
|
|
387
|
+
setGlobal("sessionStorage", new MockStorage());
|
|
388
|
+
|
|
389
|
+
const first = createTracker({ minReplayLengthMs: 0 });
|
|
390
|
+
const firstBatch = await captureReplayBatch(first);
|
|
391
|
+
|
|
392
|
+
setGlobal("sessionStorage", new MockStorage());
|
|
393
|
+
const second = createTracker({ minReplayLengthMs: 0 });
|
|
394
|
+
const secondBatch = await captureReplayBatch(second);
|
|
395
|
+
|
|
396
|
+
expect(firstBatch.sessionId).toBe("shared-session");
|
|
397
|
+
expect(secondBatch.sessionId).toBe("shared-session");
|
|
398
|
+
expect(firstBatch.windowId).not.toBe(secondBatch.windowId);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("trackPageChange rotates viewId and emits meta event", () => {
|
|
402
|
+
const tracker = createTracker();
|
|
403
|
+
(tracker as unknown as { started: boolean }).started = true;
|
|
404
|
+
const firstViewId = (tracker as unknown as { viewId: string }).viewId;
|
|
405
|
+
|
|
406
|
+
tracker.trackPageChange("https://example.com/about");
|
|
407
|
+
|
|
408
|
+
const viewEvent = tracker.events.find(
|
|
409
|
+
(e) =>
|
|
410
|
+
(e.data as Record<string, unknown> | undefined)?.tag ===
|
|
411
|
+
"faststats:view",
|
|
412
|
+
);
|
|
413
|
+
expect(viewEvent).toBeDefined();
|
|
414
|
+
const payload = (viewEvent?.data as Record<string, unknown> | undefined)
|
|
415
|
+
?.payload as Record<string, unknown> | undefined;
|
|
416
|
+
expect(payload?.href).toBe("https://example.com/about");
|
|
417
|
+
expect((tracker as unknown as { viewId: string }).viewId).not.toBe(
|
|
418
|
+
firstViewId,
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
252
422
|
test("sends replay as plain JSON when CompressionStream is unavailable", async () => {
|
|
253
423
|
const tracker = createTracker({
|
|
254
424
|
minReplayLengthMs: 0,
|
|
@@ -332,7 +502,7 @@ describe("ReplayTracker", () => {
|
|
|
332
502
|
tracker.startTime = Date.now() - 1000;
|
|
333
503
|
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
334
504
|
|
|
335
|
-
await tracker.flush(true);
|
|
505
|
+
await tracker.flush(true, undefined, true);
|
|
336
506
|
|
|
337
507
|
expect(compressedFlag).toBe(true);
|
|
338
508
|
expect(lowLatencyFlag).toBe(true);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createId,
|
|
4
|
+
getOrCreateSessionId,
|
|
5
|
+
getSessionContext,
|
|
6
|
+
getSessionStart,
|
|
7
|
+
onSessionRotated,
|
|
8
|
+
resetSession,
|
|
9
|
+
setCookielessMode,
|
|
10
|
+
setDefaultSiteKey,
|
|
11
|
+
touchActivity,
|
|
12
|
+
} from "../src/utils/session-manager";
|
|
13
|
+
|
|
14
|
+
class MockStorage {
|
|
15
|
+
private readonly data = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
getItem(key: string): string | null {
|
|
18
|
+
return this.data.get(key) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setItem(key: string, value: string): void {
|
|
22
|
+
this.data.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeItem(key: string): void {
|
|
26
|
+
this.data.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setGlobal(name: keyof typeof globalThis, value: unknown): void {
|
|
31
|
+
Object.defineProperty(globalThis, name, {
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const original = {
|
|
39
|
+
localStorage: globalThis.localStorage,
|
|
40
|
+
sessionStorage: globalThis.sessionStorage,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
setGlobal("localStorage", new MockStorage());
|
|
45
|
+
setGlobal("sessionStorage", new MockStorage());
|
|
46
|
+
setCookielessMode(true);
|
|
47
|
+
setCookielessMode(false);
|
|
48
|
+
setDefaultSiteKey("site_test");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
setGlobal("localStorage", original.localStorage);
|
|
53
|
+
setGlobal("sessionStorage", original.sessionStorage);
|
|
54
|
+
setCookielessMode(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("session-manager", () => {
|
|
58
|
+
test("rotates session after idle timeout", () => {
|
|
59
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
60
|
+
const idleMs = 30 * 60 * 1000 + 1;
|
|
61
|
+
local.setItem("faststats_session_id", "old-session");
|
|
62
|
+
local.setItem(
|
|
63
|
+
"faststats_session_activity",
|
|
64
|
+
(Date.now() - idleMs).toString(),
|
|
65
|
+
);
|
|
66
|
+
local.setItem("faststats_session_start", (Date.now() - idleMs).toString());
|
|
67
|
+
|
|
68
|
+
const rotations: Array<{ prev: string; next: string }> = [];
|
|
69
|
+
const unsubscribe = onSessionRotated((prev, next) => {
|
|
70
|
+
rotations.push({ prev: prev.sessionId, next: next.sessionId });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const next = getSessionContext("site_test");
|
|
74
|
+
unsubscribe();
|
|
75
|
+
expect(next.sessionId).not.toBe("old-session");
|
|
76
|
+
expect(rotations).toHaveLength(1);
|
|
77
|
+
expect(rotations[0]?.prev).toBe("old-session");
|
|
78
|
+
expect(rotations[0]?.next).toBe(next.sessionId);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("resetSession notifies rotation listeners", () => {
|
|
82
|
+
const first = getSessionContext("site_test");
|
|
83
|
+
const rotations: Array<{ prev: string; next: string }> = [];
|
|
84
|
+
const unsubscribe = onSessionRotated((prev, next) => {
|
|
85
|
+
rotations.push({ prev: prev.sessionId, next: next.sessionId });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const second = resetSession("site_test");
|
|
89
|
+
unsubscribe();
|
|
90
|
+
expect(second.sessionId).not.toBe(first.sessionId);
|
|
91
|
+
expect(rotations).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("touchActivity extends session without rotating", () => {
|
|
95
|
+
const first = getSessionContext("site_test");
|
|
96
|
+
touchActivity();
|
|
97
|
+
const second = getSessionContext("site_test");
|
|
98
|
+
expect(second.sessionId).toBe(first.sessionId);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("session id is recreated when activity timestamp is invalid", () => {
|
|
102
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
103
|
+
local.setItem("faststats_session_id", "old-session");
|
|
104
|
+
local.setItem("faststats_session_activity", "NaN");
|
|
105
|
+
const id = getOrCreateSessionId();
|
|
106
|
+
expect(id).not.toBe("old-session");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("session start falls back when stored value is invalid", () => {
|
|
110
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
local.setItem("faststats_session_id", "session-1");
|
|
113
|
+
local.setItem("faststats_session_activity", now.toString());
|
|
114
|
+
local.setItem("faststats_session_start", "NaN");
|
|
115
|
+
expect(getSessionStart()).toBe(now);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("session id is shared across tabs via localStorage", () => {
|
|
119
|
+
const first = getOrCreateSessionId();
|
|
120
|
+
setGlobal("sessionStorage", new MockStorage());
|
|
121
|
+
const second = getOrCreateSessionId();
|
|
122
|
+
expect(second).toBe(first);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("migrates legacy sessionStorage keys into localStorage", () => {
|
|
126
|
+
const session = globalThis.sessionStorage as unknown as MockStorage;
|
|
127
|
+
session.setItem("session_id", "legacy-session");
|
|
128
|
+
session.setItem("session_timestamp", Date.now().toString());
|
|
129
|
+
const id = getOrCreateSessionId();
|
|
130
|
+
expect(id).toBe("legacy-session");
|
|
131
|
+
const local = globalThis.localStorage as unknown as MockStorage;
|
|
132
|
+
expect(local.getItem("faststats_session_id")).toBe("legacy-session");
|
|
133
|
+
expect(session.getItem("session_id")).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("createId generates unique values", () => {
|
|
137
|
+
expect(createId()).not.toBe(createId());
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("falls back to ephemeral session when storage access fails", () => {
|
|
141
|
+
Object.defineProperty(globalThis, "localStorage", {
|
|
142
|
+
configurable: true,
|
|
143
|
+
get() {
|
|
144
|
+
throw new Error("storage blocked");
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
Object.defineProperty(globalThis, "sessionStorage", {
|
|
148
|
+
configurable: true,
|
|
149
|
+
get() {
|
|
150
|
+
throw new Error("storage blocked");
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const first = getSessionContext("site_test");
|
|
155
|
+
const second = getSessionContext("site_test");
|
|
156
|
+
|
|
157
|
+
expect(first.sessionId).toBeTruthy();
|
|
158
|
+
expect(second.sessionId).toBe(first.sessionId);
|
|
159
|
+
expect(second.windowId).toBe(first.sessionId);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=`faststats_anon_id`,t=`session_id`,n=`session_timestamp`,r=`session_start`;let i=!1;function a(e){i=e}function o(e){return e??i?``:s()}function s(){if(!localStorage)return``;let t=localStorage.getItem(e);if(t)return t;let n=crypto.randomUUID();return localStorage.setItem(e,n),n}function c(t){return t||!localStorage?``:(localStorage.removeItem(e),s())}function l(){if(!sessionStorage)return``;let e=sessionStorage.getItem(t),i=sessionStorage.getItem(n);if(e&&i){if(Date.now()-Number.parseInt(i,10)<18e5)return sessionStorage.setItem(n,Date.now().toString()),e;p(sessionStorage)}let a=Date.now().toString(),o=crypto.randomUUID();return sessionStorage.setItem(t,o),sessionStorage.setItem(n,a),sessionStorage.setItem(r,a),o}function u(){return sessionStorage?(p(sessionStorage),l()):``}function d(){sessionStorage&&sessionStorage.getItem(t)&&sessionStorage.setItem(n,Date.now().toString())}function f(){if(!sessionStorage)return Date.now();let e=sessionStorage.getItem(r);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(n);return t?Number.parseInt(t,10):Date.now()}function p(e){e.removeItem(t),e.removeItem(n),e.removeItem(r)}export{c as a,d as i,l as n,u as o,f as r,a as s,o as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{n as t,t as n}from"./api-urls-DaeYkG0_.js";import{n as r,r as i,t as a}from"./identifiers-CQeWm7wi.js";import{t as o}from"./send-data-DL_GlsQw.js";import{t as s}from"./types-CYzR5xtT.js";import{EventType as c}from"@rrweb/types";import{record as l}from"rrweb";var u=e({default:()=>p});const d={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},f={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var p=class{endpoint;compressionSupported=typeof window<`u`&&`CompressionStream`in window;sampled;events=[];pending=[];pendingSizeBytes=0;sessionId;started=!1;startTime=0;sequence=0;intervalId=null;flushTask=null;retryTask=null;minLengthFlushTask=null;stopRecording;sending=!1;constructor(e){this.options=e,this.endpoint=`${t(e.baseUrl)}${n.replay}`,this.sampled=Math.random()*100<s(e.samplingPercentage)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??1e4}get maxEvents(){return this.options.maxEvents??500}get maxPendingBatches(){return this.options.maxPendingBatches??30}get maxQueueSizeBytes(){return this.options.maxQueueSizeBytes??2097152}get minReplayLengthMs(){return this.options.minReplayLengthMs??3e3}get shouldCompress(){return this.options.compress??!0}log(...e){this.debug&&console.log(`[Replay]`,...e)}start(){this.started||typeof window>`u`||!this.sampled||(this.started=!0,this.sessionId=r(),this.startTime=i(),this.beginRecording(),this.intervalId=setInterval(this.requestFlush,this.flushInterval),window.addEventListener(`beforeunload`,this.onUnload),window.addEventListener(`pagehide`,this.onUnload),document.addEventListener(`visibilitychange`,this.onVisibilityChange),this.log(`Recording started`))}async beginRecording(){let e=this.options.recordConsole??!0,[{getRecordSequentialIdPlugin:t},n]=await Promise.all([import(`@rrweb/rrweb-plugin-sequential-id-record`),e?import(`@rrweb/rrweb-plugin-console-record`):Promise.resolve(null)]);if(!this.started)return;let r=[t({key:`_faststatsSeqId`})];n&&r.push(n.getRecordConsolePlugin()),this.stopRecording=l({emit:this.onEvent,sampling:this.options.sampling??d,slimDOMOptions:this.options.slimDOMOptions??f,maskAllInputs:this.options.maskAllInputs??!0,maskInputOptions:this.options.maskInputOptions??{password:!0,email:!0,tel:!0},blockClass:this.options.blockClass,blockSelector:this.options.blockSelector,maskTextClass:this.options.maskTextClass,maskTextSelector:this.options.maskTextSelector,checkoutEveryNms:this.options.checkoutEveryNms??6e4,checkoutEveryNth:this.options.checkoutEveryNth,plugins:r})}stop(){if(this.started){if(this.started=!1,this.stopRecording?.(),this.stopRecording=void 0,this.intervalId&&clearInterval(this.intervalId),this.flushTask&&clearTimeout(this.flushTask),this.retryTask&&clearTimeout(this.retryTask),this.minLengthFlushTask&&clearTimeout(this.minLengthFlushTask),this.intervalId=null,this.flushTask=null,this.retryTask=null,this.minLengthFlushTask=null,window.removeEventListener(`beforeunload`,this.onUnload),window.removeEventListener(`pagehide`,this.onUnload),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!this.hasReachedMinLength()){this.events.length=0,this.sessionId=void 0,this.log(`Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush(!0),this.sessionId=void 0,this.log(`Recording stopped`)}}getSessionId(){return this.sessionId??r()}onEvent=(e,t)=>{if(this.events.push(e),t||this.events.length>=this.maxEvents||e.type===c.FullSnapshot&&this.hasReachedMinLength()){this.requestFlush();return}this.scheduleMinLengthFlush()};onUnload=()=>{this.flush(!0)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush(!0)};hasReachedMinLength(){return this.minReplayLengthMs<=0||Date.now()-this.startTime>=this.minReplayLengthMs}requestFlush=()=>{this.flushTask||this.sending||this.events.length===0||(this.minLengthFlushTask&&=(clearTimeout(this.minLengthFlushTask),null),this.flushTask=setTimeout(()=>{this.flushTask=null,this.flush(!1)},0))};scheduleMinLengthFlush(){if(this.minLengthFlushTask||this.events.length===0||this.hasReachedMinLength())return;let e=Math.max(0,this.minReplayLengthMs-(Date.now()-this.startTime)),t=Math.min(e,2147483647);this.minLengthFlushTask=setTimeout(()=>{this.minLengthFlushTask=null,this.requestFlush()},t)}createBatch(e){let t=a(),n=this.sequence++;return{token:this.options.siteKey,sessionId:this.getSessionId(),...t?{identifier:t}:{},batchId:this.createBatchId(n),sequence:n,timestamp:Date.now(),url:window.location.href,events:e}}createBatchId(e){let t=typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;return`${this.getSessionId()??`unknown`}-${e}-${t}`}getBatchSizeBytes(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}dropOldestPendingBatch(e){let t=this.pending.shift();t&&(this.pendingSizeBytes=Math.max(0,this.pendingSizeBytes-this.getBatchSizeBytes(t)),this.log(`${e}, dropping batch ${t.sequence}`))}enqueueBatch(e){let t=this.getBatchSizeBytes(e);if(t>this.maxQueueSizeBytes){this.log(`Replay batch ${e.sequence} is ${t}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`);return}for(;this.pending.length>0&&this.pendingSizeBytes+t>this.maxQueueSizeBytes;)this.dropOldestPendingBatch(`Pending queue size limit reached`);for(;this.pending.length>=this.maxPendingBatches;)this.dropOldestPendingBatch(`Pending batch limit reached`);this.pending.push(e),this.pendingSizeBytes+=t}async encodeBatch(e){let t=JSON.stringify(e);if(!this.shouldCompress||!this.compressionSupported)return{data:t,isCompressed:!1};try{let e=await this.compress(t);return this.log(`Compressed ${t.length}B -> ${e.byteLength}B (${Math.round(e.byteLength/t.length*100)}%)`),{data:e,isCompressed:!0}}catch{return this.log(`Compression failed, using uncompressed`),{data:t,isCompressed:!1}}}async flush(e){if(!this.sending){if(this.events.length>0)if(!this.hasReachedMinLength())this.scheduleMinLengthFlush();else{let e=this.createBatch(this.events.splice(0));this.enqueueBatch(e)}if(this.pending.length!==0){this.sending=!0;try{for(;this.pending.length>0;){let t=this.pending[0];if(!t)break;let n=await this.encodeBatch(t);if(!await this.send(n.data,n.isCompressed,e)){this.log(`Failed to send replay batch ${t.sequence}, retrying`),this.scheduleRetry();break}this.dropOldestPendingBatch(`Sent replay batch`),e=!1}}finally{this.sending=!1}}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async compress(e){let t=new Blob([e]).stream().pipeThrough(new CompressionStream(`gzip`)),n=await new Response(t).arrayBuffer();return new Uint8Array(n)}send(e,t,n){return o({url:t?`${this.endpoint}?encoding=gzip`:this.endpoint,data:e,contentType:t?`application/octet-stream`:`application/json`,debug:!1,useBeacon:!1,keepalive:n})??Promise.resolve(!1)}};export{u as n,p as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
async function e(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!0}=e;if(s&&typeof globalThis.navigator?.sendBeacon==`function`)try{let e=n instanceof Blob?n:typeof Blob<`u`?new Blob([n],{type:r}):n;if(globalThis.navigator.sendBeacon(t,e))return a&&console.log(`${o} Sent via beacon`),!0}catch{}try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:c}),s=e.ok;if(a){let t=s?`${o} Sent via fetch`:`${o} Failed: ${e.status}`;console[s?`log`:`warn`](t)}return s}catch{return a&&console.warn(`${o} Failed to send`),!1}}export{e as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{n as t,t as n}from"./api-urls-DaeYkG0_.js";import{n as r}from"./identifiers-CQeWm7wi.js";import{t as i}from"./types-CYzR5xtT.js";var a=e({default:()=>o}),o=class{endpoint;sampled;metrics=new Map;started=!1;url=``;constructor(e){this.options=e,this.endpoint=`${t(e.baseUrl)}${n.vitals}`,this.sampled=Math.random()*100<i(e.samplingPercentage)}get debug(){return this.options.debug??!1}log(...e){this.debug&&console.log(`[WebVitals]`,...e)}async start(){if(this.started||typeof window>`u`)return;this.started=!0,this.url=window.location.href,document.addEventListener(`visibilitychange`,this.onHidden,{passive:!0}),window.addEventListener(`pagehide`,this.onPageHide,{passive:!0});let e=this.options.attribution?await import(`web-vitals/attribution`):await import(`web-vitals`);this.started&&(e.onCLS(this.capture,{reportAllChanges:!0}),e.onINP(this.capture,{reportAllChanges:!0}),e.onLCP(this.capture,{reportAllChanges:!0}),e.onFCP(this.capture),e.onTTFB(this.capture),this.log(`Tracking started`))}stop(){!this.started||typeof window>`u`||(this.started=!1,this.removeListeners(),this.flush())}trackPageChange(e){if(!this.started||typeof window>`u`)return;let t=e??window.location.href;t!==this.url&&(this.flush(),this.url=t)}onHidden=()=>{document.visibilityState===`hidden`&&this.flush()};onPageHide=()=>{this.flush()};capture=e=>{if(!this.started||!this.sampled)return;let t=e.name,n=e.attribution;this.metrics.set(t,{metric:t,value:e.value,attributes:{id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...n??{}}})};async flush(){if(typeof window>`u`||typeof fetch!=`function`||!this.metrics.size)return;let e=Array.from(this.metrics.values()),t=this.url;this.metrics.clear();try{let n=new AbortController,i=window.setTimeout(()=>n.abort(),3e3),a=await fetch(this.endpoint,{method:`POST`,body:JSON.stringify({sessionId:r(),vitals:e,metadata:{url:t}}),headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.options.siteKey}`},keepalive:!0,signal:n.signal});if(clearTimeout(i),!a.ok)throw Error(`HTTP ${a.status}`)}catch(e){this.log(`Failed to send metrics`,e)}}removeListeners(){document.removeEventListener(`visibilitychange`,this.onHidden),window.removeEventListener(`pagehide`,this.onPageHide)}};export{a as n,o as t};
|