@faststats/web 0.5.0 → 0.7.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,579 @@
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-Dp2XNjRF.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-DR1JUXka.js";
3
+ import { t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
4
+ import { record } from "@rrweb/record";
5
+ //#region src/replay-mutation-throttler.ts
6
+ const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
7
+ const MUTATION_SOURCE_TYPE = 0;
8
+ const MAX_TRACKED_NODES = 1e4;
9
+ /** Bounds pathological per-node attribute churn without breaking the mutation chain. */
10
+ var ReplayMutationThrottler = class {
11
+ buckets = /* @__PURE__ */ new Map();
12
+ bucketSize;
13
+ refillRate;
14
+ constructor(options = {}) {
15
+ this.bucketSize = Math.max(1, options.bucketSize ?? 100);
16
+ this.refillRate = Math.max(1, options.refillRate ?? 10);
17
+ }
18
+ reset() {
19
+ this.buckets.clear();
20
+ }
21
+ throttle(event) {
22
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE) return event;
23
+ const data = event.data;
24
+ if (data.source !== MUTATION_SOURCE_TYPE || !data.attributes?.length) return event;
25
+ const attributes = data.attributes.filter(({ id }) => this.consume(id));
26
+ if (attributes.length === data.attributes.length) return event;
27
+ if (attributes.length === 0 && (data.adds?.length ?? 0) === 0 && (data.removes?.length ?? 0) === 0 && (data.texts?.length ?? 0) === 0) return;
28
+ return {
29
+ ...event,
30
+ data: {
31
+ ...data,
32
+ attributes
33
+ }
34
+ };
35
+ }
36
+ consume(nodeId) {
37
+ const now = Date.now();
38
+ const bucket = this.buckets.get(nodeId) ?? {
39
+ tokens: this.bucketSize,
40
+ updatedAt: now
41
+ };
42
+ const elapsedSeconds = Math.max(0, now - bucket.updatedAt) / 1e3;
43
+ bucket.tokens = Math.min(this.bucketSize, bucket.tokens + elapsedSeconds * this.refillRate);
44
+ bucket.updatedAt = now;
45
+ if (!this.buckets.has(nodeId) && this.buckets.size >= MAX_TRACKED_NODES) this.buckets.delete(this.buckets.keys().next().value);
46
+ this.buckets.set(nodeId, bucket);
47
+ if (bucket.tokens < 1) return false;
48
+ bucket.tokens -= 1;
49
+ return true;
50
+ }
51
+ };
52
+ //#endregion
53
+ //#region src/replay.ts
54
+ const MAX_TIMEOUT_MS = 2147483647;
55
+ const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2 * 1024 * 1024;
56
+ const DEFAULT_MAX_BATCH_SIZE_BYTES = 900 * 1024;
57
+ const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 60 * 1024;
58
+ const RETRY_BASE_DELAY_MS = 2e3;
59
+ const RETRY_MAX_ATTEMPTS = 5;
60
+ const RETRY_COOLDOWN_MS = 5 * 6e4;
61
+ const VIEW_META_EVENT_TYPE = 5;
62
+ const RRWEB_EVENT_FULL_SNAPSHOT = 2;
63
+ const RRWEB_EVENT_META = 4;
64
+ const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
65
+ const textEncoder = new TextEncoder();
66
+ const batchSizeCache = /* @__PURE__ */ new WeakMap();
67
+ var ReplayTracker = class {
68
+ options;
69
+ endpoint;
70
+ compressionSupported = typeof window !== "undefined" && "CompressionStream" in window;
71
+ flushInterval;
72
+ maxEvents;
73
+ maxBatchSizeBytes;
74
+ maxLowLatencyBatchSizeBytes;
75
+ maxPendingBatches;
76
+ maxQueueSizeBytes;
77
+ minReplayLengthMs;
78
+ events = [];
79
+ pending = [];
80
+ minLengthBlockedBatches = /* @__PURE__ */ new WeakSet();
81
+ queuedEventsSizeBytes = 0;
82
+ pendingSizeBytes = 0;
83
+ viewId = createId();
84
+ viewUrl = "";
85
+ chunkStartedAt = 0;
86
+ started = false;
87
+ disposed = false;
88
+ startGeneration = 0;
89
+ startTime = 0;
90
+ sequence = 0;
91
+ intervalId = null;
92
+ flushTask = null;
93
+ retryTask = null;
94
+ retryAttempt = 0;
95
+ minLengthFlushTask = null;
96
+ stopRecording;
97
+ sending = false;
98
+ inFlightBatch = null;
99
+ queuedFlush = null;
100
+ unsubscribeRotation;
101
+ lastCookielessMode = isCookielessMode();
102
+ terminalFlushRequested = false;
103
+ overflowed = false;
104
+ mutationThrottler;
105
+ constructor(options) {
106
+ this.options = options;
107
+ this.endpoint = `${resolveBaseUrl(options.baseUrl, ANALYTICS_BASE)}${URLS.replay}`;
108
+ this.flushInterval = options.flushInterval ?? 15e3;
109
+ this.maxEvents = options.maxEvents ?? 1e3;
110
+ this.maxBatchSizeBytes = options.maxBatchSizeBytes ?? DEFAULT_MAX_BATCH_SIZE_BYTES;
111
+ this.maxLowLatencyBatchSizeBytes = options.maxLowLatencyBatchSizeBytes ?? DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES;
112
+ this.maxPendingBatches = options.maxPendingBatches ?? 30;
113
+ this.maxQueueSizeBytes = options.maxQueueSizeBytes ?? DEFAULT_MAX_QUEUE_SIZE_BYTES;
114
+ this.minReplayLengthMs = options.minReplayLengthMs ?? 3e3;
115
+ this.mutationThrottler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
116
+ }
117
+ log(...args) {
118
+ if (this.options.debug) console.log("[Replay]", ...args);
119
+ }
120
+ clearFlushTimers() {
121
+ if (this.flushTask) clearTimeout(this.flushTask);
122
+ if (this.retryTask) clearTimeout(this.retryTask);
123
+ if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
124
+ this.flushTask = null;
125
+ this.retryTask = null;
126
+ this.retryAttempt = 0;
127
+ this.minLengthFlushTask = null;
128
+ }
129
+ clearQueues() {
130
+ this.events.length = 0;
131
+ this.queuedEventsSizeBytes = 0;
132
+ this.pending.length = 0;
133
+ this.pendingSizeBytes = 0;
134
+ }
135
+ dropMinLengthBlockedBatches() {
136
+ for (let index = this.pending.length - 1; index >= 0; index--) {
137
+ const batch = this.pending[index];
138
+ if (batch && this.minLengthBlockedBatches.has(batch)) this.removePending(batch);
139
+ }
140
+ }
141
+ unblockSessionBatches(sessionId) {
142
+ for (const batch of this.pending) if (batch.sessionId === sessionId) this.minLengthBlockedBatches.delete(batch);
143
+ }
144
+ finalizeSessionPending(context, flushReason) {
145
+ const batch = this.pending.findLast((candidate) => candidate.sessionId === context.sessionId);
146
+ if (!batch || batch.isFinal) return;
147
+ if (batch === this.inFlightBatch) {
148
+ this.enqueueBatch(this.createBatch([], context, true, flushReason));
149
+ return;
150
+ }
151
+ const previousSize = this.batchSizeBytes(batch);
152
+ batch.isFinal = true;
153
+ batch.flushReason = flushReason;
154
+ batchSizeCache.delete(batch);
155
+ this.pendingSizeBytes += this.batchSizeBytes(batch) - previousSize;
156
+ }
157
+ nextBatchSequence(context) {
158
+ const key = `faststats_replay_batch_sequence_${this.options.siteKey}_${context.windowId}`;
159
+ let sequence = this.sequence;
160
+ const parsed = Number.parseInt(getStorageItem("sessionStorage", key) ?? "", 10);
161
+ if (Number.isFinite(parsed) && parsed >= 0) sequence = parsed;
162
+ setStorageItem("sessionStorage", key, String(sequence + 1));
163
+ this.sequence = sequence + 1;
164
+ return sequence;
165
+ }
166
+ setCookielessMode(cookieless) {
167
+ if (this.lastCookielessMode === cookieless) return;
168
+ this.lastCookielessMode = cookieless;
169
+ this.clearQueues();
170
+ this.clearFlushTimers();
171
+ this.startTime = getSessionContext(this.options.siteKey).sessionStart;
172
+ this.chunkStartedAt = Date.now();
173
+ }
174
+ start() {
175
+ if (this.started || typeof window === "undefined") return;
176
+ this.started = true;
177
+ this.disposed = false;
178
+ this.terminalFlushRequested = false;
179
+ this.overflowed = false;
180
+ this.viewId = createId();
181
+ this.viewUrl = sanitizeUrl(window.location.href, this.options.trackHash ?? false);
182
+ this.startTime = getSessionContext(this.options.siteKey).sessionStart;
183
+ this.chunkStartedAt = Date.now();
184
+ this.unsubscribeRotation = onSessionRotated((prev, next) => {
185
+ this.onSessionRotated(prev, next);
186
+ });
187
+ const generation = ++this.startGeneration;
188
+ this.beginRecording(generation);
189
+ this.intervalId = setInterval(() => this.requestFlush("interval"), this.flushInterval);
190
+ window.addEventListener("beforeunload", this.onUnload);
191
+ this.log("Recording started");
192
+ }
193
+ trackPageChange(url) {
194
+ if (!this.started) return;
195
+ this.viewId = createId();
196
+ const href = sanitizeUrl(url ?? window.location.href, this.options.trackHash ?? false);
197
+ this.viewUrl = href;
198
+ this.onEvent({
199
+ type: VIEW_META_EVENT_TYPE,
200
+ timestamp: Date.now(),
201
+ data: {
202
+ tag: "faststats:view",
203
+ payload: {
204
+ href,
205
+ viewId: this.viewId
206
+ }
207
+ }
208
+ }, false);
209
+ }
210
+ onPageHidden(persisted = false, terminal = false) {
211
+ if (persisted) return;
212
+ if (terminal) {
213
+ this.requestTerminalFlush("pageHidden");
214
+ return;
215
+ }
216
+ this.flush(false, false, "pageHidden");
217
+ }
218
+ onPageShow(persisted = false) {
219
+ if (!persisted || !this.started) return;
220
+ if (this.pending.length > 0) this.flush(false, false, "pageShow");
221
+ }
222
+ async beginRecording(generation) {
223
+ 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")]);
224
+ if (generation !== this.startGeneration) return;
225
+ const plugins = [sequentialIdPluginModule.getRecordSequentialIdPlugin({ key: REPLAY_SEQUENTIAL_ID_KEY })];
226
+ if (consolePluginModule) plugins.push(consolePluginModule.getRecordConsolePlugin({
227
+ level: this.options.consoleLevel ?? [
228
+ "error",
229
+ "warn",
230
+ "info",
231
+ "log"
232
+ ],
233
+ lengthThreshold: this.options.consoleLengthThreshold ?? 200,
234
+ stringifyOptions: {
235
+ numOfKeysLimit: 25,
236
+ depthOfLimit: 3,
237
+ stringLengthLimit: this.options.consoleStringLengthLimit ?? 500
238
+ }
239
+ }));
240
+ const stop = record({
241
+ emit: this.onEvent,
242
+ sampling: this.options.sampling ?? {
243
+ mousemove: 50,
244
+ mouseInteraction: true,
245
+ scroll: 150,
246
+ media: 800,
247
+ input: "last"
248
+ },
249
+ slimDOMOptions: this.options.slimDOMOptions ?? {
250
+ script: true,
251
+ comment: true,
252
+ headFavicon: true,
253
+ headWhitespace: true,
254
+ headMetaDescKeywords: true,
255
+ headMetaSocial: true,
256
+ headMetaRobots: true,
257
+ headMetaHttpEquiv: true,
258
+ headMetaAuthorship: true
259
+ },
260
+ maskAllInputs: this.options.maskAllInputs ?? true,
261
+ maskInputOptions: this.options.maskInputOptions ?? {
262
+ password: true,
263
+ email: true,
264
+ tel: true
265
+ },
266
+ blockClass: this.options.blockClass,
267
+ blockSelector: this.options.blockSelector,
268
+ maskTextClass: this.options.maskTextClass,
269
+ maskTextSelector: this.options.maskTextSelector,
270
+ checkoutEveryNms: this.options.checkoutEveryNms ?? 5 * 6e4,
271
+ checkoutEveryNth: this.options.checkoutEveryNth,
272
+ plugins
273
+ });
274
+ if (generation !== this.startGeneration) {
275
+ stop?.();
276
+ return;
277
+ }
278
+ this.stopRecording = stop;
279
+ }
280
+ stop(discard = false) {
281
+ if (!this.started) return;
282
+ this.started = false;
283
+ this.disposed = true;
284
+ this.startGeneration++;
285
+ this.unsubscribeRotation?.();
286
+ this.unsubscribeRotation = void 0;
287
+ this.stopRecording?.();
288
+ this.stopRecording = void 0;
289
+ if (this.intervalId) clearInterval(this.intervalId);
290
+ this.clearFlushTimers();
291
+ this.intervalId = null;
292
+ window.removeEventListener("beforeunload", this.onUnload);
293
+ if (discard) {
294
+ this.clearQueues();
295
+ this.log("Recording discarded");
296
+ return;
297
+ }
298
+ if (!this.hasReachedMinLength()) {
299
+ this.events.length = 0;
300
+ this.queuedEventsSizeBytes = 0;
301
+ this.dropMinLengthBlockedBatches();
302
+ this.log(`Session too short (${Date.now() - this.startTime}ms), discarding events`);
303
+ }
304
+ this.flush(true, true, "stop");
305
+ this.log("Recording stopped");
306
+ }
307
+ onSessionRotated(prev, next) {
308
+ if (!this.started) return;
309
+ this.applySessionRotation(prev, next);
310
+ if (this.pending.length > 0) queueMicrotask(() => void this.flush(false, false, "sessionRotate"));
311
+ }
312
+ applySessionRotation(prev, next) {
313
+ this.enqueueEventsIfReady(prev, {
314
+ isFinal: true,
315
+ flushReason: "sessionRotate"
316
+ });
317
+ this.finalizeSessionPending(prev, "sessionRotate");
318
+ this.unblockSessionBatches(prev.sessionId);
319
+ this.resetSessionTiming(next);
320
+ }
321
+ resetSessionTiming(next) {
322
+ if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
323
+ this.minLengthFlushTask = null;
324
+ this.startTime = next.sessionStart;
325
+ this.chunkStartedAt = Date.now();
326
+ }
327
+ onEvent = (event, _isCheckout) => {
328
+ if (this.overflowed) return;
329
+ let capturedEvent = event;
330
+ if (event.type === RRWEB_EVENT_META) {
331
+ const data = event.data;
332
+ if (typeof data.href === "string") capturedEvent = {
333
+ ...event,
334
+ data: {
335
+ ...data,
336
+ href: sanitizeUrl(data.href, this.options.trackHash ?? false)
337
+ }
338
+ };
339
+ }
340
+ if (capturedEvent.type === RRWEB_EVENT_FULL_SNAPSHOT) this.mutationThrottler?.reset();
341
+ else {
342
+ const throttled = this.mutationThrottler?.throttle(capturedEvent);
343
+ if (this.mutationThrottler && !throttled) return;
344
+ if (throttled) capturedEvent = throttled;
345
+ }
346
+ this.events.push(capturedEvent);
347
+ this.queuedEventsSizeBytes += this.eventSizeBytes(capturedEvent);
348
+ let reason;
349
+ if (this.events.length >= this.maxEvents) reason = "maxEvents";
350
+ else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
351
+ if (reason) this.requestFlush(reason);
352
+ else this.scheduleMinLengthFlush();
353
+ };
354
+ onUnload = () => {
355
+ this.requestTerminalFlush("unload");
356
+ };
357
+ requestTerminalFlush(reason) {
358
+ if (this.terminalFlushRequested) return;
359
+ this.terminalFlushRequested = true;
360
+ if (!this.hasReachedMinLength()) {
361
+ this.events.length = 0;
362
+ this.queuedEventsSizeBytes = 0;
363
+ this.dropMinLengthBlockedBatches();
364
+ return;
365
+ }
366
+ this.flush(true, true, reason);
367
+ }
368
+ hasReachedMinLength() {
369
+ return this.minReplayLengthMs <= 0 || Date.now() - this.startTime >= this.minReplayLengthMs;
370
+ }
371
+ requestFlush = (reason = "manual") => {
372
+ if (this.flushTask || this.events.length === 0) return;
373
+ if (this.minLengthFlushTask) {
374
+ clearTimeout(this.minLengthFlushTask);
375
+ this.minLengthFlushTask = null;
376
+ }
377
+ this.flushTask = setTimeout(() => {
378
+ this.flushTask = null;
379
+ this.flush(false, false, reason);
380
+ }, 0);
381
+ };
382
+ scheduleMinLengthFlush() {
383
+ if (this.minLengthFlushTask || this.events.length === 0 || this.hasReachedMinLength()) return;
384
+ const remainingMs = Math.max(0, this.minReplayLengthMs - (Date.now() - this.startTime));
385
+ this.minLengthFlushTask = setTimeout(() => {
386
+ this.minLengthFlushTask = null;
387
+ this.requestFlush("minLength");
388
+ }, Math.min(remainingMs, MAX_TIMEOUT_MS));
389
+ }
390
+ enqueueEventsIfReady(context, options = {}) {
391
+ const { isFinal = false, flushReason = "manual", maxBatchSizeBytes = this.maxBatchSizeBytes, sealBeforeMinLength = false } = options;
392
+ if (this.events.length === 0) return;
393
+ if (!isFinal && !sealBeforeMinLength && !this.hasReachedMinLength()) {
394
+ this.scheduleMinLengthFlush();
395
+ return;
396
+ }
397
+ const batches = this.takeEventBatches(maxBatchSizeBytes);
398
+ const blockedByMinLength = sealBeforeMinLength && !this.hasReachedMinLength();
399
+ for (const [index, events] of batches.entries()) {
400
+ const batch = this.createBatch(events, context, isFinal && index === batches.length - 1, flushReason);
401
+ if (blockedByMinLength) this.minLengthBlockedBatches.add(batch);
402
+ this.enqueueBatch(batch);
403
+ }
404
+ this.queuedEventsSizeBytes = 0;
405
+ }
406
+ takeEventBatches(maxBatchSizeBytes) {
407
+ const batches = [];
408
+ let current = [];
409
+ let currentSize = 0;
410
+ for (const event of this.events.splice(0)) {
411
+ const eventSize = this.eventSizeBytes(event);
412
+ if (current.length > 0 && currentSize + eventSize > maxBatchSizeBytes) {
413
+ batches.push(current);
414
+ current = [];
415
+ currentSize = 0;
416
+ }
417
+ current.push(event);
418
+ currentSize += eventSize;
419
+ }
420
+ if (current.length > 0) batches.push(current);
421
+ return batches;
422
+ }
423
+ createBatch(events, context, isFinal, flushReason = "manual") {
424
+ const identifier = getAnonymousId(this.options.siteKey);
425
+ const sequence = this.nextBatchSequence(context);
426
+ const chunkStartedAt = this.chunkStartedAt;
427
+ const now = Date.now();
428
+ this.chunkStartedAt = now;
429
+ return {
430
+ token: this.options.siteKey,
431
+ sessionId: context.sessionId,
432
+ windowId: context.windowId,
433
+ viewId: this.viewId,
434
+ sessionStart: context.sessionStart,
435
+ chunkStartedAt,
436
+ chunkEndedAt: now,
437
+ ...identifier ? { identifier } : {},
438
+ batchId: `${context.sessionId}-${sequence}-${chunkStartedAt}`,
439
+ sequence,
440
+ timestamp: now,
441
+ url: this.viewUrl || sanitizeUrl(window.location.href, this.options.trackHash ?? false),
442
+ ...isFinal ? { isFinal: true } : {},
443
+ flushReason,
444
+ events
445
+ };
446
+ }
447
+ eventSizeBytes(event) {
448
+ return textEncoder.encode(JSON.stringify(event)).byteLength + 1;
449
+ }
450
+ batchSizeBytes(batch) {
451
+ const cached = batchSizeCache.get(batch);
452
+ if (cached !== void 0) return cached;
453
+ const size = textEncoder.encode(JSON.stringify(batch)).byteLength;
454
+ batchSizeCache.set(batch, size);
455
+ return size;
456
+ }
457
+ removePending(batch, reason) {
458
+ const index = this.pending.indexOf(batch);
459
+ if (index < 0) return false;
460
+ this.pending.splice(index, 1);
461
+ this.pendingSizeBytes = Math.max(0, this.pendingSizeBytes - this.batchSizeBytes(batch));
462
+ if (reason) this.log(`${reason}, dropping batch ${batch.sequence}`);
463
+ return true;
464
+ }
465
+ enqueueBatch(batch) {
466
+ const size = this.batchSizeBytes(batch);
467
+ if (size > this.maxQueueSizeBytes || this.pending.length >= this.maxPendingBatches || this.pendingSizeBytes + size > this.maxQueueSizeBytes) {
468
+ this.log(`Replay queue limit reached at batch ${batch.sequence}; stopping recording at the last complete chain`);
469
+ this.overflowed = true;
470
+ this.stopRecording?.();
471
+ this.stopRecording = void 0;
472
+ return;
473
+ }
474
+ this.pending.push(batch);
475
+ this.pendingSizeBytes += size;
476
+ }
477
+ async flush(lowLatency, isFinal = false, flushReason = "manual") {
478
+ const rotation = touchActivity(this.options.siteKey);
479
+ if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
480
+ const context = getSessionContext(this.options.siteKey);
481
+ this.enqueueEventsIfReady(context, {
482
+ isFinal,
483
+ flushReason,
484
+ maxBatchSizeBytes: lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes
485
+ });
486
+ if (isFinal) this.finalizeSessionPending(context, flushReason);
487
+ if (this.pending.length === 0) return;
488
+ if (isFinal) for (const batch of this.pending) this.minLengthBlockedBatches.delete(batch);
489
+ else if (!rotation && flushReason !== "sessionRotate" && this.hasReachedMinLength()) this.unblockSessionBatches(context.sessionId);
490
+ if (this.retryTask) {
491
+ if (!lowLatency || !isFinal) return;
492
+ clearTimeout(this.retryTask);
493
+ this.retryTask = null;
494
+ }
495
+ if (this.sending) {
496
+ const queued = this.queuedFlush;
497
+ this.queuedFlush = {
498
+ lowLatency: (queued?.lowLatency ?? false) || lowLatency,
499
+ isFinal: queued?.isFinal || isFinal,
500
+ flushReason: isFinal ? flushReason : queued?.flushReason ?? flushReason
501
+ };
502
+ return;
503
+ }
504
+ this.sending = true;
505
+ try {
506
+ while (this.pending.length > 0) {
507
+ const batch = this.pending[0];
508
+ if (this.minLengthBlockedBatches.has(batch)) break;
509
+ this.inFlightBatch = batch;
510
+ let sent;
511
+ try {
512
+ sent = await this.sendBatch(batch, lowLatency);
513
+ } finally {
514
+ if (this.inFlightBatch === batch) this.inFlightBatch = null;
515
+ }
516
+ if (!sent) {
517
+ this.log(`Failed to send replay batch ${batch.sequence}`);
518
+ if (!this.disposed) this.scheduleRetry();
519
+ break;
520
+ }
521
+ this.retryAttempt = 0;
522
+ this.removePending(batch, "Sent replay batch");
523
+ if (this.queuedFlush?.lowLatency) break;
524
+ }
525
+ } finally {
526
+ this.sending = false;
527
+ if (this.queuedFlush) {
528
+ const queued = this.queuedFlush;
529
+ this.queuedFlush = null;
530
+ this.flush(queued.lowLatency, queued.isFinal, queued.flushReason);
531
+ }
532
+ }
533
+ }
534
+ scheduleRetry() {
535
+ if (this.retryTask) return;
536
+ this.retryAttempt = Math.min(this.retryAttempt + 1, RETRY_MAX_ATTEMPTS);
537
+ const delay = this.retryDelay(this.retryAttempt);
538
+ this.log(this.retryAttempt === RETRY_MAX_ATTEMPTS ? `Replay delivery paused for ${delay}ms` : `Retrying replay delivery in ${delay}ms`);
539
+ this.retryTask = setTimeout(() => {
540
+ this.retryTask = null;
541
+ if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
542
+ this.flush(false);
543
+ }, delay);
544
+ }
545
+ retryDelay(attempt) {
546
+ if (attempt >= RETRY_MAX_ATTEMPTS) return RETRY_COOLDOWN_MS;
547
+ const ceiling = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
548
+ return Math.round(ceiling * (.5 + Math.random() * .5));
549
+ }
550
+ async sendBatch(batch, lowLatency) {
551
+ const json = JSON.stringify(batch);
552
+ const lowLatencySafe = lowLatency && textEncoder.encode(json).byteLength <= 60 * 1024;
553
+ const sendOptions = {
554
+ useBeacon: lowLatencySafe,
555
+ keepalive: lowLatencySafe
556
+ };
557
+ if (!lowLatency && (this.options.compress ?? true) && this.compressionSupported) try {
558
+ const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
559
+ const compressed = new Uint8Array(await new Response(stream).arrayBuffer());
560
+ this.log(`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round(compressed.byteLength / json.length * 100)}%)`);
561
+ return sendData({
562
+ url: `${this.endpoint}?encoding=gzip`,
563
+ data: compressed,
564
+ contentType: "application/octet-stream",
565
+ ...sendOptions
566
+ });
567
+ } catch {
568
+ this.log("Compression failed, using uncompressed");
569
+ }
570
+ return sendData({
571
+ url: this.endpoint,
572
+ data: json,
573
+ contentType: "application/json",
574
+ ...sendOptions
575
+ });
576
+ }
577
+ };
578
+ //#endregion
579
+ export { ReplayTracker as default };
@@ -4,6 +4,7 @@ interface WebVitalsOptions {
4
4
  baseUrl?: string;
5
5
  debug?: boolean;
6
6
  attribution?: boolean;
7
+ trackHash?: boolean;
7
8
  }
8
9
  declare class WebVitalsTracker {
9
10
  private readonly options;
@@ -12,14 +13,12 @@ declare class WebVitalsTracker {
12
13
  private started;
13
14
  private flushing;
14
15
  private initialUrl;
15
- private currentUrl;
16
+ private session;
16
17
  constructor(options: WebVitalsOptions);
17
18
  start(): Promise<void>;
18
- stop(): void;
19
+ stop(discard?: boolean): void;
19
20
  onPageHidden(persisted?: boolean): void;
20
- trackPageChange(url?: string): void;
21
21
  private onMetric;
22
- private getMetricUrl;
23
22
  private flush;
24
23
  }
25
24
  //#endregion