@faststats/web 0.2.6 → 0.2.8

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/chunks/analytics-Cmawx5f9.js +1 -0
  3. package/dist/chunks/api-urls-BrkcoElX.js +1 -0
  4. package/dist/{error-Df3cDcoP.js → chunks/error-1K0dai1m.js} +2 -2
  5. package/dist/chunks/feature-flags-6rZlmhfu.d.ts +18 -0
  6. package/dist/chunks/feature-flags-BPRTgvIh.js +1 -0
  7. package/dist/chunks/replay-CtMtn0C-.js +1 -0
  8. package/dist/chunks/replay-IhsP2Ab4.d.ts +77 -0
  9. package/dist/chunks/rolldown-runtime-MP-BAFHD.js +1 -0
  10. package/dist/chunks/types-C3vW7XGe.js +1 -0
  11. package/dist/chunks/web-vitals-BooBPWJC.js +1 -0
  12. package/dist/error.d.ts +44 -0
  13. package/dist/error.js +1 -0
  14. package/dist/feature-flags.d.ts +2 -0
  15. package/dist/feature-flags.js +1 -0
  16. package/dist/index.d.ts +140 -0
  17. package/dist/index.js +1 -0
  18. package/dist/replay.d.ts +2 -0
  19. package/dist/replay.js +1 -0
  20. package/dist/web-vitals.d.ts +27 -0
  21. package/dist/web-vitals.js +1 -0
  22. package/package.json +32 -11
  23. package/scripts/check-bundle-size.mjs +90 -9
  24. package/src/analytics.ts +47 -41
  25. package/src/entries/error.ts +6 -0
  26. package/src/entries/feature-flags.ts +5 -0
  27. package/src/entries/main.ts +21 -0
  28. package/src/entries/replay.ts +1 -0
  29. package/src/entries/web-vitals.ts +1 -0
  30. package/src/env.d.ts +2 -0
  31. package/src/error.ts +5 -0
  32. package/src/replay.ts +110 -62
  33. package/src/sdk.ts +8 -0
  34. package/src/utils/types.ts +1 -1
  35. package/src/web-vitals.ts +32 -14
  36. package/tests/replay.test.ts +178 -16
  37. package/tsdown.config.ts +16 -1
  38. package/dist/analytics-D_dgxSVU.js +0 -1
  39. package/dist/module.d.ts +0 -531
  40. package/dist/module.js +0 -1
  41. package/dist/replay-alCILAnP.js +0 -1
  42. package/dist/types-Df70G0eM.js +0 -1
  43. package/dist/web-vitals-gcUR-TX_.js +0 -1
  44. package/src/module.ts +0 -25
package/src/web-vitals.ts CHANGED
@@ -1,11 +1,4 @@
1
- import {
2
- type MetricWithAttribution,
3
- onCLS,
4
- onFCP,
5
- onINP,
6
- onLCP,
7
- onTTFB,
8
- } from "web-vitals/attribution";
1
+ import type { Metric, MetricWithAttribution } from "web-vitals";
9
2
  import { webVitalsEventsUrl } from "./utils/api-urls";
10
3
  import { getOrCreateSessionId } from "./utils/identifiers";
11
4
  import { normalizeSamplingPercentage } from "./utils/types";
@@ -15,6 +8,7 @@ export interface WebVitalsOptions {
15
8
  baseUrl?: string;
16
9
  debug?: boolean;
17
10
  samplingPercentage?: number;
11
+ attribution?: boolean;
18
12
  }
19
13
 
20
14
  type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
@@ -24,7 +18,19 @@ type MetricState = {
24
18
  attributes: Record<string, unknown>;
25
19
  };
26
20
 
27
- const observeVitals = [onCLS, onINP, onLCP, onFCP, onTTFB];
21
+ type AnyMetric = Metric | MetricWithAttribution;
22
+ type Observer = (cb: (metric: AnyMetric) => void) => void;
23
+
24
+ async function loadObservers(attribution: boolean): Promise<Observer[]> {
25
+ if (attribution) {
26
+ const { onCLS, onFCP, onINP, onLCP, onTTFB } = await import(
27
+ "web-vitals/attribution"
28
+ );
29
+ return [onCLS, onFCP, onINP, onLCP, onTTFB] as Observer[];
30
+ }
31
+ const { onCLS, onFCP, onINP, onLCP, onTTFB } = await import("web-vitals");
32
+ return [onCLS, onFCP, onINP, onLCP, onTTFB] as Observer[];
33
+ }
28
34
 
