@faststats/web 0.2.7 → 0.2.9

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 (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/chunks/analytics-BlPwufvF.js +1 -0
  3. package/dist/chunks/api-urls-BrkcoElX.js +1 -0
  4. package/dist/{error-asweBGYd.js → chunks/error-C-fHJ_1D.js} +2 -2
  5. package/dist/chunks/feature-flags-6rZlmhfu.d.ts +18 -0
  6. package/dist/chunks/feature-flags-CqVtrpX2.js +1 -0
  7. package/dist/chunks/replay-CKrEvguu.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 +60 -47
  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/feature-flags.ts +2 -4
  33. package/src/replay.ts +104 -59
  34. package/src/sdk.ts +8 -0
  35. package/src/utils/types.ts +1 -1
  36. package/src/web-vitals.ts +32 -14
  37. package/tests/analytics.test.ts +33 -2
  38. package/tests/replay.test.ts +150 -21
  39. package/tsdown.config.ts +16 -1
  40. package/dist/analytics-Ct-lghlf.js +0 -1
  41. package/dist/module.d.ts +0 -531
  42. package/dist/module.js +0 -1
  43. package/dist/replay-DUA5c3Jf.js +0 -1
  44. package/dist/types-Df70G0eM.js +0 -1
  45. package/dist/web-vitals-gcUR-TX_.js +0 -1
  46. 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
 
@@ -320,19 +320,50 @@ describe("WebAnalytics lifecycle", () => {
320
320
  ).toBe(false);
321
321
 
322
322
  analytics.setConsentMode("granted");
323
- analytics.identify("user_1", "user@example.com", {
323
+ const identified = await analytics.identify("user_1", "user@example.com", {
324
324
  name: "User One",
325
325
  });
326
- await wait(0);
327
326
 
328
327
  const identifyCall = harness.fetchCalls.find((call) =>
329
328
  call.url.endsWith("/v1/identify"),
330
329
  );
330
+ expect(identified).toBe(true);
331
331
  expect(identifyCall).toBeTruthy();
332
332
  expect(identifyCall?.body).toContain('"externalId":"user_1"');
333
333
  expect(identifyCall?.body).toContain('"email":"user@example.com"');
334
334
  });
335
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
+ });
366
+
336
367
  test("navigation sends page leave and navigation pageview", async () => {
337
368
  const analytics = new WebAnalytics({
338
369
  siteKey: "site_test",
@@ -1,7 +1,13 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { gunzipSync } from "node:zlib";
2
3
  import ReplayTracker from "../src/replay";
3
4
  import { getOrCreateSessionId } from "../src/utils/identifiers";
4
5
 
6
+ function decodeBody(data: string | Uint8Array): string {
7
+ if (typeof data === "string") return data;
8
+ return gunzipSync(data).toString("utf8");
9
+ }
10
+
5
11
  class MockStorage {
6
12
  private readonly data = new Map<string, string>();
7
13
 
@@ -37,13 +43,14 @@ type ReplayBatch = {
37
43
  type ReplayTrackerInternals = {
38
44
  events: ReplayEvent[];
39
45
  pending: ReplayBatch[];
46
+ pendingSizeBytes: number;
40
47
  minLengthFlushTask: ReturnType<typeof setTimeout> | null;
41
48
  retryTask: ReturnType<typeof setTimeout> | null;
42
49
  startTime: number;
43
50
  onEvent: (event: ReplayEvent, isCheckout?: boolean) => void;
44
51
  flush: (lowLatency: boolean) => Promise<void>;
45
52
  send: (
46
- data: string | Blob,
53
+ data: string | Uint8Array,
47
54
  isCompressed: boolean,
48
55
  lowLatency: boolean,
49
56
  ) => Promise<boolean>;
@@ -167,13 +174,13 @@ describe("ReplayTracker", () => {
167
174
  minReplayLengthMs: 0,
168
175
  });
169
176
 
170
- let payload: ReplayBatch | null = null;
171
- let lowLatency = false;
177
+ const captured: { payload: ReplayBatch | null; lowLatency: boolean } = {
178
+ payload: null,
179
+ lowLatency: false,
180
+ };
172
181
  tracker.send = async (data, _isCompressed, nextLowLatency) => {
173
- payload = JSON.parse(
174
- typeof data === "string" ? data : await data.text(),
175
- ) as ReplayBatch;
176
- lowLatency = nextLowLatency;
182
+ captured.payload = JSON.parse(decodeBody(data)) as ReplayBatch;
183
+ captured.lowLatency = nextLowLatency;
177
184
  return true;
178
185
  };
179
186
  tracker.startTime = Date.now() - 1000;
@@ -181,11 +188,11 @@ describe("ReplayTracker", () => {
181
188
 
182
189
  await tracker.flush(true);
183
190
 
184
- expect(payload).not.toBeNull();
185
- expect(payload?.token).toBe("site_test");
186
- expect(payload?.sessionId).toBeTruthy();
187
- expect(payload?.events).toHaveLength(1);
188
- 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);
189
196
  expect(tracker.pending.length).toBe(0);
190
197
  });
191
198
 
@@ -196,11 +203,7 @@ describe("ReplayTracker", () => {
196
203
 
197
204
  const payloads: ReplayBatch[] = [];
198
205
  tracker.send = async (data) => {
199
- payloads.push(
200
- JSON.parse(
201
- typeof data === "string" ? data : await data.text(),
202
- ) as ReplayBatch,
203
- );
206
+ payloads.push(JSON.parse(decodeBody(data)) as ReplayBatch);
204
207
  return true;
205
208
  };
206
209
 
@@ -221,14 +224,16 @@ describe("ReplayTracker", () => {
221
224
  expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
222
225
  });
223
226
 
224
- test("sends replay as plain JSON by default", async () => {
227
+ test("sends replay as plain JSON when CompressionStream is unavailable", async () => {
225
228
  const tracker = createTracker({
226
229
  minReplayLengthMs: 0,
227
230
  });
228
231
 
229
- let bodyType: "string" | "blob" | null = null;
232
+ const captured: { bodyType: "string" | "blob" | null } = {
233
+ bodyType: null,
234
+ };
230
235
  tracker.send = async (data) => {
231
- bodyType = typeof data === "string" ? "string" : "blob";
236
+ captured.bodyType = typeof data === "string" ? "string" : "blob";
232
237
  return true;
233
238
  };
234
239
  tracker.startTime = Date.now() - 1000;
@@ -236,7 +241,76 @@ describe("ReplayTracker", () => {
236
241
 
237
242
  await tracker.flush(false);
238
243
 
239
- expect(bodyType).toBe("string");
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);
240
314
  });
241
315
 
242
316
  test("keeps failed batches queued for retry", async () => {
@@ -255,4 +329,59 @@ describe("ReplayTracker", () => {
255
329
 
256
330
  if (tracker.retryTask) clearTimeout(tracker.retryTask);
257
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
+ });
258
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-asweBGYd.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-DUA5c3Jf.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};