@faststats/web 0.2.11 → 0.2.13

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/src/web-vitals.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Metric, MetricWithAttribution } from "web-vitals";
2
- import { webVitalsEventsUrl } from "./utils/api-urls";
2
+ import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
3
3
  import { getOrCreateSessionId } from "./utils/identifiers";
4
4
  import { normalizeSamplingPercentage } from "./utils/types";
5
5
 
@@ -12,64 +12,23 @@ export interface WebVitalsOptions {
12
12
  }
13
13
 
14
14
  type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
15
+ type AnyMetric = Metric | MetricWithAttribution;
15
16
 
16
- type MetricState = {
17
+ type MetricPayload = {
18
+ metric: MetricName;
17
19
  value: number;
18
20
  attributes: Record<string, unknown>;
19
21
  };
20
22
 
21
- type AnyMetric = Metric | MetricWithAttribution;
22
- type ObserverOptions = {
23
- reportAllChanges?: boolean;
24
- };
25
- type Observer = (
26
- cb: (metric: AnyMetric) => void,
27
- opts?: ObserverOptions,
28
- ) => void;
29
- type ObserverTuple = [Observer, Observer, Observer, Observer, Observer];
30
- type ObserverConfig = {
31
- observe: Observer;
32
- options?: ObserverOptions;
33
- };
34
-
35
- async function loadObservers(attribution: boolean): Promise<ObserverConfig[]> {
36
- const { onCLS, onFCP, onINP, onLCP, onTTFB } = attribution
37
- ? await import("web-vitals/attribution")
38
- : await import("web-vitals");
39
- return createObserverConfigs([onCLS, onFCP, onINP, onLCP, onTTFB]);
40
- }
41
-
42
- function createObserverConfigs([
43
- onCLS,
44
- onFCP,
45
- onINP,
46
- onLCP,
47
- onTTFB,
48
- ]: ObserverTuple): ObserverConfig[] {
49
- return [
50
- { observe: onCLS, options: { reportAllChanges: true } },
51
- { observe: onFCP },
52
- { observe: onINP, options: { reportAllChanges: true } },
53
- { observe: onLCP, options: { reportAllChanges: true } },
54
- { observe: onTTFB },
55
- ];
56
- }
57
-
58
23
  export default class WebVitalsTracker {
59
24
  private readonly endpoint: string;
60
- private readonly metricsByUrl = new Map<
61
- string,
62
- Map<MetricName, MetricState>
63
- >();
64
- private readonly pendingFlushUrls = new Set<string>();
65
25
  private readonly sampled: boolean;
26
+ private readonly metrics = new Map<MetricName, MetricPayload>();
66
27
  private started = false;
67
- private flushing = false;
68
- private finalFlushRequested = false;
69
- private currentUrl = "";
28
+ private url = "";
70
29
 
71
30
  constructor(private readonly options: WebVitalsOptions) {
72
- this.endpoint = webVitalsEventsUrl(options.baseUrl);
31
+ this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.vitals}`;
73
32
  this.sampled =
74
33
  Math.random() * 100 <
75
34
  normalizeSamplingPercentage(options.samplingPercentage);
@@ -83,77 +42,68 @@ export default class WebVitalsTracker {
83
42
  if (this.debug) console.log("[WebVitals]", ...args);
84
43
  }
85
44
 
86
- start(): void {
45
+ async start(): Promise<void> {
87
46
  if (this.started || typeof window === "undefined") return;
88
47
 
89
48
  this.started = true;
90
- this.currentUrl = window.location.href;
49
+ this.url = window.location.href;
91
50
 
92
- document.addEventListener("visibilitychange", this.onVisibilityChange, {
51
+ document.addEventListener("visibilitychange", this.onHidden, {
93
52
  passive: true,
94
53
  });
95
54
  window.addEventListener("pagehide", this.onPageHide, { passive: true });
96
55
 
56
+ const mod = this.options.attribution
57
+ ? await import("web-vitals/attribution")
58
+ : await import("web-vitals");
59
+
60
+ if (!this.started) return;
61
+
62
+ mod.onCLS(this.capture, { reportAllChanges: true });
63
+ mod.onINP(this.capture, { reportAllChanges: true });
64
+ mod.onLCP(this.capture, { reportAllChanges: true });
65
+ mod.onFCP(this.capture);
66
+ mod.onTTFB(this.capture);
67
+
97
68
  this.log("Tracking started");
98
- void this.observe();
99
69
  }
100
70
 
101
71
  stop(): void {
102
72
  if (!this.started || typeof window === "undefined") return;
103
73
 
104
74
  this.started = false;
105
- this.cleanupListeners();
106
- this.queueFlush(this.currentUrl, true);
75
+ this.removeListeners();
76
+ void this.flush();
107
77
  }
108
78
 
109
79
  trackPageChange(url?: string): void {
110
80
  if (!this.started || typeof window === "undefined") return;
111
- const nextUrl = url ?? window.location.href;
112
- if (nextUrl === this.currentUrl) return;
113
81
 
114
- const previousUrl = this.currentUrl;
115
- this.currentUrl = nextUrl;
116
- this.queueFlush(previousUrl);
117
- }
82
+ const nextUrl = url ?? window.location.href;
83
+ if (nextUrl === this.url) return;
118
84
 
119
- private cleanupListeners(): void {
120
- document.removeEventListener("visibilitychange", this.onVisibilityChange);
121
- window.removeEventListener("pagehide", this.onPageHide);
85
+ void this.flush();
86
+ this.url = nextUrl;
122
87
  }
123
88
 
124
- private async observe(): Promise<void> {
125
- try {
126
- const observers = await loadObservers(this.options.attribution ?? false);
127
-
128
- if (!this.started) return;
129
-
130
- for (const { observe, options } of observers) {
131
- observe(this.captureMetric, options);
132
- }
133
- } catch (error) {
134
- this.log("Failed to load web-vitals", error);
135
- }
136
- }
137
-
138
- private onVisibilityChange = (): void => {
89
+ private onHidden = (): void => {
139
90
  if (document.visibilityState === "hidden") {
140
- this.queueFlush(this.currentUrl, true);
91
+ void this.flush();
141
92
  }
142
93
  };
143
94
 
144
95
  private onPageHide = (): void => {
145
- this.queueFlush(this.currentUrl, true);
96
+ void this.flush();
146
97
  };
147
98
 
148
- private captureMetric = (metric: AnyMetric): void => {
99
+ private capture = (metric: AnyMetric): void => {
149
100
  if (!this.started || !this.sampled) return;
150
101
 
151
102
  const name = metric.name as MetricName;
152
- const attribution =
153
- (metric as MetricWithAttribution).attribution ?? undefined;
154
- const metrics = this.metricsForUrl(this.currentUrl);
103
+ const attribution = (metric as MetricWithAttribution).attribution;
155
104
 
156
- metrics.set(name, {
105
+ this.metrics.set(name, {
106
+ metric: name,
157
107
  value: metric.value,
158
108
  attributes: {
159
109
  id: metric.id,
@@ -163,99 +113,47 @@ export default class WebVitalsTracker {
163
113
  ...(attribution ?? {}),
164
114
  },
165
115
  });
166
-
167
- this.log(`${name} captured: ${metric.value}`);
168
116
  };
169
117
 
170
- private metricsForUrl(url: string): Map<MetricName, MetricState> {
171
- let metrics = this.metricsByUrl.get(url);
172
- if (!metrics) {
173
- metrics = new Map();
174
- this.metricsByUrl.set(url, metrics);
175
- }
176
- return metrics;
177
- }
178
-
179
- private queueFlush(url: string, final = false): void {
180
- this.finalFlushRequested ||= final;
181
- this.pendingFlushUrls.add(url);
182
- void this.flushPending();
183
- }
184
-
185
- private async flushPending(): Promise<void> {
186
- if (this.flushing) return;
187
- if (typeof window === "undefined" || typeof fetch !== "function") {
188
- this.finishFinalFlushIfNeeded();
189
- return;
190
- }
191
-
192
- this.flushing = true;
193
-
194
- try {
195
- const urls = [...this.pendingFlushUrls];
196
- this.pendingFlushUrls.clear();
197
- for (const url of urls) {
198
- if (!(await this.sendMetricsForUrl(url)))
199
- this.pendingFlushUrls.add(url);
200
- }
201
- } finally {
202
- this.flushing = false;
203
- this.finishFinalFlushIfNeeded();
204
- }
205
- }
206
-
207
- private async sendMetricsForUrl(url: string): Promise<boolean> {
208
- const metrics = this.metricsByUrl.get(url);
209
- if (!metrics || metrics.size === 0) return true;
210
-
211
- const vitals = [...metrics.entries()].map(([metric, data]) => ({
212
- metric,
213
- value: data.value,
214
- attributes: data.attributes,
215
- }));
216
- const body = JSON.stringify({
217
- sessionId: getOrCreateSessionId(),
218
- vitals,
219
- metadata: { url },
220
- });
118
+ private async flush(): Promise<void> {
119
+ if (typeof window === "undefined" || typeof fetch !== "function") return;
120
+ if (!this.metrics.size) return;
221
121
 
222
- this.log(`Sending metrics for ${url} (${vitals.length})`);
122
+ const vitals = Array.from(this.metrics.values());
123
+ const url = this.url;
124
+ this.metrics.clear();
223
125
 
224
126
  try {
225
127
  const controller = new AbortController();
226
- const timeout = window.setTimeout(() => controller.abort(), 3000);
227
-
228
- try {
229
- const response = await fetch(this.endpoint, {
230
- method: "POST",
231
- body,
232
- headers: {
233
- "Content-Type": "application/json",
234
- Authorization: `Bearer ${this.options.siteKey}`,
235
- },
236
- keepalive: true,
237
- signal: controller.signal,
238
- });
239
-
240
- if (!response.ok) {
241
- throw new Error(`HTTP ${response.status}`);
242
- }
243
-
244
- this.metricsByUrl.delete(url);
245
- } finally {
246
- clearTimeout(timeout);
128
+ const timeoutId = window.setTimeout(() => controller.abort(), 3000);
129
+
130
+ const response = await fetch(this.endpoint, {
131
+ method: "POST",
132
+ body: JSON.stringify({
133
+ sessionId: getOrCreateSessionId(),
134
+ vitals,
135
+ metadata: { url },
136
+ }),
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Authorization: `Bearer ${this.options.siteKey}`,
140
+ },
141
+ keepalive: true,
142
+ signal: controller.signal,
143
+ });
144
+
145
+ clearTimeout(timeoutId);
146
+
147
+ if (!response.ok) {
148
+ throw new Error(`HTTP ${response.status}`);
247
149
  }
248
- return true;
249
150
  } catch (error) {
250
151
  this.log("Failed to send metrics", error);
251
- return false;
252
152
  }
253
153
  }
254
154
 
255
- private finishFinalFlushIfNeeded(): void {
256
- if (!this.finalFlushRequested) return;
257
- this.finalFlushRequested = false;
258
- this.started = false;
259
- this.cleanupListeners();
155
+ private removeListeners(): void {
156
+ document.removeEventListener("visibilitychange", this.onHidden);
157
+ window.removeEventListener("pagehide", this.onPageHide);
260
158
  }
261
159
  }
@@ -362,6 +362,7 @@ describe("WebAnalytics lifecycle", () => {
362
362
  expect(checkCall?.body).toContain('"key":"beta"');
363
363
  expect(checkCall?.body).toContain('"externalId":"user_1"');
364
364
  expect(checkCall?.body).toContain('"identifier":');
365
+ expect(checkCall?.body).toContain('"sessionId":');
365
366
  });
366
367
 
367
368
  test("navigation sends page leave and navigation pageview", async () => {
@@ -391,6 +392,41 @@ describe("WebAnalytics lifecycle", () => {
391
392
  ).toBe(true);
392
393
  });
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.sessionStorage as unknown as MockStorage;
403
+ const sessionId = storage.getItem("session_id");
404
+ expect(sessionId).toBeTruthy();
405
+ storage.setItem("session_timestamp", "0");
406
+ harness.fetchCalls.length = 0;
407
+
408
+ Object.defineProperty(globalThis.document, "visibilityState", {
409
+ configurable: true,
410
+ value: "hidden",
411
+ });
412
+ for (const listener of harness.documentTarget.listeners.get(
413
+ "visibilitychange",
414
+ ) ?? []) {
415
+ if (typeof listener === "function") {
416
+ listener(new Event("visibilitychange"));
417
+ } else {
418
+ listener.handleEvent(new Event("visibilitychange"));
419
+ }
420
+ }
421
+ await wait(0);
422
+
423
+ expect(storage.getItem("session_id")).toBe(sessionId);
424
+ const pageLeaveCall = harness.fetchCalls.find((call) =>
425
+ call.body?.includes('"event":"page_leave"'),
426
+ );
427
+ expect(pageLeaveCall?.body).toContain(`"sessionId":"${sessionId}"`);
428
+ });
429
+
394
430
  test("resolveReplayTrackerOptions honors sessionReplays config", () => {
395
431
  const options = resolveReplayTrackerOptions(
396
432
  {
@@ -74,4 +74,22 @@ describe("identifiers", () => {
74
74
  expect(second.length).toBeGreaterThan(0);
75
75
  expect(second).not.toBe(first);
76
76
  });
77
+
78
+ test("storage access failures disable persisted identifiers", () => {
79
+ Object.defineProperty(globalThis, "localStorage", {
80
+ configurable: true,
81
+ get() {
82
+ throw new Error("storage blocked");
83
+ },
84
+ });
85
+ Object.defineProperty(globalThis, "sessionStorage", {
86
+ configurable: true,
87
+ get() {
88
+ throw new Error("storage blocked");
89
+ },
90
+ });
91
+
92
+ expect(getOrCreateAnonymousId()).toBe("");
93
+ expect(getOrCreateSessionId()).toBe("");
94
+ });
77
95
  });
@@ -1 +0,0 @@
1
- const e=`https://metrics.faststats.dev`,t=`https://flags.faststats.dev`;function n(e){return e.replace(/\/+$/,``)}function r(t){return t===void 0||t===``?e:n(t)||e}function i(e){return`${r(e)}/v1/web`}function a(e){return`${r(e)}/v1/identify`}function o(e){return`${r(e)}/v1/replay`}function s(e){return`${r(e)}/v1/vitals`}function c(e){return e===void 0||e===``?t:n(e)||t}function l(e){return`${c(e)}/v1/check`}export{o as a,c as i,a as n,i as o,r,s,l as t};
@@ -1,2 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{o as t}from"./api-urls-BrkcoElX.js";import{n,t as r}from"./identifiers-BN-atT_6.js";import{t as i}from"./send-data-DL_GlsQw.js";var a=e({default:()=>d});const o=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//,s=e=>e?.split(`
2
- `).map(e=>e.trim()).filter(Boolean);function c(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:s(e.stack),cause:c(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function l(e){return e?`${e.error}\0${e.message??``}\0${l(e.cause)}`:``}function u(e){let t=[e.kind,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,l(e.cause)].join(`\0`),n=2166136261,r=3598710387;for(let e=0;e<t.length;e++){let i=t.charCodeAt(e);n=Math.imul(n^i,16777619),r=Math.imul(r^i,2246822519)}return`err_${(n>>>0).toString(16).padStart(8,`0`)}${(r>>>0).toString(16).padStart(8,`0`)}`}var d=class{endpoint;handled=new WeakSet;queue=new Map;timer=null;started=!1;flushing=!1;constructor(e){this.options=e,this.endpoint=t(e.baseUrl)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??5e3}get maxQueueSize(){return this.options.maxQueueSize??50}log(...e){this.debug&&console.log(`[ErrorTracker]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.onError),window.addEventListener(`unhandledrejection`,this.onRejection),document.addEventListener(`visibilitychange`,this.onVisibilityChange),window.addEventListener(`pagehide`,this.requestFlush),this.timer=setInterval(this.requestFlush,this.flushInterval),this.log(`Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.onError),window.removeEventListener(`unhandledrejection`,this.onRejection),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.requestFlush),this.timer&&clearInterval(this.timer),this.timer=null,this.requestFlush(),this.log(`Stopped listening for errors`))}captureError(e){this.record({kind:`error`,message:e.message,stack:e.stack,handled:!0,cause:c(e.cause)},e)}onError=e=>{let t=e.error;this.record({kind:`error`,message:e.message||`Unknown error`,filename:e.filename||void 0,lineno:e.lineno||void 0,stack:t instanceof Error?t.stack:void 0,handled:!1,cause:t instanceof Error?c(t.cause):void 0},t)};onRejection=e=>{let t=e.reason;this.record({kind:`unhandledrejection`,message:t.message||`Unhandled promise rejection`,stack:t.stack||void 0,handled:!1,cause:c(t.cause)},t)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.requestFlush()};requestFlush=()=>{this.flush()};restoreErrors(e){for(let t of e)this.queue.set(t.hash,{...t,count:t.count+(this.queue.get(t.hash)?.count??0)})}record(e,t){if(t instanceof Error){if(this.handled.has(t)){this.log(`Skipping duplicate:`,t.message);return}this.handled.add(t)}if(o.test(`${e.filename??``}\n${e.stack??``}`))return;let n=u(e),r=this.queue.get(n);r?r.count+=1:this.queue.set(n,{hash:n,count:1,error:e.kind===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:s(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}}),this.log(`Captured error:`,e),this.queue.size>=this.maxQueueSize&&this.requestFlush()}async flush(){if(this.flushing||this.queue.size===0)return;let e=[...this.queue.values()];this.queue.clear(),this.flushing=!0;let t=r(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim()?a.buildId:void 0,s=JSON.stringify({token:this.options.siteKey,...t?{userId:t}:{},sessionId:n(),...o?{buildId:o}:{},sdkName:this.options.sdkName??`@faststats/web`,sdkVersion:this.options.sdkVersion??`0.2.11`,data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.log(`Flushing errors:`,e),this.log(`Payload:`,s);try{await i({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})||this.restoreErrors(e)}catch{this.restoreErrors(e)}finally{this.flushing=!1}}};export{a as n,d as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{t}from"./api-urls-BrkcoElX.js";var n=e({fetchFeatureFlagEvaluation:()=>r});async function r(e,n){if(n.projectToken&&n.projectId)throw Error(`provide either projectToken or projectId, not both`);let r=n.projectToken,i=n.projectId,a=n.identifier,o=n.externalId;if(!r&&!i)throw Error(`feature flag check requires projectToken or projectId`);if(!a&&!o)throw Error(`feature flag check requires serverId or externalId`);let s={key:e,...i?{projectId:i}:{},...a?{identifier:a}:{},...o?{externalId:o}:{}};n.attributes&&Object.keys(n.attributes).length>0&&(s.attributes=n.attributes);let c={"Content-Type":`application/json`};r&&(c.Authorization=`Bearer ${r}`);let l=t(n.baseUrl),u=await fetch(l,{method:`POST`,headers:c,body:JSON.stringify(s),credentials:`omit`,signal:n.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()}export{r as n,n as t};
@@ -1 +0,0 @@
1
- let e=!1;function t(t){e=t}function n(t){return t??e?``:r()}function r(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function i(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),r())}function a(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function o(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),a())}function s(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function c(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}export{i as a,s as i,a as n,o,c as r,t as s,n as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{a as t}from"./api-urls-BrkcoElX.js";import{n,r,t as i}from"./identifiers-BN-atT_6.js";import{t as a}from"./send-data-DL_GlsQw.js";import{t as o}from"./types-CYzR5xtT.js";import{EventType as s}from"@rrweb/types";import{record as c}from"rrweb";var l=e({default:()=>f});const u={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},d={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var f=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),this.sampled=Math.random()*100<o(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=n(),this.startTime=r(),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=c({emit:this.onEvent,sampling:this.options.sampling??u,slimDOMOptions:this.options.slimDOMOptions??d,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??n()}onEvent=(e,t)=>{if(this.events.push(e),t||this.events.length>=this.maxEvents||e.type===s.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=i(),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 a({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{l as n,f as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{s as t}from"./api-urls-BrkcoElX.js";import{n}from"./identifiers-BN-atT_6.js";import{t as r}from"./types-CYzR5xtT.js";var i=e({default:()=>s});async function a(e){let{onCLS:t,onFCP:n,onINP:r,onLCP:i,onTTFB:a}=e?await import(`web-vitals/attribution`):await import(`web-vitals`);return o([t,n,r,i,a])}function o([e,t,n,r,i]){return[{observe:e,options:{reportAllChanges:!0}},{observe:t},{observe:n,options:{reportAllChanges:!0}},{observe:r,options:{reportAllChanges:!0}},{observe:i}]}var s=class{endpoint;metricsByUrl=new Map;pendingFlushUrls=new Set;sampled;started=!1;flushing=!1;finalFlushRequested=!1;currentUrl=``;constructor(e){this.options=e,this.endpoint=t(e.baseUrl),this.sampled=Math.random()*100<r(e.samplingPercentage)}get debug(){return this.options.debug??!1}log(...e){this.debug&&console.log(`[WebVitals]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,this.currentUrl=window.location.href,document.addEventListener(`visibilitychange`,this.onVisibilityChange,{passive:!0}),window.addEventListener(`pagehide`,this.onPageHide,{passive:!0}),this.log(`Tracking started`),this.observe())}stop(){!this.started||typeof window>`u`||(this.started=!1,this.cleanupListeners(),this.queueFlush(this.currentUrl,!0))}trackPageChange(e){if(!this.started||typeof window>`u`)return;let t=e??window.location.href;if(t===this.currentUrl)return;let n=this.currentUrl;this.currentUrl=t,this.queueFlush(n)}cleanupListeners(){document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.onPageHide)}async observe(){try{let e=await a(this.options.attribution??!1);if(!this.started)return;for(let{observe:t,options:n}of e)t(this.captureMetric,n)}catch(e){this.log(`Failed to load web-vitals`,e)}}onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.queueFlush(this.currentUrl,!0)};onPageHide=()=>{this.queueFlush(this.currentUrl,!0)};captureMetric=e=>{if(!this.started||!this.sampled)return;let t=e.name,n=e.attribution??void 0;this.metricsForUrl(this.currentUrl).set(t,{value:e.value,attributes:{id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...n??{}}}),this.log(`${t} captured: ${e.value}`)};metricsForUrl(e){let t=this.metricsByUrl.get(e);return t||(t=new Map,this.metricsByUrl.set(e,t)),t}queueFlush(e,t=!1){this.finalFlushRequested||=t,this.pendingFlushUrls.add(e),this.flushPending()}async flushPending(){if(!this.flushing){if(typeof window>`u`||typeof fetch!=`function`){this.finishFinalFlushIfNeeded();return}this.flushing=!0;try{let e=[...this.pendingFlushUrls];this.pendingFlushUrls.clear();for(let t of e)await this.sendMetricsForUrl(t)||this.pendingFlushUrls.add(t)}finally{this.flushing=!1,this.finishFinalFlushIfNeeded()}}}async sendMetricsForUrl(e){let t=this.metricsByUrl.get(e);if(!t||t.size===0)return!0;let r=[...t.entries()].map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes})),i=JSON.stringify({sessionId:n(),vitals:r,metadata:{url:e}});this.log(`Sending metrics for ${e} (${r.length})`);try{let t=new AbortController,n=window.setTimeout(()=>t.abort(),3e3);try{let n=await fetch(this.endpoint,{method:`POST`,body:i,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.options.siteKey}`},keepalive:!0,signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}`);this.metricsByUrl.delete(e)}finally{clearTimeout(n)}return!0}catch(e){return this.log(`Failed to send metrics`,e),!1}}finishFinalFlushIfNeeded(){this.finalFlushRequested&&(this.finalFlushRequested=!1,this.started=!1,this.cleanupListeners())}};export{i as n,s as t};