29
35
  export default class WebVitalsTracker {
30
36
  private readonly endpoint: string;
@@ -58,9 +64,7 @@ export default class WebVitalsTracker {
58
64
 
59
65
  this.log("Tracking started");
60
66
 
61
- for (const observe of observeVitals) {
62
- observe(this.captureMetric);
63
- }
67
+ void this.observe();
64
68
  }
65
69
 
66
70
  stop(): void {
@@ -73,14 +77,28 @@ export default class WebVitalsTracker {
73
77
  this.flush();
74
78
  }
75
79
 
80
+ private async observe(): Promise<void> {
81
+ try {
82
+ const observers = await loadObservers(this.options.attribution ?? false);
83
+ if (!this.started) return;
84
+ for (const observe of observers) {
85
+ observe(this.captureMetric);
86
+ }
87
+ } catch (error) {
88
+ this.log("Failed to load web-vitals", error);
89
+ }
90
+ }
91
+
76
92
  private onVisibilityChange = (): void => {
77
93
  if (document.visibilityState === "hidden") this.flush();
78
94
  };
79
95
 
80
- private captureMetric = (metric: MetricWithAttribution): void => {
96
+ private captureMetric = (metric: AnyMetric): void => {
81
97
  if (this.flushed || !this.sampled) return;
82
98
 
83
99
  const name = metric.name as MetricName;
100
+ const attribution =
101
+ (metric as MetricWithAttribution).attribution ?? undefined;
84
102
 
85
103
  this.metrics.set(name, {
86
104
  value: metric.value,
@@ -89,7 +107,7 @@ export default class WebVitalsTracker {
89
107
  rating: metric.rating,
90
108
  delta: metric.delta,
91
109
  navigationType: metric.navigationType,
92
- ...(metric.attribution ?? {}),
110
+ ...(attribution ?? {}),
93
111
  },
94
112
  });
95
113
 
@@ -1,5 +1,12 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { gunzipSync } from "node:zlib";
2
3
  import ReplayTracker from "../src/replay";
4
+ import { getOrCreateSessionId } from "../src/utils/identifiers";
5
+
6
+ function decodeBody(data: string | Uint8Array): string {
7
+ if (typeof data === "string") return data;
8
+ return gunzipSync(data).toString("utf8");
9
+ }
3
10
 
4
11
  class MockStorage {
5
12
  private readonly data = new Map<string, string>();
@@ -36,13 +43,14 @@ type ReplayBatch = {
36
43
  type ReplayTrackerInternals = {
37
44
  events: ReplayEvent[];
38
45
  pending: ReplayBatch[];
46
+ pendingSizeBytes: number;
39
47
  minLengthFlushTask: ReturnType<typeof setTimeout> | null;
40
48
  retryTask: ReturnType<typeof setTimeout> | null;
41
49
  startTime: number;
42
50
  onEvent: (event: ReplayEvent, isCheckout?: boolean) => void;
43
51
  flush: (lowLatency: boolean) => Promise<void>;
44
52
  send: (
45
- data: string | Blob,
53
+ data: string | Uint8Array,
46
54
  isCompressed: boolean,
47
55
  lowLatency: boolean,
48
56
  ) => Promise<boolean>;
@@ -166,13 +174,13 @@ describe("ReplayTracker", () => {
166
174
  minReplayLengthMs: 0,
167
175
  });
168
176
 
169
- let payload: ReplayBatch | null = null;
170
- let lowLatency = false;
177
+ const captured: { payload: ReplayBatch | null; lowLatency: boolean } = {
178
+ payload: null,
179
+ lowLatency: false,
180
+ };
171
181
  tracker.send = async (data, _isCompressed, nextLowLatency) => {
172
- payload = JSON.parse(
173
- typeof data === "string" ? data : await data.text(),
174
- ) as ReplayBatch;
175
- lowLatency = nextLowLatency;
182
+ captured.payload = JSON.parse(decodeBody(data)) as ReplayBatch;
183
+ captured.lowLatency = nextLowLatency;
176
184
  return true;
177
185
  };
178
186
  tracker.startTime = Date.now() - 1000;
@@ -180,30 +188,129 @@ describe("ReplayTracker", () => {
180
188
 
181
189
  await tracker.flush(true);
182
190
 
183
- expect(payload).not.toBeNull();
184
- expect(payload?.token).toBe("site_test");
185
- expect(payload?.sessionId).toBeTruthy();
186
- expect(payload?.events).toHaveLength(1);
187
- expect(lowLatency).toBe(true);
191
+ expect(captured.payload).not.toBeNull();
192
+ expect(captured.payload?.token).toBe("site_test");
193
+ expect(captured.payload?.sessionId).toBeTruthy();
194
+ expect(captured.payload?.events).toHaveLength(1);
195
+ expect(captured.lowLatency).toBe(true);
188
196
  expect(tracker.pending.length).toBe(0);
189
197
  });
190
198
 
191
- test("sends replay as plain JSON by default", async () => {
199
+ test("keeps the same replay session id across later batches", async () => {
192
200
  const tracker = createTracker({
193
201
  minReplayLengthMs: 0,
194
202
  });
195
203
 
196
- let bodyType: "string" | "blob" | null = null;
204
+ const payloads: ReplayBatch[] = [];
197
205
  tracker.send = async (data) => {
198
- bodyType = typeof data === "string" ? "string" : "blob";
206
+ payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
199
207
  return true;
200
208
  };
209
+
210
+ (tracker as unknown as { sessionId?: string }).sessionId =
211
+ getOrCreateSessionId();
201
212
  tracker.startTime = Date.now() - 1000;
202
213
  tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
214
+ await tracker.flush(false);
203
215
 
216
+ const storage = globalThis.sessionStorage as unknown as MockStorage;
217
+ storage.setItem("session_timestamp", "0");
218
+
219
+ tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
204
220
  await tracker.flush(false);
205
221
 
206
- expect(bodyType).toBe("string");
222
+ expect(payloads).toHaveLength(2);
223
+ expect(payloads[0]?.sessionId).toBeTruthy();
224
+ expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
225
+ });
226
+
227
+ test("sends replay as plain JSON when CompressionStream is unavailable", async () => {
228
+ const tracker = createTracker({
229
+ minReplayLengthMs: 0,
230
+ });
231
+
232
+ const captured: { bodyType: "string" | "blob" | null } = {
233
+ bodyType: null,
234
+ };
235
+ tracker.send = async (data) => {
236
+ captured.bodyType = typeof data === "string" ? "string" : "blob";
237
+ return true;
238
+ };
239
+ tracker.startTime = Date.now() - 1000;
240
+ tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
241
+
242
+ await tracker.flush(false);
243
+
244
+ expect(captured.bodyType).toBe("string");
245
+ });
246
+
247
+ test("sends replay as gzip Uint8Array when CompressionStream is available", async () => {
248
+ setGlobal("window", {
249
+ ...(globalThis.window as unknown as Record<string, unknown>),
250
+ CompressionStream: globalThis.CompressionStream,
251
+ });
252
+
253
+ const tracker = createTracker({
254
+ minReplayLengthMs: 0,
255
+ });
256
+
257
+ const captured: {
258
+ bodyType: "string" | "binary" | null;
259
+ compressed: boolean;
260
+ size: number;
261
+ } = { bodyType: null, compressed: false, size: 0 };
262
+ tracker.send = async (data, isCompressed) => {
263
+ captured.bodyType = typeof data === "string" ? "string" : "binary";
264
+ captured.compressed = isCompressed;
265
+ if (data instanceof Uint8Array) {
266
+ captured.size = data.byteLength;
267
+ }
268
+ return true;
269
+ };
270
+ tracker.startTime = Date.now() - 1000;
271
+ for (let i = 0; i < 10; i++) {
272
+ tracker.onEvent(
273
+ {
274
+ type: 2,
275
+ timestamp: Date.now(),
276
+ data: { i, payload: "x".repeat(500) },
277
+ },
278
+ false,
279
+ );
280
+ }
281
+
282
+ await tracker.flush(false);
283
+
284
+ expect(captured.bodyType).toBe("binary");
285
+ expect(captured.compressed).toBe(true);
286
+ expect(captured.size).toBeGreaterThan(0);
287
+ expect(captured.size).toBeLessThan(5000);
288
+ });
289
+
290
+ test("compresses on unload (low latency) flush", async () => {
291
+ setGlobal("window", {
292
+ ...(globalThis.window as unknown as Record<string, unknown>),
293
+ CompressionStream: globalThis.CompressionStream,
294
+ });
295
+
296
+ const tracker = createTracker({
297
+ minReplayLengthMs: 0,
298
+ });
299
+
300
+ let compressedFlag = false;
301
+ let lowLatencyFlag = false;
302
+ tracker.send = async (_data, isCompressed, lowLatency) => {
303
+ compressedFlag = isCompressed;
304
+ lowLatencyFlag = lowLatency;
305
+ return true;
306
+ };
307
+ tracker.startTime = Date.now() - 1000;
308
+ tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
309
+
310
+ await tracker.flush(true);
311
+
312
+ expect(compressedFlag).toBe(true);
313
+ expect(lowLatencyFlag).toBe(true);
207
314
  });
208
315
 
209
316
  test("keeps failed batches queued for retry", async () => {
@@ -222,4 +329,59 @@ describe("ReplayTracker", () => {
222
329
 
223
330
  if (tracker.retryTask) clearTimeout(tracker.retryTask);
224
331
  });
332
+
333
+ test("drops replay batches that exceed the queue byte limit", async () => {
334
+ const tracker = createTracker({
335
+ minReplayLengthMs: 0,
336
+ maxQueueSizeBytes: 600,
337
+ });
338
+
339
+ let sent = 0;
340
+ tracker.send = async () => {
341
+ sent++;
342
+ return true;
343
+ };
344
+ tracker.startTime = Date.now() - 1000;
345
+ tracker.onEvent(
346
+ {
347
+ type: 2,
348
+ timestamp: Date.now(),
349
+ data: { payload: "x".repeat(1000) },
350
+ },
351
+ false,
352
+ );
353
+
354
+ await tracker.flush(false);
355
+
356
+ expect(sent).toBe(0);
357
+ expect(tracker.pending.length).toBe(0);
358
+ expect(tracker.pendingSizeBytes).toBe(0);
359
+ });
360
+
361
+ test("keeps pending replay queue under the byte limit", async () => {
362
+ const tracker = createTracker({
363
+ minReplayLengthMs: 0,
364
+ maxQueueSizeBytes: 900,
365
+ });
366
+
367
+ tracker.send = async () => false;
368
+ tracker.startTime = Date.now() - 1000;
369
+
370
+ for (let i = 0; i < 4; i++) {
371
+ tracker.onEvent(
372
+ {
373
+ type: 2,
374
+ timestamp: Date.now(),
375
+ data: { i, payload: "x".repeat(300) },
376
+ },
377
+ false,
378
+ );
379
+ await tracker.flush(false);
380
+ }
381
+
382
+ expect(tracker.pending.length).toBeGreaterThan(0);
383
+ expect(tracker.pendingSizeBytes).toBeLessThanOrEqual(900);
384
+
385
+ if (tracker.retryTask) clearTimeout(tracker.retryTask);
386
+ });
225
387
  });
package/tsdown.config.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import { defineConfig } from "tsdown";
2
+ import pkg from "./package.json" with { type: "json" };
2
3
 
3
4
  export default defineConfig({
4
- entry: "src/module.ts",
5
+ entry: {
6
+ index: "src/entries/main.ts",
7
+ "feature-flags": "src/entries/feature-flags.ts",
8
+ replay: "src/entries/replay.ts",
9
+ error: "src/entries/error.ts",
10
+ "web-vitals": "src/entries/web-vitals.ts",
11
+ },
5
12
  format: ["esm"],
6
13
  platform: "browser",
7
14
  outDir: "dist",
@@ -9,4 +16,12 @@ export default defineConfig({
9
16
  minify: true,
10
17
  deps: { onlyBundle: false },
11
18
  checks: { pluginTimings: false },
19
+ define: {
20
+ __FASTSTATS_SDK_NAME__: JSON.stringify(pkg.name),
21
+ __FASTSTATS_SDK_VERSION__: JSON.stringify(pkg.version),
22
+ },
23
+ outputOptions: {
24
+ chunkFileNames: "chunks/[name]-[hash].js",
25
+ entryFileNames: "[name].js",
26
+ },
12
27
  });
@@ -1 +0,0 @@
1
- import{a as e,c as t,d as n,f as r,i,l as a,m as o,n as s,o as c,r as l,s as u,t as d,u as f}from"./types-Df70G0eM.js";async function p(e,t){if(t.projectToken&&t.projectId)throw Error(`provide either projectToken or projectId, not both`);if(t.identifier&&t.externalId)throw Error(`provide either serverId or externalId, not both`);let n=t.projectToken,r=t.projectId,i=t.identifier,o=t.externalId;if(!n&&!r)throw Error(`feature flag check requires projectToken or projectId`);if(!i&&!o)throw Error(`feature flag check requires serverId or externalId`);let s={key:e,...r?{projectId:r}:{},...i?{identifier:i}:{externalId:o}};t.attributes&&Object.keys(t.attributes).length>0&&(s.attributes=t.attributes);let c={"Content-Type":`application/json`};n&&(c.Authorization=`Bearer ${n}`);let l=a(t.baseUrl),u=await fetch(l,{method:`POST`,headers:c,body:JSON.stringify(s),credentials:`omit`,signal:t.signal});if(!u.ok){let e=``;try{let t=await u.json();typeof t.error==`string`&&t.error.length>0&&(e=` — ${t.error}`)}catch{}throw Error(`feature flag check failed: HTTP ${u.status}${e}`)}return await u.json()}function m(e){return e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0}function h(e,t,n){if(!(e.sessionReplays?.enabled??e.trackReplay??m(e)))return null;let r=e.replayOptions??{};return{siteKey:e.siteKey,baseUrl:t,debug:n,...r,samplingPercentage:d(r.samplingPercentage??e.sessionReplays?.sampling?.percentage)}}let g=null,_;function v(){return g}function y(e,t){typeof window>`u`||E()||g?.track(e,t??{})}function b(e,t,n){typeof window>`u`||E()||g?.identify(e,t,n??{})}function x(e=!0){typeof window>`u`||E()||g?.logout(e)}function S(e){if(g){g.setConsentMode(e);return}_=e}function C(){S(`granted`)}function w(){S(`denied`)}function T(e){typeof window>`u`||E()||g?.reportError(e)}function E(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function D(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 navigator<`u`&&typeof navigator.sendBeacon==`function`)try{let e=n instanceof Blob?n:typeof Blob<`u`?new Blob([n],{type:r}):n;if(navigator.sendBeacon(t,e))return a&&console.log(`${o} Sent via beacon`),!0}catch{}if(typeof fetch!=`function`)return a&&console.warn(`${o} Failed to send`),!1;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:c}),s=e.ok;return a&&(s?console.log(`${o} Sent via fetch`):console.warn(`${o} Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} Failed to send`),!1}}function O(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function k(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var A=class{webEndpoint;baseUrl;featureFlagsBaseUrl;debug;started=!1;destroyed=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;cleanupCallbacks=[];childTrackers=[];pendingReportedErrors=[];errorTracker=null;handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.leavePage(),this.stopHeartbeat()):this.startHeartbeat()};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(e){this.options=e,this.baseUrl=n(e.baseUrl),this.featureFlagsBaseUrl=r(e.featureFlagsBaseUrl),this.webEndpoint=o(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,_!==void 0&&(this.consentMode=_,_=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(E()){this.log(`disabled`);return}g=this,t(this.isCookielessMode()),setTimeout(()=>void this.start(),0)}}registerCleanup(e){this.cleanupCallbacks.push(e)}addWindowListener(e,t){window.addEventListener(e,t),this.registerCleanup(()=>window.removeEventListener(e,t))}addDocumentListener(e,t){document.addEventListener(e,t),this.registerCleanup(()=>document.removeEventListener(e,t))}patchHistory(){let e=history.pushState.bind(history),t=history.replaceState.bind(history);history.pushState=(t,n,r)=>{e(t,n,r),this.navigate()},history.replaceState=(e,n,r)=>{t(e,n,r),this.navigate()},this.registerCleanup(()=>{history.pushState=e,history.replaceState=t})}ensureStarted(){return typeof window>`u`||this.destroyed||E()?!1:(this.started||this.start(),!0)}stopHeartbeat(){this.heartbeatTimer&&=(clearInterval(this.heartbeatTimer),null)}stopNavigationTimer(){this.navTimer&&=(clearTimeout(this.navTimer),null)}stopChildTrackers(){for(let e of this.childTrackers.splice(0))e.stop?.();this.errorTracker=null}registerChildTracker(e){return!this.started||this.destroyed||g!==this?(e.stop?.(),!1):(this.childTrackers.push(e),!0)}async startErrorTracker(){try{let{default:e}=await import(`./error-Df3cDcoP.js`);if(!this.started||this.destroyed||g!==this)return;let t=new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug});if(t.start(),!this.registerChildTracker(t))return;for(this.errorTracker=t;this.pendingReportedErrors.length>0;){let e=this.pendingReportedErrors.shift();e&&t.captureError(e)}this.log(`error loaded`)}catch(e){this.log(`failed to initialize error tracker: ${String(e)}`)}}async startWebVitalsTracker(){try{let{default:e}=await import(`./web-vitals-gcUR-TX_.js`);if(!this.started||this.destroyed||g!==this)return;let t=new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:d(this.options.webVitals?.sampling?.percentage)});if(t.start(),!this.registerChildTracker(t))return;this.log(`web-vitals loaded`)}catch(e){this.log(`failed to initialize web-vitals tracker: ${String(e)}`)}}async startReplayTracker(e){try{let{default:t}=await import(`./replay-alCILAnP.js`);if(!this.started||this.destroyed||g!==this)return;let n=new t(e);if(n.start(),!this.registerChildTracker(n))return;this.log(`replay loaded`)}catch(e){this.log(`failed to initialize replay tracker: ${String(e)}`)}}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(g&&g!==this){this.log(`already started by another instance`);return}if(E()){this.log(`disabled`);return}this.started=!0,g=this,t(this.isCookielessMode()),l();let e=this.options,n=h(e,this.baseUrl,this.debug);n&&this.startReplayTracker(n),e.trackErrors&&this.startErrorTracker(),e.trackWebVitals&&this.startWebVitalsTracker(),this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),this.addDocumentListener(`visibilitychange`,this.handleVisibilityChange),this.addWindowListener(`pagehide`,this.handlePageHide),this.addWindowListener(`popstate`,this.handlePopState),e.trackHash&&this.addWindowListener(`hashchange`,this.handleHashChange),this.patchHistory()}destroy(){if(!this.destroyed){for(this.started&&typeof window<`u`&&this.leavePage(),this.pendingReportedErrors.length=0,this.stopNavigationTimer(),this.stopHeartbeat(),this.scrollHandler&&typeof window<`u`&&(window.removeEventListener(`scroll`,this.scrollHandler),this.scrollHandler=null);this.cleanupCallbacks.length>0;)this.cleanupCallbacks.pop()?.();this.stopChildTrackers(),g===this&&(g=null),this.started=!1,this.destroyed=!0}}pageview(e={}){if(!this.ensureStarted())return;let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.ensureStarted()&&this.send(e,t)}identify(e,t,n={}){if(!this.ensureStarted()||this.isCookielessMode())return;let r=e.trim(),i=t.trim();!r||!i||D({url:f(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:s(!1),externalId:r,email:i,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){this.ensureStarted()&&(e&&c(this.isCookielessMode()),u())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return s(this.isCookielessMode())}getSessionId(){return l()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||E())return{value:`false`};let r=n?.externalId?.trim();if(r)return p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,externalId:r,attributes:t,signal:n?.signal});let i=this.getAnonymousId();return i?p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,identifier:i,attributes:t,signal:n?.signal}):{value:`false`}}reportError(e){if(this.destroyed||typeof window>`u`||E()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.errorTracker;if(t){t.captureError(e);return}this.pendingReportedErrors.length>=50&&this.pendingReportedErrors.shift(),this.pendingReportedErrors.push(e)}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){if(typeof window>`u`||this.destroyed||E())return;let n=s(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:l(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...k(),...t}});this.log(e),D({url:this.webEndpoint,data:r,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.destroyed||this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-i()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){this.stopHeartbeat();return}e()},300*1e3)}navigate(){!this.started||this.destroyed||(this.stopNavigationTimer(),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300))}links(){let e=e=>{let t=O(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{x as a,T as c,y as d,p as f,E as i,D as l,v as n,C as o,b as r,w as s,A as t,S as u};