@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.
@@ -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 { getOrCreateSessionId } from "../src/utils/identifiers";
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?: string;
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
- flush: (lowLatency: boolean) => Promise<void>;
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
- (tracker as unknown as { sessionId?: string }).sessionId =
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};
package/wrangler.toml DELETED
@@ -1,8 +0,0 @@
1
- name = "web-analytics"
2
- main = "worker/index.ts"
3
- compatibility_date = "2024-01-01"
4
-
5
- [assets]
6
- directory = "./dist"
7
- binding = "ASSETS"
8
- run_worker_first = true