@faststats/web 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/replay.js CHANGED
@@ -1 +1,506 @@
1
- import{a as e,r as t,t as n}from"./chunks/api-urls-Bh77rElT.js";import{a as r,c as i,i as a,n as o,r as s,t as c}from"./chunks/send-data-CIbuTxPh.js";import{t as l}from"./chunks/identifiers-DcGHdsnG.js";import{record as u}from"@rrweb/record";const d=new TextEncoder,f=new WeakMap;var p=class{options;endpoint;compressionSupported=typeof window<`u`&&`CompressionStream`in window;flushInterval;maxEvents;maxBatchSizeBytes;maxLowLatencyBatchSizeBytes;maxPendingBatches;maxQueueSizeBytes;minReplayLengthMs;events=[];pending=[];queuedEventsSizeBytes=0;pendingSizeBytes=0;viewId=o();chunkStartedAt=0;started=!1;startGeneration=0;startTime=0;sequence=0;intervalId=null;flushTask=null;retryTask=null;minLengthFlushTask=null;stopRecording;sending=!1;flushQueued=!1;flushQueuedLowLatency=!1;unsubscribeRotation;lastCookielessMode=a();constructor(r){this.options=r,this.endpoint=`${e(r.baseUrl,n)}${t.replay}`,this.flushInterval=r.flushInterval??5e3,this.maxEvents=r.maxEvents??1e3,this.maxBatchSizeBytes=r.maxBatchSizeBytes??524288,this.maxLowLatencyBatchSizeBytes=r.maxLowLatencyBatchSizeBytes??61440,this.maxPendingBatches=r.maxPendingBatches??30,this.maxQueueSizeBytes=r.maxQueueSizeBytes??2097152,this.minReplayLengthMs=r.minReplayLengthMs??3e3}log(...e){this.options.debug&&console.log(`[Replay]`,...e)}clearFlushTimers(){this.flushTask&&clearTimeout(this.flushTask),this.retryTask&&clearTimeout(this.retryTask),this.minLengthFlushTask&&clearTimeout(this.minLengthFlushTask),this.flushTask=null,this.retryTask=null,this.minLengthFlushTask=null}clearQueues(){this.events.length=0,this.queuedEventsSizeBytes=0,this.pending.length=0,this.pendingSizeBytes=0}storage(){try{return globalThis.sessionStorage}catch{return null}}counterKey(e){return`faststats_replay_batch_sequence_${this.options.siteKey}_${e.windowId}`}readCounter(e){let t=this.storage()?.getItem(this.counterKey(e));if(!t)return this.sequence;let n=Number.parseInt(t,10);return Number.isFinite(n)&&n>=0?n:this.sequence}writeCounter(e,t){this.sequence=t;try{this.storage()?.setItem(this.counterKey(e),String(t))}catch{}}nextBatchSequence(e){let t=this.readCounter(e);return this.writeCounter(e,t+1),t}setCookielessMode(e){this.lastCookielessMode!==e&&(this.lastCookielessMode=e,e&&(this.clearQueues(),this.clearFlushTimers()),this.startTime=s(this.options.siteKey).sessionStart,this.chunkStartedAt=Date.now())}start(){if(this.started||typeof window>`u`)return;this.started=!0,this.viewId=o(),this.startTime=s(this.options.siteKey).sessionStart,this.chunkStartedAt=Date.now(),this.unsubscribeRotation=r(e=>{this.onSessionRotated(e)});let e=++this.startGeneration;this.beginRecording(e),this.intervalId=setInterval(()=>this.requestFlush(`interval`),this.flushInterval),window.addEventListener(`beforeunload`,this.onUnload),this.log(`Recording started`)}trackPageChange(e){this.started&&(this.viewId=o(),this.chunkStartedAt=Date.now(),this.onEvent({type:5,timestamp:Date.now(),data:{tag:`faststats:view`,payload:{href:e??window.location.href,viewId:this.viewId}}},!1))}onPageHidden(e=!1){e||this.flush(!0,!0,`pageHidden`)}onPageShow(e=!1){!e||!this.started||this.pending.length>0&&this.flush(!1,!1,`pageShow`)}async beginRecording(e){let[t,n]=await Promise.all([this.options.recordConsole===!1?Promise.resolve(null):import(`@rrweb/rrweb-plugin-console-record`),import(`@rrweb/rrweb-plugin-sequential-id-record`)]);if(e!==this.startGeneration)return;let r=[n.getRecordSequentialIdPlugin({key:`_faststatsSeqId`})];t&&r.push(t.getRecordConsolePlugin({level:this.options.consoleLevel??[`error`,`warn`,`info`,`log`],lengthThreshold:this.options.consoleLengthThreshold??200,stringifyOptions:{numOfKeysLimit:25,depthOfLimit:3,stringLengthLimit:this.options.consoleStringLengthLimit??500}}));let i=u({emit:this.onEvent,sampling:this.options.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},slimDOMOptions:this.options.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},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});if(e!==this.startGeneration){i?.();return}this.stopRecording=i}stop(){this.started&&(this.started=!1,this.startGeneration++,this.unsubscribeRotation?.(),this.unsubscribeRotation=void 0,this.stopRecording?.(),this.stopRecording=void 0,this.intervalId&&clearInterval(this.intervalId),this.clearFlushTimers(),this.intervalId=null,window.removeEventListener(`beforeunload`,this.onUnload),this.hasReachedMinLength()||(this.events.length=0,this.queuedEventsSizeBytes=0,this.log(`Session too short (${Date.now()-this.startTime}ms), discarding events`)),this.flush(!0,!0,`stop`),this.log(`Recording stopped`))}onSessionRotated(e){this.started&&(this.enqueueEventsIfReady(e,!0,`sessionRotate`),this.pending.length>0&&queueMicrotask(()=>void this.flush(!1)))}onEvent=(e,t)=>{if(this.events.push(e),this.queuedEventsSizeBytes+=this.eventSizeBytes(e),t){this.requestFlush(`checkout`);return}if(this.events.length>=this.maxEvents){this.requestFlush(`maxEvents`);return}if(this.queuedEventsSizeBytes>=this.maxBatchSizeBytes){this.requestFlush(`maxBytes`);return}if(e.type===2&&this.hasReachedMinLength()){this.requestFlush(`fullSnapshot`);return}this.scheduleMinLengthFlush()};onUnload=()=>{this.flush(!0,!0,`unload`)};hasReachedMinLength(){return this.minReplayLengthMs<=0||Date.now()-this.startTime>=this.minReplayLengthMs}requestFlush=(e=`manual`)=>{this.flushTask||this.events.length===0||(this.minLengthFlushTask&&=(clearTimeout(this.minLengthFlushTask),null),this.flushTask=setTimeout(()=>{this.flushTask=null,this.flush(!1,!1,e)},0))};scheduleMinLengthFlush(){if(this.minLengthFlushTask||this.events.length===0||this.hasReachedMinLength())return;let e=Math.max(0,this.minReplayLengthMs-(Date.now()-this.startTime));this.minLengthFlushTask=setTimeout(()=>{this.minLengthFlushTask=null,this.requestFlush(`minLength`)},Math.min(e,2147483647))}enqueueEventsIfReady(e,t,n=`manual`,r=this.maxBatchSizeBytes){if(this.events.length===0)return;if(!t&&!this.hasReachedMinLength()){this.scheduleMinLengthFlush();return}let i=this.takeEventBatches(r);for(let r=0;r<i.length;r+=1)this.enqueueBatch(this.createBatch(i[r]??[],e,t&&r===i.length-1,n));this.queuedEventsSizeBytes=0}takeEventBatches(e){let t=[],n=[],r=0;for(let i of this.events.splice(0)){let a=this.eventSizeBytes(i);n.length>0&&r+a>e&&(t.push(n),n=[],r=0),n.push(i),r+=a}return n.length>0&&t.push(n),t}createBatch(e,t,n,r=`manual`){let i=l(),a=this.nextBatchSequence(t),o=this.chunkStartedAt,s=Date.now();return this.chunkStartedAt=s,{token:this.options.siteKey,sessionId:t.sessionId,windowId:t.windowId,viewId:this.viewId,sessionStart:t.sessionStart,chunkStartedAt:o,chunkEndedAt:s,...i?{identifier:i}:{},batchId:`${t.sessionId}-${a}-${o}`,sequence:a,timestamp:s,url:window.location.href,...n?{isFinal:!0}:{},flushReason:r,events:e}}eventSizeBytes(e){return d.encode(JSON.stringify(e)).byteLength+1}batchSizeBytes(e){let t=f.get(e);if(t!==void 0)return t;let n=d.encode(JSON.stringify(e)).byteLength;return f.set(e,n),n}dropOldestPending(e){let t=this.pending.shift();t&&(this.pendingSizeBytes=Math.max(0,this.pendingSizeBytes-this.batchSizeBytes(t)),this.log(`${e}, dropping batch ${t.sequence}`))}enqueueBatch(e){let t=this.batchSizeBytes(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>=this.maxPendingBatches||this.pendingSizeBytes+t>this.maxQueueSizeBytes)&&this.pending.length>0;)this.dropOldestPending(`Pending queue limit reached`);this.pending.push(e),this.pendingSizeBytes+=t}async flush(e,t,n=`manual`){let r=i(this.options.siteKey);if(r&&this.enqueueEventsIfReady(r.prev,!0,`sessionRotate`),this.enqueueEventsIfReady(s(this.options.siteKey),t,n,e?this.maxLowLatencyBatchSizeBytes:this.maxBatchSizeBytes),this.pending.length!==0){if(this.sending){this.flushQueued=!0,this.flushQueuedLowLatency||=e;return}this.sending=!0;try{for(;this.pending.length>0;){let t=this.pending[0];if(!t)break;if(!await this.sendBatch(t,e)){this.log(`Failed to send replay batch ${t.sequence}, retrying`),this.scheduleRetry();break}this.dropOldestPending(`Sent replay batch`)}}finally{if(this.sending=!1,this.flushQueued){let e=this.flushQueuedLowLatency;this.flushQueued=!1,this.flushQueuedLowLatency=!1,this.flush(e)}}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async sendBatch(e,t){let n=JSON.stringify(e),r={useBeacon:t,keepalive:t};if(!t&&(this.options.compress??!0)&&this.compressionSupported)try{let e=new Blob([n]).stream().pipeThrough(new CompressionStream(`gzip`)),t=new Uint8Array(await new Response(e).arrayBuffer());return this.log(`Compressed ${n.length}B -> ${t.byteLength}B (${Math.round(t.byteLength/n.length*100)}%)`),c({url:`${this.endpoint}?encoding=gzip`,data:t,contentType:`application/octet-stream`,...r})}catch{this.log(`Compression failed, using uncompressed`)}return c({url:this.endpoint,data:n,contentType:`application/json`,...r})}};export{p as default};
1
+ import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-CaNa8upY.js";
2
+ import { a as onSessionRotated, c as touchActivity, d as setStorageItem, i as isCookielessMode, l as getStorageItem, n as createId, r as getSessionContext, t as sendData } from "./chunks/send-data-DjdbGDDc.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-cUymVK5H.js";
4
+ import { record } from "@rrweb/record";
5
+ //#region src/replay.ts
6
+ const MAX_TIMEOUT_MS = 2147483647;
7
+ const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
8
+ const DEFAULT_MAX_BATCH_SIZE_BYTES = 512 * 1024;
9
+ const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 60 * 1024;
10
+ const RETRY_BASE_DELAY_MS = 2e3;
11
+ const RETRY_MAX_ATTEMPTS = 5;
12
+ const RETRY_COOLDOWN_MS = 5 * 6e4;
13
+ const VIEW_META_EVENT_TYPE = 5;
14
+ const RRWEB_EVENT_FULL_SNAPSHOT = 2;
15
+ const RRWEB_EVENT_META = 4;
16
+ const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
17
+ const textEncoder = new TextEncoder();
18
+ const batchSizeCache = /* @__PURE__ */ new WeakMap();
19
+ var ReplayTracker = class {
20
+ options;
21
+ endpoint;
22
+ compressionSupported = typeof window !== "undefined" && "CompressionStream" in window;
23
+ flushInterval;
24
+ maxEvents;
25
+ maxBatchSizeBytes;
26
+ maxLowLatencyBatchSizeBytes;
27
+ maxPendingBatches;
28
+ maxQueueSizeBytes;
29
+ minReplayLengthMs;
30
+ events = [];
31
+ pending = [];
32
+ minLengthBlockedBatches = /* @__PURE__ */ new WeakSet();
33
+ queuedEventsSizeBytes = 0;
34
+ pendingSizeBytes = 0;
35
+ viewId = createId();
36
+ chunkStartedAt = 0;
37
+ started = false;
38
+ disposed = false;
39
+ startGeneration = 0;
40
+ startTime = 0;
41
+ sequence = 0;
42
+ intervalId = null;
43
+ flushTask = null;
44
+ retryTask = null;
45
+ retryAttempt = 0;
46
+ minLengthFlushTask = null;
47
+ stopRecording;
48
+ sending = false;
49
+ inFlightBatch = null;
50
+ queuedFlush = null;
51
+ unsubscribeRotation;
52
+ lastCookielessMode = isCookielessMode();
53
+ constructor(options) {
54
+ this.options = options;
55
+ this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
56
+ this.flushInterval = options.flushInterval ?? 5e3;
57
+ this.maxEvents = options.maxEvents ?? 1e3;
58
+ this.maxBatchSizeBytes = options.maxBatchSizeBytes ?? DEFAULT_MAX_BATCH_SIZE_BYTES;
59
+ this.maxLowLatencyBatchSizeBytes = options.maxLowLatencyBatchSizeBytes ?? DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
60
+ this.maxPendingBatches = options.maxPendingBatches ?? 30;
61
+ this.maxQueueSizeBytes = options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
62
+ this.minReplayLengthMs = options.minReplayLengthMs ?? 3e3;
63
+ }
64
+ log(...args) {
65
+ if (this.options.debug) console.log("[Replay]", ...args);
66
+ }
67
+ clearFlushTimers() {
68
+ if (this.flushTask) clearTimeout(this.flushTask);
69
+ if (this.retryTask) clearTimeout(this.retryTask);
70
+ if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
71
+ this.flushTask = null;
72
+ this.retryTask = null;
73
+ this.retryAttempt = 0;
74
+ this.minLengthFlushTask = null;
75
+ }
76
+ clearQueues() {
77
+ this.events.length = 0;
78
+ this.queuedEventsSizeBytes = 0;
79
+ this.pending.length = 0;
80
+ this.pendingSizeBytes = 0;
81
+ }
82
+ dropMinLengthBlockedBatches() {
83
+ for (let index = this.pending.length - 1; index >= 0; index--) {
84
+ const batch = this.pending[index];
85
+ if (batch && this.minLengthBlockedBatches.has(batch)) this.removePending(batch);
86
+ }
87
+ }
88
+ unblockSessionBatches(sessionId) {
89
+ for (const batch of this.pending) if (batch.sessionId === sessionId) this.minLengthBlockedBatches.delete(batch);
90
+ }
91
+ finalizeSessionPending(context, flushReason) {
92
+ const batch = this.pending.findLast((candidate) => candidate.sessionId === context.sessionId);
93
+ if (!batch || batch.isFinal) return;
94
+ if (batch === this.inFlightBatch) {
95
+ this.enqueueBatch(this.createBatch([], context, true, flushReason));
96
+ return;
97
+ }
98
+ const previousSize = this.batchSizeBytes(batch);
99
+ batch.isFinal = true;
100
+ batch.flushReason = flushReason;
101
+ batchSizeCache.delete(batch);
102
+ this.pendingSizeBytes += this.batchSizeBytes(batch) - previousSize;
103
+ }
104
+ nextBatchSequence(context) {
105
+ const key = `faststats_replay_batch_sequence_${this.options.siteKey}_${context.windowId}`;
106
+ let sequence = this.sequence;
107
+ const parsed = Number.parseInt(getStorageItem("sessionStorage", key) ?? "", 10);
108
+ if (Number.isFinite(parsed) && parsed >= 0) sequence = parsed;
109
+ setStorageItem("sessionStorage", key, String(sequence + 1));
110
+ this.sequence = sequence + 1;
111
+ return sequence;
112
+ }
113
+ setCookielessMode(cookieless) {
114
+ if (this.lastCookielessMode === cookieless) return;
115
+ this.lastCookielessMode = cookieless;
116
+ if (cookieless) {
117
+ this.clearQueues();
118
+ this.clearFlushTimers();
119
+ }
120
+ this.startTime = getSessionContext(this.options.siteKey).sessionStart;
121
+ this.chunkStartedAt = Date.now();
122
+ }
123
+ start() {
124
+ if (this.started || typeof window === "undefined") return;
125
+ this.started = true;
126
+ this.disposed = false;
127
+ this.viewId = createId();
128
+ this.startTime = getSessionContext(this.options.siteKey).sessionStart;
129
+ this.chunkStartedAt = Date.now();
130
+ this.unsubscribeRotation = onSessionRotated((prev, next) => {
131
+ this.onSessionRotated(prev, next);
132
+ });
133
+ const generation = ++this.startGeneration;
134
+ this.beginRecording(generation);
135
+ this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
136
+ window.addEventListener("beforeunload", this.onUnload);
137
+ this.log("Recording started");
138
+ }
139
+ trackPageChange(url) {
140
+ if (!this.started) return;
141
+ this.enqueueEventsIfReady(getSessionContext(this.options.siteKey), {
142
+ flushReason: "navigation",
143
+ sealBeforeMinLength: true
144
+ });
145
+ this.viewId = createId();
146
+ this.chunkStartedAt = Date.now();
147
+ const href = sanitizeUrl(url ?? window.location.href);
148
+ this.onEvent({
149
+ type: VIEW_META_EVENT_TYPE,
150
+ timestamp: Date.now(),
151
+ data: {
152
+ tag: "faststats:view",
153
+ payload: {
154
+ href,
155
+ viewId: this.viewId
156
+ }
157
+ }
158
+ }, false);
159
+ if (this.pending.length > 0) queueMicrotask(() => void this.flush(false));
160
+ }
161
+ onPageHidden(persisted = false) {
162
+ if (persisted) return;
163
+ this.flush(true, true, "pageHidden");
164
+ }
165
+ onPageShow(persisted = false) {
166
+ if (!persisted || !this.started) return;
167
+ if (this.pending.length > 0) this.flush(false, false, "pageShow");
168
+ }
169
+ async beginRecording(generation) {
170
+ const [consolePluginModule, sequentialIdPluginModule] = await Promise.all([this.options.recordConsole === true ? import("@rrweb/rrweb-plugin-console-record") : Promise.resolve(null), import("@rrweb/rrweb-plugin-sequential-id-record")]);
171
+ if (generation !== this.startGeneration) return;
172
+ const plugins = [sequentialIdPluginModule.getRecordSequentialIdPlugin({ key: REPLAY_SEQUENTIAL_ID_KEY })];
173
+ if (consolePluginModule) plugins.push(consolePluginModule.getRecordConsolePlugin({
174
+ level: this.options.consoleLevel ?? [
175
+ "error",
176
+ "warn",
177
+ "info",
178
+ "log"
179
+ ],
180
+ lengthThreshold: this.options.consoleLengthThreshold ?? 200,
181
+ stringifyOptions: {
182
+ numOfKeysLimit: 25,
183
+ depthOfLimit: 3,
184
+ stringLengthLimit: this.options.consoleStringLengthLimit ?? 500
185
+ }
186
+ }));
187
+ const stop = record({
188
+ emit: this.onEvent,
189
+ sampling: this.options.sampling ?? {
190
+ mousemove: 50,
191
+ mouseInteraction: true,
192
+ scroll: 150,
193
+ media: 800,
194
+ input: "last"
195
+ },
196
+ slimDOMOptions: this.options.slimDOMOptions ?? {
197
+ script: true,
198
+ comment: true,
199
+ headFavicon: true,
200
+ headWhitespace: true,
201
+ headMetaDescKeywords: true,
202
+ headMetaSocial: true,
203
+ headMetaRobots: true,
204
+ headMetaHttpEquiv: true,
205
+ headMetaAuthorship: true
206
+ },
207
+ maskAllInputs: this.options.maskAllInputs ?? true,
208
+ maskInputOptions: this.options.maskInputOptions ?? {
209
+ password: true,
210
+ email: true,
211
+ tel: true
212
+ },
213
+ blockClass: this.options.blockClass,
214
+ blockSelector: this.options.blockSelector,
215
+ maskTextClass: this.options.maskTextClass,
216
+ maskTextSelector: this.options.maskTextSelector,
217
+ checkoutEveryNms: this.options.checkoutEveryNms ?? 6e4,
218
+ checkoutEveryNth: this.options.checkoutEveryNth,
219
+ plugins
220
+ });
221
+ if (generation !== this.startGeneration) {
222
+ stop?.();
223
+ return;
224
+ }
225
+ this.stopRecording = stop;
226
+ }
227
+ stop() {
228
+ if (!this.started) return;
229
+ this.started = false;
230
+ this.disposed = true;
231
+ this.startGeneration++;
232
+ this.unsubscribeRotation?.();
233
+ this.unsubscribeRotation = void 0;
234
+ this.stopRecording?.();
235
+ this.stopRecording = void 0;
236
+ if (this.intervalId) clearInterval(this.intervalId);
237
+ this.clearFlushTimers();
238
+ this.intervalId = null;
239
+ window.removeEventListener("beforeunload", this.onUnload);
240
+ if (!this.hasReachedMinLength()) {
241
+ this.events.length = 0;
242
+ this.queuedEventsSizeBytes = 0;
243
+ this.dropMinLengthBlockedBatches();
244
+ this.log(`Session too short (${Date.now() - this.startTime}ms), discarding events`);
245
+ }
246
+ this.flush(true, true, "stop");
247
+ this.log("Recording stopped");
248
+ }
249
+ onSessionRotated(prev, next) {
250
+ if (!this.started) return;
251
+ this.applySessionRotation(prev, next);
252
+ if (this.pending.length > 0) queueMicrotask(() => void this.flush(false, false, "sessionRotate"));
253
+ }
254
+ applySessionRotation(prev, next) {
255
+ this.enqueueEventsIfReady(prev, {
256
+ isFinal: true,
257
+ flushReason: "sessionRotate"
258
+ });
259
+ this.finalizeSessionPending(prev, "sessionRotate");
260
+ this.unblockSessionBatches(prev.sessionId);
261
+ this.resetSessionTiming(next);
262
+ }
263
+ resetSessionTiming(next) {
264
+ if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
265
+ this.minLengthFlushTask = null;
266
+ this.startTime = next.sessionStart;
267
+ this.chunkStartedAt = Date.now();
268
+ }
269
+ onEvent = (event, isCheckout) => {
270
+ let capturedEvent = event;
271
+ if (event.type === RRWEB_EVENT_META) {
272
+ const data = event.data;
273
+ if (typeof data.href === "string") capturedEvent = {
274
+ ...event,
275
+ data: {
276
+ ...data,
277
+ href: sanitizeUrl(data.href)
278
+ }
279
+ };
280
+ }
281
+ this.events.push(capturedEvent);
282
+ this.queuedEventsSizeBytes += this.eventSizeBytes(capturedEvent);
283
+ let reason;
284
+ if (isCheckout) reason = "checkout";
285
+ else if (this.events.length >= this.maxEvents) reason = "maxEvents";
286
+ else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
287
+ else if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT && this.hasReachedMinLength()) reason = "fullSnapshot";
288
+ if (reason) this.requestFlush(reason);
289
+ else this.scheduleMinLengthFlush();
290
+ };
291
+ onUnload = () => {
292
+ this.flush(true, true, "unload");
293
+ };
294
+ hasReachedMinLength() {
295
+ return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
296
+ }
297
+ requestFlush = (reason = "manual") => {
298
+ if (this.flushTask || this.events.length === 0) return;
299
+ if (this.minLengthFlushTask) {
300
+ clearTimeout(this.minLengthFlushTask);
301
+ this.minLengthFlushTask = null;
302
+ }
303
+ this.flushTask = setTimeout(() => {
304
+ this.flushTask = null;
305
+ this.flush(false, false, reason);
306
+ }, 0);
307
+ };
308
+ scheduleMinLengthFlush() {
309
+ if (this.minLengthFlushTask || this.events.length === 0 || this.hasReachedMinLength()) return;
310
+ const remainingMs = Math.max(0, this.minReplayLengthMs - (Date.now() - this.startTime));
311
+ this.minLengthFlushTask = setTimeout(() => {
312
+ this.minLengthFlushTask = null;
313
+ this.requestFlush("minLength");
314
+ }, Math.min(remainingMs, MAX_TIMEOUT_MS));
315
+ }
316
+ enqueueEventsIfReady(context, options = {}) {
317
+ const { isFinal = false, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes, sealBeforeMinLength = false } = options;
318
+ if (this.events.length === 0) return;
319
+ if (!isFinal && !sealBeforeMinLength && !this.hasReachedMinLength()) {
320
+ this.scheduleMinLengthFlush();
321
+ return;
322
+ }
323
+ const batches = this.takeEventBatches(maxBatchSizeBytes);
324
+ const blockedByMinLength = sealBeforeMinLength && !this.hasReachedMinLength();
325
+ for (const [index, events] of batches.entries()) {
326
+ const batch = this.createBatch(events, context, isFinal && index === batches.length - 1, flushReason);
327
+ if (blockedByMinLength) this.minLengthBlockedBatches.add(batch);
328
+ this.enqueueBatch(batch);
329
+ }
330
+ this.queuedEventsSizeBytes = 0;
331
+ }
332
+ takeEventBatches(maxBatchSizeBytes) {
333
+ const batches = [];
334
+ let current = [];
335
+ let currentSize = 0;
336
+ for (const event of this.events.splice(0)) {
337
+ const eventSize = this.eventSizeBytes(event);
338
+ if (current.length > 0 && currentSize + eventSize > maxBatchSizeBytes) {
339
+ batches.push(current);
340
+ current = [];
341
+ currentSize = 0;
342
+ }
343
+ current.push(event);
344
+ currentSize += eventSize;
345
+ }
346
+ if (current.length > 0) batches.push(current);
347
+ return batches;
348
+ }
349
+ createBatch(events, context, isFinal, flushReason = "manual") {
350
+ const identifier = getAnonymousId();
351
+ const sequence = this.nextBatchSequence(context);
352
+ const chunkStartedAt = this.chunkStartedAt;
353
+ const now = Date.now();
354
+ this.chunkStartedAt = now;
355
+ return {
356
+ token: this.options.siteKey,
357
+ sessionId: context.sessionId,
358
+ windowId: context.windowId,
359
+ viewId: this.viewId,
360
+ sessionStart: context.sessionStart,
361
+ chunkStartedAt,
362
+ chunkEndedAt: now,
363
+ ...identifier ? { identifier } : {},
364
+ batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
365
+ sequence,
366
+ timestamp: now,
367
+ url: sanitizeUrl(window.location.href),
368
+ ...isFinal ? { isFinal: true } : {},
369
+ flushReason,
370
+ events
371
+ };
372
+ }
373
+ eventSizeBytes(event) {
374
+ return textEncoder.encode(JSON.stringify(event)).byteLength + 1;
375
+ }
376
+ batchSizeBytes(batch) {
377
+ const cached = batchSizeCache.get(batch);
378
+ if (cached !== void 0) return cached;
379
+ const size = textEncoder.encode(JSON.stringify(batch)).byteLength;
380
+ batchSizeCache.set(batch, size);
381
+ return size;
382
+ }
383
+ dropOldestPending(reason) {
384
+ const batch = this.pending[0];
385
+ if (batch) this.removePending(batch, reason);
386
+ }
387
+ removePending(batch, reason) {
388
+ const index = this.pending.indexOf(batch);
389
+ if (index < 0) return false;
390
+ this.pending.splice(index, 1);
391
+ this.pendingSizeBytes = Math.max(0, this.pendingSizeBytes - this.batchSizeBytes(batch));
392
+ if (reason) this.log(`${reason}, dropping batch ${batch.sequence}`);
393
+ return true;
394
+ }
395
+ enqueueBatch(batch) {
396
+ const size = this.batchSizeBytes(batch);
397
+ if (size > this.maxQueueSizeBytes) {
398
+ this.log(`Replay batch ${batch.sequence} is ${size}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`);
399
+ return;
400
+ }
401
+ while ((this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) && this.pending.length > 0) this.dropOldestPending("Pending queue limit reached");
402
+ this.pending.push(batch);
403
+ this.pendingSizeBytes += size;
404
+ }
405
+ async flush(lowLatency, isFinal = false, flushReason = "manual") {
406
+ const rotation = touchActivity(this.options.siteKey);
407
+ if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
408
+ const context = getSessionContext(this.options.siteKey);
409
+ this.enqueueEventsIfReady(context, {
410
+ isFinal,
411
+ flushReason,
412
+ maxBatchSizeBytes: lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes
413
+ });
414
+ if (isFinal) this.finalizeSessionPending(context, flushReason);
415
+ if (this.pending.length === 0) return;
416
+ if (isFinal) for (const batch of this.pending) this.minLengthBlockedBatches.delete(batch);
417
+ else if (!rotation && flushReason !== "sessionRotate" && this.hasReachedMinLength()) this.unblockSessionBatches(context.sessionId);
418
+ if (this.retryTask) {
419
+ if (!lowLatency || !isFinal) return;
420
+ clearTimeout(this.retryTask);
421
+ this.retryTask = null;
422
+ }
423
+ if (this.sending) {
424
+ const queued = this.queuedFlush;
425
+ this.queuedFlush = {
426
+ lowLatency: (queued?.lowLatency ?? false) || lowLatency,
427
+ isFinal: queued?.isFinal || isFinal,
428
+ flushReason: isFinal ? flushReason : queued?.flushReason ?? flushReason
429
+ };
430
+ return;
431
+ }
432
+ this.sending = true;
433
+ try {
434
+ while (this.pending.length > 0) {
435
+ const batch = this.pending[0];
436
+ if (this.minLengthBlockedBatches.has(batch)) break;
437
+ this.inFlightBatch = batch;
438
+ let sent;
439
+ try {
440
+ sent = await this.sendBatch(batch, lowLatency);
441
+ } finally {
442
+ if (this.inFlightBatch === batch) this.inFlightBatch = null;
443
+ }
444
+ if (!sent) {
445
+ this.log(`Failed to send replay batch ${batch.sequence}`);
446
+ if (!this.disposed) this.scheduleRetry();
447
+ break;
448
+ }
449
+ this.retryAttempt = 0;
450
+ this.removePending(batch, "Sent replay batch");
451
+ if (this.queuedFlush?.lowLatency) break;
452
+ }
453
+ } finally {
454
+ this.sending = false;
455
+ if (this.queuedFlush) {
456
+ const queued = this.queuedFlush;
457
+ this.queuedFlush = null;
458
+ this.flush(queued.lowLatency, queued.isFinal, queued.flushReason);
459
+ }
460
+ }
461
+ }
462
+ scheduleRetry() {
463
+ if (this.retryTask) return;
464
+ this.retryAttempt = Math.min(this.retryAttempt + 1, RETRY_MAX_ATTEMPTS);
465
+ const delay = this.retryDelay(this.retryAttempt);
466
+ this.log(this.retryAttempt === RETRY_MAX_ATTEMPTS ? `Replay delivery paused for ${delay}ms` : `Retrying replay delivery in ${delay}ms`);
467
+ this.retryTask = setTimeout(() => {
468
+ this.retryTask = null;
469
+ if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
470
+ this.flush(false);
471
+ }, delay);
472
+ }
473
+ retryDelay(attempt) {
474
+ if (attempt >= RETRY_MAX_ATTEMPTS) return RETRY_COOLDOWN_MS;
475
+ const ceiling = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
476
+ return Math.round(ceiling * (.5 + Math.random() * .5));
477
+ }
478
+ async sendBatch(batch, lowLatency) {
479
+ const json = JSON.stringify(batch);
480
+ const sendOptions = {
481
+ useBeacon: lowLatency,
482
+ keepalive: lowLatency
483
+ };
484
+ if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
485
+ const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
486
+ const compressed = new Uint8Array(await new Response(stream).arrayBuffer());
487
+ this.log(`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round(compressed.byteLength / json.length * 100)}%)`);
488
+ return sendData({
489
+ url: `${this.endpoint}?encoding=gzip`,
490
+ data: compressed,
491
+ contentType: "application/octet-stream",
492
+ ...sendOptions
493
+ });
494
+ } catch {
495
+ this.log("Compression failed, using uncompressed");
496
+ }
497
+ return sendData({
498
+ url: this.endpoint,
499
+ data: json,
500
+ contentType: "application/json",
501
+ ...sendOptions
502
+ });
503
+ }
504
+ };
505
+ //#endregion
506
+ export { ReplayTracker as default };
@@ -1 +1,115 @@
1
- import{a as e,r as t,t as n}from"./chunks/api-urls-Bh77rElT.js";import{r,t as i}from"./chunks/send-data-CIbuTxPh.js";var a=class{options;endpoint;metrics=new Map;started=!1;flushing=!1;initialUrl=``;currentUrl=``;constructor(r){this.options=r,this.endpoint=`${e(r.baseUrl,n)}${t.vitals}`}async start(){if(this.started||typeof window>`u`)return;this.started=!0,this.initialUrl=window.location.href,this.currentUrl=this.initialUrl;let e=await(this.options.attribution?import(`web-vitals/attribution`):import(`web-vitals`));if(!this.started)return;let t={reportAllChanges:!0};e.onCLS(this.onMetric,t),e.onINP(this.onMetric,t),e.onLCP(this.onMetric,t),e.onFCP(this.onMetric),e.onTTFB(this.onMetric)}stop(){this.started&&(this.started=!1,this.flush())}onPageHidden(e=!1){e||this.flush()}trackPageChange(e){if(!this.started)return;let t=e??window.location.href;t!==this.currentUrl&&(this.currentUrl=t)}onMetric=e=>{if(!this.started)return;let t=e.name,n=this.getMetricUrl(t),{id:r,rating:i,delta:a,navigationType:o}=e,s=`attribution`in e&&e.attribution?e.attribution:void 0;this.metrics.set(`${n}\n${t}`,{metric:t,url:n,value:e.value,attributes:{id:r,rating:i,delta:a,navigationType:o,...s}})};getMetricUrl(e){return e===`FCP`||e===`LCP`||e===`TTFB`?this.initialUrl:this.currentUrl||this.initialUrl}flush(){if(!this.metrics.size||this.flushing)return;this.flushing=!0;let e=this.metrics;this.metrics=new Map;let t=r(this.options.siteKey),n=new Map;for(let t of e.values()){let e=n.get(t.url)??[];e.push(t),n.set(t.url,e)}Promise.all([...n].map(([e,n])=>i({url:this.endpoint,data:JSON.stringify({token:this.options.siteKey,sessionId:t.sessionId,windowId:t.windowId,vitals:n.map(({url:e,...t})=>t),metadata:{url:e}}),debug:this.options.debug,debugPrefix:`[WebVitals]`}).then(t=>({ok:t,url:e,vitals:n})))).then(e=>{for(let t of e)if(!t.ok)for(let e of t.vitals){let n=`${t.url}\n${e.metric}`;this.metrics.has(n)||this.metrics.set(n,e)}this.flushing=!1})}};export{a as default};
1
+ import { a as resolveBaseUrl, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-CaNa8upY.js";
2
+ import { r as getSessionContext, t as sendData } from "./chunks/send-data-DjdbGDDc.js";
3
+ //#region src/web-vitals.ts
4
+ const METRIC_NAMES = /* @__PURE__ */ new Set([
5
+ "CLS",
6
+ "INP",
7
+ "LCP",
8
+ "FCP",
9
+ "TTFB"
10
+ ]);
11
+ var WebVitalsTracker = class {
12
+ options;
13
+ endpoint;
14
+ metrics = /* @__PURE__ */ new Map();
15
+ started = false;
16
+ flushing = false;
17
+ initialUrl = "";
18
+ currentUrl = "";
19
+ constructor(options) {
20
+ this.options = options;
21
+ this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.vitals}`;
22
+ }
23
+ async start() {
24
+ if (this.started || typeof window === "undefined") return;
25
+ this.started = true;
26
+ this.initialUrl = sanitizeUrl(window.location.href);
27
+ this.currentUrl = this.initialUrl;
28
+ const mod = await (this.options.attribution ? import("web-vitals/attribution") : import("web-vitals"));
29
+ if (!this.started) return;
30
+ const changes = { reportAllChanges: true };
31
+ mod.onCLS(this.onMetric, changes);
32
+ mod.onINP(this.onMetric, changes);
33
+ mod.onLCP(this.onMetric, changes);
34
+ mod.onFCP(this.onMetric);
35
+ mod.onTTFB(this.onMetric);
36
+ }
37
+ stop() {
38
+ if (!this.started) return;
39
+ this.started = false;
40
+ this.flush();
41
+ }
42
+ onPageHidden(persisted = false) {
43
+ if (persisted) return;
44
+ this.flush();
45
+ }
46
+ trackPageChange(url) {
47
+ if (!this.started) return;
48
+ const next = sanitizeUrl(url ?? window.location.href);
49
+ if (next === this.currentUrl) return;
50
+ this.currentUrl = next;
51
+ }
52
+ onMetric = (metric) => {
53
+ if (!this.started) return;
54
+ const name = metric.name;
55
+ if (!METRIC_NAMES.has(name) || !Number.isFinite(metric.value) || metric.value < 0) return;
56
+ const url = this.getMetricUrl(name);
57
+ const { id, rating, delta, navigationType } = metric;
58
+ const attribution = "attribution" in metric && metric.attribution ? metric.attribution : void 0;
59
+ this.metrics.set(`${url}\n${name}`, {
60
+ metric: name,
61
+ url,
62
+ value: metric.value,
63
+ attributes: {
64
+ id,
65
+ rating,
66
+ delta,
67
+ navigationType,
68
+ ...attribution
69
+ }
70
+ });
71
+ };
72
+ getMetricUrl(metric) {
73
+ if (metric === "FCP" || metric === "LCP" || metric === "TTFB") return this.initialUrl;
74
+ return this.currentUrl || this.initialUrl;
75
+ }
76
+ async flush() {
77
+ if (!this.metrics.size || this.flushing) return;
78
+ this.flushing = true;
79
+ const batch = this.metrics;
80
+ this.metrics = /* @__PURE__ */ new Map();
81
+ const session = getSessionContext(this.options.siteKey);
82
+ const byUrl = /* @__PURE__ */ new Map();
83
+ for (const vital of batch.values()) {
84
+ const vitals = byUrl.get(vital.url) ?? [];
85
+ vitals.push(vital);
86
+ byUrl.set(vital.url, vitals);
87
+ }
88
+ const results = await Promise.all([...byUrl].map(async ([url, vitals]) => ({
89
+ ok: await sendData({
90
+ url: this.endpoint,
91
+ data: JSON.stringify({
92
+ token: this.options.siteKey,
93
+ sessionId: session.sessionId,
94
+ windowId: session.windowId,
95
+ vitals: vitals.map(({ url: _url, ...vital }) => vital),
96
+ metadata: { url }
97
+ }),
98
+ debug: this.options.debug,
99
+ debugPrefix: "[WebVitals]"
100
+ }),
101
+ url,
102
+ vitals
103
+ })));
104
+ for (const result of results) {
105
+ if (result.ok) continue;
106
+ for (const vital of result.vitals) {
107
+ const key = `${result.url}\n${vital.metric}`;
108
+ if (!this.metrics.has(key)) this.metrics.set(key, vital);
109
+ }
110
+ }
111
+ this.flushing = false;
112
+ }
113
+ };
114
+ //#endregion
115
+ export { WebVitalsTracker as default };
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.5.0",
45
+ "version": "0.6.0",
46
46
  "scripts": {
47
47
  "build": "tsdown && bun run check-size",
48
48
  "dev": "bun run build",
@@ -60,6 +60,6 @@
60
60
  "@rrweb/record": "^2.1.0",
61
61
  "@rrweb/rrweb-plugin-console-record": "^2.1.0",
62
62
  "@rrweb/rrweb-plugin-sequential-id-record": "^2.1.0",
63
- "web-vitals": "^5.3.0"
63
+ "web-vitals": "^6.0.0"
64
64
  }
65
65
  }
@@ -1 +0,0 @@
1
- const e=`https://metrics.faststats.dev`,t=`https://flags.faststats.dev`;function n(e,t){return e?.replace(/\/+$/,``)||t}const r={events:`/v1/web`,identify:`/v1/identify`,replay:`/v1/replay`,vitals:`/v1/vitals`,flags:`/v1/check`},i=[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`];function a(){let e={page:location.pathname,url:location.href,referrer:document.referrer||null,title:document.title||``};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of i){let r=t.get(n);r&&(e[n]=r)}return e}export{n as a,a as i,t as n,r,e as t};
@@ -1 +0,0 @@
1
- import{i as e,n as t}from"./send-data-CIbuTxPh.js";const n=`faststats_anon_id`;function r(){try{return globalThis.localStorage}catch{return}}function i(){let e=r();if(!e)return``;try{let r=e.getItem(n);if(r)return r;let i=t();return e.setItem(n,i),i}catch{return``}}function a(t){return t??e()?``:i()}function o(e){if(e)return``;try{let e=r();if(!e)return``;e.removeItem(n)}catch{return``}return i()}export{o as n,a as t};
@@ -1 +0,0 @@
1
- const e=`faststats_session`;let t=!1,n=null;const r=new Set;function i(e){try{return globalThis[e]}catch{return null}}function a(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}function o(){let t=i(`localStorage`);if(!t)return n;let r=t.getItem(e);if(r)try{let e=JSON.parse(r);if(e.id)return e}catch{}return n}function s(t){let r=i(`localStorage`);if(!r){n=t;return}try{r.setItem(e,JSON.stringify(t))}catch{n=t}}function c(){n=null;try{i(`localStorage`)?.removeItem(e)}catch{}}function l(e,t){return t-e.activity>=18e5||t-e.start>=864e5}function u(e,t){for(let n of r)n(e,t)}function d(e,t){let r=Date.now();if(e)return n?t&&(n.activity=r):n={id:a(),start:r,activity:r},{session:n,rotated:!1,previous:null};let i=o();if(i&&!l(i,r))return t&&(i.activity=r,s(i)),{session:i,rotated:!1,previous:null};let c=i,u={id:a(),start:r,activity:r};return s(u),{session:u,rotated:c!==null,previous:c}}function f(e,t,n){if(t)return n;let r=`faststats_window_id_${e}`,o=i(`sessionStorage`);if(!o)return n;try{let e=o.getItem(r);if(e)return e;let t=a();return o.setItem(r,t),t}catch{return n}}function p(e,t,n,r){let i=r??f(e,n,t.id);return{sessionId:t.id,windowId:i,sessionStart:t.start}}function m(e){t=e,n=null}function h(){return t}function g(e,n){let r=n??t,{session:i}=d(r,!1);return p(e,i,r)}function _(e,n){let r=n??t,{session:i,rotated:a,previous:o}=d(r,!0);if(!a||!o)return null;let s=f(e,r,i.id),c={prev:p(e,o,r,s),next:p(e,i,r,s)};return u(c.prev,c.next),c}function v(e){let r=t,o=g(e,r);if(c(),r){let e=Date.now();n={id:a(),start:e,activity:e}}else try{i(`sessionStorage`)?.removeItem(`faststats_window_id_${e}`)}catch{}let s=g(e,r);return o.sessionId!==s.sessionId&&u(o,s),s}function y(e){return r.add(e),()=>r.delete(e)}async function b(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!0,signal:l}=e;if(l?.aborted)return!1;if(s&&typeof globalThis.navigator?.sendBeacon==`function`)try{let e=n instanceof Blob||typeof Blob>`u`?n:new Blob([n],{type:r});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,credentials:`omit`,signal:l}),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{y as a,_ as c,h as i,a as n,v as o,g as r,m as s,b as t};