@faststats/web 0.2.14 → 0.3.1
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/dist/chunks/api-urls-nKmJnyjD.js +1 -0
- package/dist/chunks/identifiers-DrjjHcKB.js +1 -0
- package/dist/chunks/{replay-DvJYurEC.d.ts → replay-DR_T0Iz4.d.ts} +12 -17
- package/dist/chunks/send-data-Cb4xlYF0.js +1 -0
- package/dist/error.d.ts +4 -12
- package/dist/error.js +2 -1
- package/dist/feature-flags.d.ts +19 -2
- package/dist/feature-flags.js +1 -1
- package/dist/index.d.ts +28 -59
- package/dist/index.js +1 -1
- package/dist/replay.d.ts +2 -2
- package/dist/replay.js +1 -1
- package/dist/web-vitals.d.ts +5 -8
- package/dist/web-vitals.js +1 -1
- package/package.json +7 -6
- package/CHANGELOG.md +0 -184
- package/REPLAY_PAYLOAD.md +0 -64
- package/dist/chunks/api-urls-DaeYkG0_.js +0 -1
- package/dist/chunks/error-Cd9PTS5v.js +0 -2
- package/dist/chunks/feature-flags-BClx56v5.d.ts +0 -19
- package/dist/chunks/feature-flags-DSOCIZHK.js +0 -1
- package/dist/chunks/replay-rTqcjOo2.js +0 -1
- package/dist/chunks/rolldown-runtime-MP-BAFHD.js +0 -1
- package/dist/chunks/send-data-B2fYGj6v.js +0 -1
- package/dist/chunks/session-manager-Cy63ptPF.js +0 -1
- package/dist/chunks/types-CYzR5xtT.js +0 -1
- package/dist/chunks/web-vitals-Be-Cg4Po.js +0 -1
- package/scripts/check-bundle-size.mjs +0 -96
- package/src/analytics.ts +0 -787
- package/src/entries/error.ts +0 -6
- package/src/entries/feature-flags.ts +0 -5
- package/src/entries/main.ts +0 -21
- package/src/entries/replay.ts +0 -1
- package/src/entries/web-vitals.ts +0 -1
- package/src/env.d.ts +0 -2
- package/src/error.ts +0 -257
- package/src/feature-flags.ts +0 -84
- package/src/replay.ts +0 -596
- package/src/sdk.ts +0 -8
- package/src/utils/api-urls.ts +0 -24
- package/src/utils/identifiers.ts +0 -47
- package/src/utils/send-data.ts +0 -52
- package/src/utils/session-manager.ts +0 -416
- package/src/utils/types.ts +0 -15
- package/src/web-vitals.ts +0 -159
- package/tests/analytics.test.ts +0 -453
- package/tests/identifiers.test.ts +0 -78
- package/tests/replay.test.ts +0 -582
- package/tests/session-manager.test.ts +0 -161
- package/tests/web-vitals.test.ts +0 -208
- package/tsconfig.json +0 -30
- package/tsdown.config.ts +0 -27
- package/worker/index.ts +0 -22
package/tests/analytics.test.ts
DELETED
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
getInstance,
|
|
4
|
-
resolveReplayTrackerOptions,
|
|
5
|
-
sendData,
|
|
6
|
-
setConsentMode,
|
|
7
|
-
WebAnalytics,
|
|
8
|
-
} from "../src/analytics";
|
|
9
|
-
|
|
10
|
-
class MockStorage {
|
|
11
|
-
private readonly store = new Map<string, string>();
|
|
12
|
-
|
|
13
|
-
getItem(key: string): string | null {
|
|
14
|
-
return this.store.get(key) ?? null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
setItem(key: string, value: string): void {
|
|
18
|
-
this.store.set(key, value);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
removeItem(key: string): void {
|
|
22
|
-
this.store.delete(key);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class MockNode {
|
|
27
|
-
parentNode: MockNode | null = null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
class MockAnchorElement extends MockNode {
|
|
31
|
-
constructor(
|
|
32
|
-
public href: string,
|
|
33
|
-
public host: string,
|
|
34
|
-
) {
|
|
35
|
-
super();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type Listener = EventListenerOrEventListenerObject;
|
|
40
|
-
|
|
41
|
-
class MockEventTarget {
|
|
42
|
-
readonly listeners = new Map<string, Set<Listener>>();
|
|
43
|
-
|
|
44
|
-
addEventListener(type: string, listener: Listener): void {
|
|
45
|
-
const existing = this.listeners.get(type) ?? new Set<Listener>();
|
|
46
|
-
existing.add(listener);
|
|
47
|
-
this.listeners.set(type, existing);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
removeEventListener(type: string, listener: Listener): void {
|
|
51
|
-
this.listeners.get(type)?.delete(listener);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
listenerCount(type: string): number {
|
|
55
|
-
return this.listeners.get(type)?.size ?? 0;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type FetchCall = {
|
|
60
|
-
url: string;
|
|
61
|
-
body?: string;
|
|
62
|
-
headers?: Record<string, string>;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type BrowserHarness = {
|
|
66
|
-
windowTarget: MockEventTarget;
|
|
67
|
-
documentTarget: MockEventTarget;
|
|
68
|
-
fetchCalls: FetchCall[];
|
|
69
|
-
restore(): void;
|
|
70
|
-
location: {
|
|
71
|
-
href: string;
|
|
72
|
-
pathname: string;
|
|
73
|
-
hash: string;
|
|
74
|
-
search: string;
|
|
75
|
-
};
|
|
76
|
-
history: History;
|
|
77
|
-
navigator: {
|
|
78
|
-
sendBeacon: (url: string, data?: BodyInit | null) => boolean;
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
function wait(ms: number): Promise<void> {
|
|
83
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function setGlobal(name: keyof typeof globalThis, value: unknown): void {
|
|
87
|
-
Object.defineProperty(globalThis, name, {
|
|
88
|
-
configurable: true,
|
|
89
|
-
writable: true,
|
|
90
|
-
value,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function setupBrowser(): BrowserHarness {
|
|
95
|
-
const originalValues = {
|
|
96
|
-
window: globalThis.window,
|
|
97
|
-
document: globalThis.document,
|
|
98
|
-
location: globalThis.location,
|
|
99
|
-
history: globalThis.history,
|
|
100
|
-
localStorage: globalThis.localStorage,
|
|
101
|
-
sessionStorage: globalThis.sessionStorage,
|
|
102
|
-
navigator: globalThis.navigator,
|
|
103
|
-
fetch: globalThis.fetch,
|
|
104
|
-
Node: globalThis.Node,
|
|
105
|
-
HTMLAnchorElement: globalThis.HTMLAnchorElement,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const fetchCalls: FetchCall[] = [];
|
|
109
|
-
const windowTarget = new MockEventTarget();
|
|
110
|
-
const documentTarget = new MockEventTarget();
|
|
111
|
-
const localStorage = new MockStorage();
|
|
112
|
-
const sessionStorage = new MockStorage();
|
|
113
|
-
const locationState = {
|
|
114
|
-
href: "https://example.com/",
|
|
115
|
-
pathname: "/",
|
|
116
|
-
hash: "",
|
|
117
|
-
search: "",
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const updateHref = (): void => {
|
|
121
|
-
locationState.href = `https://example.com${locationState.pathname}${locationState.search}${locationState.hash}`;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const history = {
|
|
125
|
-
pushState(_data: unknown, _unused: string, url?: string | URL | null) {
|
|
126
|
-
if (typeof url === "string") {
|
|
127
|
-
const parsed = new URL(url, locationState.href);
|
|
128
|
-
locationState.pathname = parsed.pathname;
|
|
129
|
-
locationState.search = parsed.search;
|
|
130
|
-
locationState.hash = parsed.hash;
|
|
131
|
-
updateHref();
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
replaceState(_data: unknown, _unused: string, url?: string | URL | null) {
|
|
135
|
-
if (typeof url === "string") {
|
|
136
|
-
const parsed = new URL(url, locationState.href);
|
|
137
|
-
locationState.pathname = parsed.pathname;
|
|
138
|
-
locationState.search = parsed.search;
|
|
139
|
-
locationState.hash = parsed.hash;
|
|
140
|
-
updateHref();
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
} as unknown as History;
|
|
144
|
-
|
|
145
|
-
const document = {
|
|
146
|
-
addEventListener: documentTarget.addEventListener.bind(documentTarget),
|
|
147
|
-
removeEventListener:
|
|
148
|
-
documentTarget.removeEventListener.bind(documentTarget),
|
|
149
|
-
referrer: "",
|
|
150
|
-
title: "FastStats",
|
|
151
|
-
visibilityState: "visible",
|
|
152
|
-
documentElement: {
|
|
153
|
-
scrollHeight: 2000,
|
|
154
|
-
scrollTop: 0,
|
|
155
|
-
},
|
|
156
|
-
body: {
|
|
157
|
-
scrollHeight: 2000,
|
|
158
|
-
},
|
|
159
|
-
} as unknown as Document;
|
|
160
|
-
|
|
161
|
-
const windowObject = {
|
|
162
|
-
addEventListener: windowTarget.addEventListener.bind(windowTarget),
|
|
163
|
-
removeEventListener: windowTarget.removeEventListener.bind(windowTarget),
|
|
164
|
-
innerHeight: 1000,
|
|
165
|
-
scrollY: 0,
|
|
166
|
-
setTimeout,
|
|
167
|
-
clearTimeout,
|
|
168
|
-
setInterval,
|
|
169
|
-
clearInterval,
|
|
170
|
-
history,
|
|
171
|
-
location: locationState,
|
|
172
|
-
} as unknown as Window & typeof globalThis;
|
|
173
|
-
|
|
174
|
-
const navigator = {
|
|
175
|
-
sendBeacon: () => false,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
setGlobal("window", windowObject);
|
|
179
|
-
setGlobal("document", document);
|
|
180
|
-
setGlobal("location", locationState as Location);
|
|
181
|
-
setGlobal("history", history);
|
|
182
|
-
setGlobal("localStorage", localStorage);
|
|
183
|
-
setGlobal("sessionStorage", sessionStorage);
|
|
184
|
-
setGlobal("navigator", navigator);
|
|
185
|
-
setGlobal("fetch", (async (
|
|
186
|
-
url: string | URL | Request,
|
|
187
|
-
init?: RequestInit,
|
|
188
|
-
): Promise<Response> => {
|
|
189
|
-
let body: string | undefined;
|
|
190
|
-
if (typeof init?.body === "string") {
|
|
191
|
-
body = init.body;
|
|
192
|
-
} else if (init?.body instanceof Blob) {
|
|
193
|
-
body = await init.body.text();
|
|
194
|
-
}
|
|
195
|
-
fetchCalls.push({
|
|
196
|
-
url: String(url),
|
|
197
|
-
body,
|
|
198
|
-
headers: init?.headers as Record<string, string> | undefined,
|
|
199
|
-
});
|
|
200
|
-
return new Response("", { status: 204 });
|
|
201
|
-
}) as typeof fetch);
|
|
202
|
-
setGlobal("Node", MockNode as unknown as typeof Node);
|
|
203
|
-
setGlobal(
|
|
204
|
-
"HTMLAnchorElement",
|
|
205
|
-
MockAnchorElement as unknown as typeof HTMLAnchorElement,
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
windowTarget,
|
|
210
|
-
documentTarget,
|
|
211
|
-
fetchCalls,
|
|
212
|
-
location: locationState,
|
|
213
|
-
history,
|
|
214
|
-
navigator,
|
|
215
|
-
restore() {
|
|
216
|
-
setGlobal("window", originalValues.window);
|
|
217
|
-
setGlobal("document", originalValues.document);
|
|
218
|
-
setGlobal("location", originalValues.location);
|
|
219
|
-
setGlobal("history", originalValues.history);
|
|
220
|
-
setGlobal("localStorage", originalValues.localStorage);
|
|
221
|
-
setGlobal("sessionStorage", originalValues.sessionStorage);
|
|
222
|
-
setGlobal("navigator", originalValues.navigator);
|
|
223
|
-
setGlobal("fetch", originalValues.fetch);
|
|
224
|
-
setGlobal("Node", originalValues.Node);
|
|
225
|
-
setGlobal("HTMLAnchorElement", originalValues.HTMLAnchorElement);
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let harness: BrowserHarness;
|
|
231
|
-
|
|
232
|
-
beforeEach(() => {
|
|
233
|
-
harness = setupBrowser();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
afterEach(() => {
|
|
237
|
-
getInstance()?.destroy();
|
|
238
|
-
harness.restore();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe("WebAnalytics lifecycle", () => {
|
|
242
|
-
test("start installs listeners and destroy removes them", async () => {
|
|
243
|
-
const analytics = new WebAnalytics({
|
|
244
|
-
siteKey: "site_test",
|
|
245
|
-
autoTrack: false,
|
|
246
|
-
});
|
|
247
|
-
await analytics.start();
|
|
248
|
-
|
|
249
|
-
expect(getInstance()).toBe(analytics);
|
|
250
|
-
expect(harness.windowTarget.listenerCount("pagehide")).toBe(1);
|
|
251
|
-
expect(harness.windowTarget.listenerCount("popstate")).toBe(1);
|
|
252
|
-
expect(harness.documentTarget.listenerCount("visibilitychange")).toBe(1);
|
|
253
|
-
expect(harness.documentTarget.listenerCount("click")).toBe(1);
|
|
254
|
-
|
|
255
|
-
analytics.destroy();
|
|
256
|
-
|
|
257
|
-
expect(getInstance()).toBeNull();
|
|
258
|
-
expect(harness.windowTarget.listenerCount("pagehide")).toBe(0);
|
|
259
|
-
expect(harness.windowTarget.listenerCount("popstate")).toBe(0);
|
|
260
|
-
expect(harness.documentTarget.listenerCount("visibilitychange")).toBe(0);
|
|
261
|
-
expect(harness.documentTarget.listenerCount("click")).toBe(0);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("sendData uses beacon before fetch", async () => {
|
|
265
|
-
harness.navigator.sendBeacon = () => true;
|
|
266
|
-
const sent = await sendData({
|
|
267
|
-
url: "https://example.com/v1/web",
|
|
268
|
-
data: '{"ok":true}',
|
|
269
|
-
});
|
|
270
|
-
expect(sent).toBe(true);
|
|
271
|
-
expect(harness.fetchCalls.length).toBe(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("sendData returns false when fetch is unavailable", async () => {
|
|
275
|
-
harness.navigator.sendBeacon = () => false;
|
|
276
|
-
setGlobal("fetch", undefined as unknown as typeof fetch);
|
|
277
|
-
const sent = await sendData({
|
|
278
|
-
url: "https://example.com/v1/web",
|
|
279
|
-
data: '{"ok":true}',
|
|
280
|
-
});
|
|
281
|
-
expect(sent).toBe(false);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("sendData can skip beacon and use fetch", async () => {
|
|
285
|
-
harness.navigator.sendBeacon = () => true;
|
|
286
|
-
const sent = await sendData({
|
|
287
|
-
url: "https://example.com/v1/web",
|
|
288
|
-
data: '{"ok":true}',
|
|
289
|
-
useBeacon: false,
|
|
290
|
-
});
|
|
291
|
-
expect(sent).toBe(true);
|
|
292
|
-
expect(harness.fetchCalls.length).toBe(1);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("module consent updates apply before instance starts", () => {
|
|
296
|
-
setConsentMode("denied");
|
|
297
|
-
const analytics = new WebAnalytics({
|
|
298
|
-
siteKey: "site_test",
|
|
299
|
-
autoTrack: false,
|
|
300
|
-
consent: { mode: "granted" },
|
|
301
|
-
});
|
|
302
|
-
expect(analytics.getConsentMode()).toBe("denied");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("consent changes unblock identify payloads", async () => {
|
|
306
|
-
const analytics = new WebAnalytics({
|
|
307
|
-
siteKey: "site_test",
|
|
308
|
-
autoTrack: false,
|
|
309
|
-
consent: {
|
|
310
|
-
mode: "pending",
|
|
311
|
-
cookielessWhilePending: true,
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
await analytics.start();
|
|
316
|
-
analytics.identify("user_1", "user@example.com");
|
|
317
|
-
await wait(0);
|
|
318
|
-
expect(
|
|
319
|
-
harness.fetchCalls.some((call) => call.url.endsWith("/v1/identify")),
|
|
320
|
-
).toBe(false);
|
|
321
|
-
|
|
322
|
-
analytics.setConsentMode("granted");
|
|
323
|
-
const identified = await analytics.identify("user_1", "user@example.com", {
|
|
324
|
-
name: "User One",
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const identifyCall = harness.fetchCalls.find((call) =>
|
|
328
|
-
call.url.endsWith("/v1/identify"),
|
|
329
|
-
);
|
|
330
|
-
expect(identified).toBe(true);
|
|
331
|
-
expect(identifyCall).toBeTruthy();
|
|
332
|
-
expect(identifyCall?.body).toContain('"externalId":"user_1"');
|
|
333
|
-
expect(identifyCall?.body).toContain('"email":"user@example.com"');
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("feature flag checks send identifier with external id", async () => {
|
|
337
|
-
const analytics = new WebAnalytics({
|
|
338
|
-
siteKey: "site_test",
|
|
339
|
-
autoTrack: false,
|
|
340
|
-
});
|
|
341
|
-
setGlobal("fetch", (async (
|
|
342
|
-
url: string | URL | Request,
|
|
343
|
-
init?: RequestInit,
|
|
344
|
-
): Promise<Response> => {
|
|
345
|
-
harness.fetchCalls.push({
|
|
346
|
-
url: String(url),
|
|
347
|
-
body: typeof init?.body === "string" ? init.body : undefined,
|
|
348
|
-
headers: init?.headers as Record<string, string> | undefined,
|
|
349
|
-
});
|
|
350
|
-
return Response.json({ value: "true" });
|
|
351
|
-
}) as typeof fetch);
|
|
352
|
-
|
|
353
|
-
await analytics.start();
|
|
354
|
-
await analytics.checkFeatureFlag("beta", undefined, {
|
|
355
|
-
externalId: "user_1",
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
const checkCall = harness.fetchCalls.find((call) =>
|
|
359
|
-
call.url.endsWith("/v1/check"),
|
|
360
|
-
);
|
|
361
|
-
expect(checkCall).toBeTruthy();
|
|
362
|
-
expect(checkCall?.body).toContain('"key":"beta"');
|
|
363
|
-
expect(checkCall?.body).toContain('"externalId":"user_1"');
|
|
364
|
-
expect(checkCall?.body).toContain('"identifier":');
|
|
365
|
-
expect(checkCall?.body).toContain('"sessionId":');
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test("navigation sends page leave and navigation pageview", async () => {
|
|
369
|
-
const analytics = new WebAnalytics({
|
|
370
|
-
siteKey: "site_test",
|
|
371
|
-
autoTrack: false,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
await analytics.start();
|
|
375
|
-
harness.fetchCalls.length = 0;
|
|
376
|
-
|
|
377
|
-
harness.history.pushState({}, "", "/about");
|
|
378
|
-
await wait(350);
|
|
379
|
-
await wait(0);
|
|
380
|
-
|
|
381
|
-
const payloads = harness.fetchCalls.map((call) => call.body ?? "");
|
|
382
|
-
expect(payloads.some((body) => body.includes('"event":"page_leave"'))).toBe(
|
|
383
|
-
true,
|
|
384
|
-
);
|
|
385
|
-
expect(
|
|
386
|
-
payloads.some(
|
|
387
|
-
(body) =>
|
|
388
|
-
body.includes('"event":"pageview"') &&
|
|
389
|
-
body.includes('"trigger":"navigation"') &&
|
|
390
|
-
body.includes('"page":"/about"'),
|
|
391
|
-
),
|
|
392
|
-
).toBe(true);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("visibility hide refreshes stale session before page leave", async () => {
|
|
396
|
-
const analytics = new WebAnalytics({
|
|
397
|
-
siteKey: "site_test",
|
|
398
|
-
autoTrack: false,
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
await analytics.start();
|
|
402
|
-
const storage = globalThis.localStorage as unknown as MockStorage;
|
|
403
|
-
const sessionId = storage.getItem("faststats_session_id");
|
|
404
|
-
expect(sessionId).toBeTruthy();
|
|
405
|
-
storage.setItem(
|
|
406
|
-
"faststats_session_activity",
|
|
407
|
-
(Date.now() - 31 * 60 * 1000).toString(),
|
|
408
|
-
);
|
|
409
|
-
harness.fetchCalls.length = 0;
|
|
410
|
-
|
|
411
|
-
Object.defineProperty(globalThis.document, "visibilityState", {
|
|
412
|
-
configurable: true,
|
|
413
|
-
value: "hidden",
|
|
414
|
-
});
|
|
415
|
-
for (const listener of harness.documentTarget.listeners.get(
|
|
416
|
-
"visibilitychange",
|
|
417
|
-
) ?? []) {
|
|
418
|
-
if (typeof listener === "function") {
|
|
419
|
-
listener(new Event("visibilitychange"));
|
|
420
|
-
} else {
|
|
421
|
-
listener.handleEvent(new Event("visibilitychange"));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
await wait(0);
|
|
425
|
-
|
|
426
|
-
expect(storage.getItem("faststats_session_id")).toBe(sessionId);
|
|
427
|
-
const pageLeaveCall = harness.fetchCalls.find((call) =>
|
|
428
|
-
call.body?.includes('"event":"page_leave"'),
|
|
429
|
-
);
|
|
430
|
-
expect(pageLeaveCall?.body).toContain(`"sessionId":"${sessionId}"`);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test("resolveReplayTrackerOptions honors sessionReplays config", () => {
|
|
434
|
-
const options = resolveReplayTrackerOptions(
|
|
435
|
-
{
|
|
436
|
-
siteKey: "site_test",
|
|
437
|
-
sessionReplays: {
|
|
438
|
-
enabled: true,
|
|
439
|
-
sampling: { percentage: 37 },
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
"https://metrics.example.com",
|
|
443
|
-
true,
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
expect(options).toMatchObject({
|
|
447
|
-
siteKey: "site_test",
|
|
448
|
-
baseUrl: "https://metrics.example.com",
|
|
449
|
-
debug: true,
|
|
450
|
-
samplingPercentage: 37,
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
getAnonymousId,
|
|
4
|
-
getOrCreateAnonymousId,
|
|
5
|
-
resetAnonymousId,
|
|
6
|
-
} from "../src/utils/identifiers";
|
|
7
|
-
import { setCookielessMode } from "../src/utils/session-manager";
|
|
8
|
-
|
|
9
|
-
class MockStorage {
|
|
10
|
-
private readonly data = new Map<string, string>();
|
|
11
|
-
|
|
12
|
-
getItem(key: string): string | null {
|
|
13
|
-
return this.data.get(key) ?? null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
setItem(key: string, value: string): void {
|
|
17
|
-
this.data.set(key, value);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
removeItem(key: string): void {
|
|
21
|
-
this.data.delete(key);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function setGlobal(name: keyof typeof globalThis, value: unknown): void {
|
|
26
|
-
Object.defineProperty(globalThis, name, {
|
|
27
|
-
configurable: true,
|
|
28
|
-
writable: true,
|
|
29
|
-
value,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const original = {
|
|
34
|
-
localStorage: globalThis.localStorage,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
setGlobal("localStorage", new MockStorage());
|
|
39
|
-
setCookielessMode(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
setGlobal("localStorage", original.localStorage);
|
|
44
|
-
setCookielessMode(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("identifiers", () => {
|
|
48
|
-
test("anonymous id is generated and persisted", () => {
|
|
49
|
-
const id = getOrCreateAnonymousId();
|
|
50
|
-
const second = getOrCreateAnonymousId();
|
|
51
|
-
expect(id.length).toBeGreaterThan(0);
|
|
52
|
-
expect(second).toBe(id);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("resetAnonymousId creates a new anonymous id", () => {
|
|
56
|
-
const first = getOrCreateAnonymousId();
|
|
57
|
-
const second = resetAnonymousId();
|
|
58
|
-
expect(second.length).toBeGreaterThan(0);
|
|
59
|
-
expect(second).not.toBe(first);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("cookieless mode skips anonymous id persistence", () => {
|
|
63
|
-
setCookielessMode(true);
|
|
64
|
-
expect(getAnonymousId()).toBe("");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("storage access failures disable persisted anonymous id", () => {
|
|
68
|
-
Object.defineProperty(globalThis, "localStorage", {
|
|
69
|
-
configurable: true,
|
|
70
|
-
get() {
|
|
71
|
-
throw new Error("storage blocked");
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(getOrCreateAnonymousId()).toBe("");
|
|
76
|
-
expect(resetAnonymousId()).toBe("");
|
|
77
|
-
});
|
|
78
|
-
});
|