@faststats/web 0.9.0 → 0.10.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/README.md +31 -0
- package/dist/error.js +1 -1
- package/dist/replay.d.ts +10 -4
- package/dist/replay.js +40 -34
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -78,6 +78,37 @@ since the last delivered checkpoint. The collector must accept successive events
|
|
|
78
78
|
for the same visitor and URL; deploy the removal of its five-second debounce
|
|
79
79
|
before releasing this SDK.
|
|
80
80
|
|
|
81
|
+
### Network recording in session replay
|
|
82
|
+
|
|
83
|
+
Network recording is disabled by default. Enable it with
|
|
84
|
+
`sessionReplay({ recordNetwork: true })`. This records resource URLs and timing
|
|
85
|
+
data using rrweb's network plugin. Headers and bodies are not recorded by default.
|
|
86
|
+
FastStats collection endpoints (including a custom `baseUrl`) are always excluded
|
|
87
|
+
to prevent recording replay uploads recursively.
|
|
88
|
+
|
|
89
|
+
Use `networkOptions` to configure rrweb's plugin, including filtering or redaction:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
sessionReplay({
|
|
93
|
+
recordNetwork: true,
|
|
94
|
+
networkOptions: {
|
|
95
|
+
initiatorTypes: ["fetch", "xmlhttprequest"],
|
|
96
|
+
transformRequestFn: (request) => {
|
|
97
|
+
const url = new URL(request.name);
|
|
98
|
+
if (url.pathname.startsWith("/private/")) return;
|
|
99
|
+
url.search = "";
|
|
100
|
+
request.name = url.toString();
|
|
101
|
+
return request;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`recordHeaders`, `recordBody`, and `recordInitialRequests` can also be set in
|
|
108
|
+
`networkOptions`. Redact sensitive data before enabling headers or bodies.
|
|
109
|
+
Network options alone do not enable recording. The same replay options are
|
|
110
|
+
available through the React and Nuxt integrations.
|
|
111
|
+
|
|
81
112
|
### Custom extensions
|
|
82
113
|
|
|
83
114
|
An extension registers only the lifecycle hooks it needs. Hook failures are
|
package/dist/error.js
CHANGED
|
@@ -3,7 +3,7 @@ import { r as getSessionContext, t as sendData } from "./chunks/send-data-Dplz7m
|
|
|
3
3
|
import { t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
|
|
4
4
|
//#region src/error.ts
|
|
5
5
|
const SDK_NAME = "@faststats/web";
|
|
6
|
-
const SDK_VERSION = "0.
|
|
6
|
+
const SDK_VERSION = "0.10.0";
|
|
7
7
|
function errorTracking(options = {}) {
|
|
8
8
|
return {
|
|
9
9
|
name: "error-tracking",
|
package/dist/replay.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
|
|
2
2
|
import { record } from "@rrweb/record";
|
|
3
3
|
import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
|
|
4
|
-
|
|
4
|
+
import { NetworkRecordOptions } from "@rrweb/rrweb-plugin-network-record";
|
|
5
|
+
//#region ../../node_modules/.bun/@rrweb+types@2.1.6/node_modules/@rrweb/types/dist/index.d.ts
|
|
5
6
|
declare global {
|
|
6
7
|
interface Window {
|
|
7
8
|
FontFace: typeof FontFace;
|
|
@@ -45,6 +46,10 @@ export interface ReplayTrackerOptions {
|
|
|
45
46
|
consoleLevel?: LogLevel[];
|
|
46
47
|
consoleLengthThreshold?: number;
|
|
47
48
|
consoleStringLengthLimit?: number;
|
|
49
|
+
/** Opt in to network recording. Headers and bodies are off by default. */
|
|
50
|
+
recordNetwork?: boolean;
|
|
51
|
+
/** rrweb network options; only used when recordNetwork is true. */
|
|
52
|
+
networkOptions?: NetworkRecordOptions;
|
|
48
53
|
mutationThrottle?: ReplayMutationThrottleOptions;
|
|
49
54
|
}
|
|
50
55
|
export type SessionReplayExtensionOptions = Omit<ReplayTrackerOptions, "siteKey" | "baseUrl" | "debug" | "trackHash" | "cookieless">;
|
|
@@ -80,11 +85,10 @@ export default class ReplayTracker {
|
|
|
80
85
|
private stopRecording?;
|
|
81
86
|
private activeSessionId;
|
|
82
87
|
private sending;
|
|
83
|
-
private inFlightBatch;
|
|
84
88
|
private queuedFlush;
|
|
85
89
|
private unsubscribeRotation?;
|
|
86
90
|
private lastCookielessMode;
|
|
87
|
-
private
|
|
91
|
+
private needsFinalization;
|
|
88
92
|
private initialFlushPending;
|
|
89
93
|
private overflowed;
|
|
90
94
|
private replayEventSequence;
|
|
@@ -95,13 +99,14 @@ export default class ReplayTracker {
|
|
|
95
99
|
private clearQueues;
|
|
96
100
|
private dropMinLengthBlockedBatches;
|
|
97
101
|
private unblockSessionBatches;
|
|
98
|
-
private
|
|
102
|
+
private finalizeSession;
|
|
99
103
|
private nextBatchSequence;
|
|
100
104
|
setCookielessMode(cookieless: boolean): void;
|
|
101
105
|
start(): void;
|
|
102
106
|
trackPageChange(url?: string): void;
|
|
103
107
|
onPageHidden(persisted?: boolean, terminal?: boolean): void;
|
|
104
108
|
onPageShow(persisted?: boolean): void;
|
|
109
|
+
private recordingPlugins;
|
|
105
110
|
private beginRecording;
|
|
106
111
|
stop(discard?: boolean): void;
|
|
107
112
|
private onSessionRotated;
|
|
@@ -123,6 +128,7 @@ export default class ReplayTracker {
|
|
|
123
128
|
private removePending;
|
|
124
129
|
private enqueueBatch;
|
|
125
130
|
private flush;
|
|
131
|
+
private sendPending;
|
|
126
132
|
private scheduleRetry;
|
|
127
133
|
private retryDelay;
|
|
128
134
|
private sendBatch;
|
package/dist/replay.js
CHANGED
|
@@ -3,6 +3,7 @@ import { i as onSessionRotated, l as setStorageItem, n as createId, o as touchAc
|
|
|
3
3
|
import { t as getAnonymousId } from "./chunks/identifiers-xX7o7oZz.js";
|
|
4
4
|
import { record } from "@rrweb/record";
|
|
5
5
|
import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
|
|
6
|
+
import { getRecordNetworkPlugin } from "@rrweb/rrweb-plugin-network-record";
|
|
6
7
|
//#region src/replay-mutation-throttler.ts
|
|
7
8
|
const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
|
|
8
9
|
const MUTATION_SOURCE_TYPE = 0;
|
|
@@ -117,11 +118,10 @@ var ReplayTracker = class {
|
|
|
117
118
|
stopRecording;
|
|
118
119
|
activeSessionId = "";
|
|
119
120
|
sending = false;
|
|
120
|
-
inFlightBatch = null;
|
|
121
121
|
queuedFlush = null;
|
|
122
122
|
unsubscribeRotation;
|
|
123
123
|
lastCookielessMode;
|
|
124
|
-
|
|
124
|
+
needsFinalization = false;
|
|
125
125
|
initialFlushPending = true;
|
|
126
126
|
overflowed = false;
|
|
127
127
|
replayEventSequence = 0;
|
|
@@ -152,12 +152,14 @@ var ReplayTracker = class {
|
|
|
152
152
|
this.minLengthFlushTask = null;
|
|
153
153
|
}
|
|
154
154
|
clearQueues() {
|
|
155
|
+
this.needsFinalization = false;
|
|
155
156
|
this.events.length = 0;
|
|
156
157
|
this.queuedEventsSizeBytes = 0;
|
|
157
158
|
this.pending.length = 0;
|
|
158
159
|
this.pendingSizeBytes = 0;
|
|
159
160
|
}
|
|
160
161
|
dropMinLengthBlockedBatches() {
|
|
162
|
+
this.needsFinalization = false;
|
|
161
163
|
for (let index = this.pending.length - 1; index >= 0; index--) {
|
|
162
164
|
const batch = this.pending[index];
|
|
163
165
|
if (batch && this.minLengthBlockedBatches.has(batch)) this.removePending(batch);
|
|
@@ -166,18 +168,9 @@ var ReplayTracker = class {
|
|
|
166
168
|
unblockSessionBatches(sessionId) {
|
|
167
169
|
for (const batch of this.pending) if (batch.sessionId === sessionId) this.minLengthBlockedBatches.delete(batch);
|
|
168
170
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (batch === this.inFlightBatch) {
|
|
173
|
-
this.enqueueBatch(this.createBatch([], context, true, flushReason));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const previousSize = this.batchSizeBytes(batch);
|
|
177
|
-
batch.isFinal = true;
|
|
178
|
-
batch.flushReason = flushReason;
|
|
179
|
-
batchSizeCache.delete(batch);
|
|
180
|
-
this.pendingSizeBytes += this.batchSizeBytes(batch) - previousSize;
|
|
171
|
+
finalizeSession(context, flushReason) {
|
|
172
|
+
if (!this.needsFinalization) return;
|
|
173
|
+
this.enqueueBatch(this.createBatch([], context, true, flushReason));
|
|
181
174
|
}
|
|
182
175
|
nextBatchSequence(context) {
|
|
183
176
|
const key = `faststats_replay_batch_sequence_${this.options.siteKey}_${context.windowId}`;
|
|
@@ -204,7 +197,6 @@ var ReplayTracker = class {
|
|
|
204
197
|
if (this.started || typeof window === "undefined") return;
|
|
205
198
|
this.started = true;
|
|
206
199
|
this.disposed = false;
|
|
207
|
-
this.terminalFlushRequested = false;
|
|
208
200
|
this.initialFlushPending = true;
|
|
209
201
|
this.overflowed = false;
|
|
210
202
|
this.replayEventSequence = 0;
|
|
@@ -243,13 +235,13 @@ var ReplayTracker = class {
|
|
|
243
235
|
this.requestTerminalFlush("pageHidden");
|
|
244
236
|
return;
|
|
245
237
|
}
|
|
246
|
-
this.flush(
|
|
238
|
+
this.flush(true, false, "pageHidden");
|
|
247
239
|
}
|
|
248
240
|
onPageShow(persisted = false) {
|
|
249
241
|
if (!persisted || !this.started) return;
|
|
250
242
|
if (this.pending.length > 0) this.flush(false, false, "pageShow");
|
|
251
243
|
}
|
|
252
|
-
|
|
244
|
+
recordingPlugins() {
|
|
253
245
|
const plugins = [];
|
|
254
246
|
if (this.options.recordConsole === true) plugins.push(getRecordConsolePlugin({
|
|
255
247
|
level: this.options.consoleLevel ?? [
|
|
@@ -265,6 +257,25 @@ var ReplayTracker = class {
|
|
|
265
257
|
stringLengthLimit: this.options.consoleStringLengthLimit ?? 500
|
|
266
258
|
}
|
|
267
259
|
}));
|
|
260
|
+
if (this.options.recordNetwork === true) {
|
|
261
|
+
const options = this.options.networkOptions;
|
|
262
|
+
const base = resolveBaseUrl(this.options.baseUrl, ANALYTICS_BASE);
|
|
263
|
+
const endpoints = new Set(Object.values(URLS).map((path) => {
|
|
264
|
+
const url = new URL(`${base}${path}`, window.location.href);
|
|
265
|
+
return `${url.origin}${url.pathname}`;
|
|
266
|
+
}));
|
|
267
|
+
plugins.push(getRecordNetworkPlugin({
|
|
268
|
+
...options,
|
|
269
|
+
transformRequestFn: (request) => {
|
|
270
|
+
const url = new URL(request.name, window.location.href);
|
|
271
|
+
if (endpoints.has(`${url.origin}${url.pathname}`)) return;
|
|
272
|
+
return options?.transformRequestFn ? options.transformRequestFn(request) : request;
|
|
273
|
+
}
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
return plugins;
|
|
277
|
+
}
|
|
278
|
+
beginRecording() {
|
|
268
279
|
const stop = record({
|
|
269
280
|
emit: this.onEvent,
|
|
270
281
|
sampling: this.options.sampling ?? {
|
|
@@ -297,7 +308,7 @@ var ReplayTracker = class {
|
|
|
297
308
|
maskTextSelector: this.options.maskTextSelector,
|
|
298
309
|
checkoutEveryNms: this.options.checkoutEveryNms ?? 3e5,
|
|
299
310
|
checkoutEveryNth: this.options.checkoutEveryNth,
|
|
300
|
-
plugins
|
|
311
|
+
plugins: this.recordingPlugins()
|
|
301
312
|
});
|
|
302
313
|
this.stopRecording = stop;
|
|
303
314
|
}
|
|
@@ -342,8 +353,9 @@ var ReplayTracker = class {
|
|
|
342
353
|
isFinal: true,
|
|
343
354
|
flushReason: "sessionRotate"
|
|
344
355
|
});
|
|
345
|
-
this.
|
|
356
|
+
this.finalizeSession(prev, "sessionRotate");
|
|
346
357
|
this.unblockSessionBatches(prev.sessionId);
|
|
358
|
+
this.needsFinalization = false;
|
|
347
359
|
this.activeSessionId = next.sessionId;
|
|
348
360
|
this.resetSessionTiming(next);
|
|
349
361
|
this.beginReplayStream();
|
|
@@ -407,8 +419,6 @@ var ReplayTracker = class {
|
|
|
407
419
|
this.requestTerminalFlush("unload");
|
|
408
420
|
};
|
|
409
421
|
requestTerminalFlush(reason) {
|
|
410
|
-
if (this.terminalFlushRequested) return;
|
|
411
|
-
this.terminalFlushRequested = true;
|
|
412
422
|
if (!this.hasReachedMinLength()) {
|
|
413
423
|
this.events.length = 0;
|
|
414
424
|
this.queuedEventsSizeBytes = 0;
|
|
@@ -530,6 +540,7 @@ var ReplayTracker = class {
|
|
|
530
540
|
this.stopRecording = void 0;
|
|
531
541
|
return;
|
|
532
542
|
}
|
|
543
|
+
this.needsFinalization = !batch.isFinal;
|
|
533
544
|
this.pending.push(batch);
|
|
534
545
|
this.pendingSizeBytes += size;
|
|
535
546
|
}
|
|
@@ -542,12 +553,15 @@ var ReplayTracker = class {
|
|
|
542
553
|
flushReason,
|
|
543
554
|
maxBatchSizeBytes: lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes
|
|
544
555
|
});
|
|
545
|
-
if (isFinal) this.
|
|
556
|
+
if (isFinal) this.finalizeSession(context, flushReason);
|
|
546
557
|
if (this.pending.length === 0) return;
|
|
547
558
|
if (isFinal) for (const batch of this.pending) this.minLengthBlockedBatches.delete(batch);
|
|
548
559
|
else if (!rotation && flushReason !== "sessionRotate" && this.hasReachedMinLength()) this.unblockSessionBatches(context.sessionId);
|
|
560
|
+
await this.sendPending(lowLatency, lowLatency && isFinal);
|
|
561
|
+
}
|
|
562
|
+
async sendPending(lowLatency, bypassRetryDelay) {
|
|
549
563
|
if (this.retryTask) {
|
|
550
|
-
if (!
|
|
564
|
+
if (!bypassRetryDelay) return;
|
|
551
565
|
clearTimeout(this.retryTask);
|
|
552
566
|
this.retryTask = null;
|
|
553
567
|
}
|
|
@@ -555,8 +569,7 @@ var ReplayTracker = class {
|
|
|
555
569
|
const queued = this.queuedFlush;
|
|
556
570
|
this.queuedFlush = {
|
|
557
571
|
lowLatency: (queued?.lowLatency ?? false) || lowLatency,
|
|
558
|
-
|
|
559
|
-
flushReason: isFinal ? flushReason : queued?.flushReason ?? flushReason
|
|
572
|
+
bypassRetryDelay: (queued?.bypassRetryDelay ?? false) || bypassRetryDelay
|
|
560
573
|
};
|
|
561
574
|
return;
|
|
562
575
|
}
|
|
@@ -565,14 +578,7 @@ var ReplayTracker = class {
|
|
|
565
578
|
while (this.pending.length > 0) {
|
|
566
579
|
const batch = this.pending[0];
|
|
567
580
|
if (this.minLengthBlockedBatches.has(batch)) break;
|
|
568
|
-
this.
|
|
569
|
-
let sent;
|
|
570
|
-
try {
|
|
571
|
-
sent = await this.sendBatch(batch, lowLatency);
|
|
572
|
-
} finally {
|
|
573
|
-
if (this.inFlightBatch === batch) this.inFlightBatch = null;
|
|
574
|
-
}
|
|
575
|
-
if (!sent) {
|
|
581
|
+
if (!await this.sendBatch(batch, lowLatency)) {
|
|
576
582
|
this.log(`Failed to send replay batch ${batch.sequence}`);
|
|
577
583
|
if (!this.disposed) this.scheduleRetry();
|
|
578
584
|
break;
|
|
@@ -586,7 +592,7 @@ var ReplayTracker = class {
|
|
|
586
592
|
if (this.queuedFlush) {
|
|
587
593
|
const queued = this.queuedFlush;
|
|
588
594
|
this.queuedFlush = null;
|
|
589
|
-
this.
|
|
595
|
+
this.sendPending(queued.lowLatency, queued.bypassRetryDelay);
|
|
590
596
|
}
|
|
591
597
|
}
|
|
592
598
|
}
|
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
-
"version": "0.
|
|
45
|
+
"version": "0.10.0",
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsdown && bun run check-size",
|
|
48
48
|
"dev": "bun run build",
|
|
@@ -51,14 +51,15 @@
|
|
|
51
51
|
"check-size": "node scripts/check-bundle-size.mjs"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@rrweb/types": "^2.1.
|
|
54
|
+
"@rrweb/types": "^2.1.6",
|
|
55
55
|
"@types/bun": "^1.4.2",
|
|
56
56
|
"tsdown": "^0.23.0",
|
|
57
57
|
"typescript": "^7.0.2"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@rrweb/record": "^2.1.
|
|
61
|
-
"@rrweb/rrweb-plugin-console-record": "^2.1.
|
|
60
|
+
"@rrweb/record": "^2.1.6",
|
|
61
|
+
"@rrweb/rrweb-plugin-console-record": "^2.1.6",
|
|
62
|
+
"@rrweb/rrweb-plugin-network-record": "^2.1.6",
|
|
62
63
|
"web-vitals": "^6.2.1"
|
|
63
64
|
}
|
|
64
65
|
}
|