@faststats/web 0.13.0 → 0.13.2
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/chunks/{extensions-BfubKIzk.d.ts → extensions-C2k9Im6I.d.ts} +0 -2
- package/dist/chunks/identifiers-BHYhM7DM.js +15 -0
- package/dist/chunks/send-data-BCVUgwry.js +174 -0
- package/dist/chunks/session-manager-DBWYTyfd.js +114 -0
- package/dist/error.d.ts +4 -4
- package/dist/error.js +48 -61
- package/dist/flags.d.ts +2 -4
- package/dist/flags.js +3 -4
- package/dist/index.d.ts +2 -9
- package/dist/index.js +54 -127
- package/dist/replay.d.ts +38 -71
- package/dist/replay.js +424 -444
- package/dist/web-vitals.d.ts +2 -4
- package/dist/web-vitals.js +5 -22
- package/package.json +3 -3
- package/dist/chunks/identifiers-DOtve3E5.js +0 -18
- package/dist/chunks/send-data-C76HbZ8d.js +0 -96
- package/dist/chunks/session-manager-DUpF3mi3.js +0 -149
package/dist/replay.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as resolveBaseUrl, n as URLS, o as sanitizeUrl, t as sendData } from "./chunks/send-data-
|
|
2
|
-
import { a as touchActivity,
|
|
3
|
-
import { t as getAnonymousId } from "./chunks/identifiers-
|
|
1
|
+
import { a as resolveBaseUrl, c as trackerExtension, n as URLS, o as sanitizeUrl, t as sendData } from "./chunks/send-data-BCVUgwry.js";
|
|
2
|
+
import { a as touchActivity, n as getSessionContext, o as getStorageItem, r as onSessionRotated, s as setStorageItem, t as createId } from "./chunks/session-manager-DBWYTyfd.js";
|
|
3
|
+
import { t as getAnonymousId } from "./chunks/identifiers-BHYhM7DM.js";
|
|
4
4
|
import { record } from "@rrweb/record";
|
|
5
5
|
import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
|
|
6
6
|
import { getRecordNetworkPlugin } from "@rrweb/rrweb-plugin-network-record";
|
|
7
|
-
|
|
7
|
+
// src/replay-mutation-throttler.ts
|
|
8
8
|
const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
|
|
9
9
|
const MUTATION_SOURCE_TYPE = 0;
|
|
10
10
|
const MAX_TRACKED_NODES = 1e4;
|
|
@@ -51,8 +51,7 @@ var ReplayMutationThrottler = class {
|
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
-
|
|
55
|
-
//#region src/replay-sequence.ts
|
|
54
|
+
// src/replay-sequence.ts
|
|
56
55
|
const counters = /* @__PURE__ */ new Map();
|
|
57
56
|
function sameRecording(counter, context) {
|
|
58
57
|
return counter?.sessionId === context.sessionId && counter.windowId === context.windowId;
|
|
@@ -91,227 +90,336 @@ function nextReplaySequence(siteKey, context, cookieless) {
|
|
|
91
90
|
if (!cookieless) setStorageItem("sessionStorage", sequenceKey(siteKey), JSON.stringify(counter));
|
|
92
91
|
return sequence;
|
|
93
92
|
}
|
|
94
|
-
|
|
95
|
-
//#region src/replay.ts
|
|
96
|
-
const MAX_TIMEOUT_MS = 2147483647;
|
|
97
|
-
const DEFAULT_MAX_QUEUE_SIZE_BYTES = 2097152;
|
|
98
|
-
const DEFAULT_MAX_BATCH_SIZE_BYTES = 921600;
|
|
99
|
-
const KEEPALIVE_LIMIT_BYTES = 61440;
|
|
100
|
-
const DEFAULT_MAX_LOW_LATENCY_BATCH_SIZE_BYTES = 57344;
|
|
93
|
+
// src/replay-uploader.ts
|
|
101
94
|
const RETRY_BASE_DELAY_MS = 2e3;
|
|
102
95
|
const RETRY_MAX_ATTEMPTS = 5;
|
|
103
96
|
const RETRY_COOLDOWN_MS = 3e5;
|
|
104
|
-
const
|
|
105
|
-
const RRWEB_EVENT_FULL_SNAPSHOT = 2;
|
|
106
|
-
const RRWEB_EVENT_META = 4;
|
|
107
|
-
const REPLAY_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
|
|
97
|
+
const KEEPALIVE_LIMIT_BYTES = 61440;
|
|
108
98
|
const textEncoder = new TextEncoder();
|
|
109
|
-
const
|
|
110
|
-
|
|
99
|
+
const sizeCache = /* @__PURE__ */ new WeakMap();
|
|
100
|
+
/** UTF-8 size of `value` as JSON, cached since queued values never change. */
|
|
101
|
+
function jsonBytes(value) {
|
|
102
|
+
let size = sizeCache.get(value);
|
|
103
|
+
if (size === void 0) {
|
|
104
|
+
size = textEncoder.encode(JSON.stringify(value)).byteLength;
|
|
105
|
+
sizeCache.set(value, size);
|
|
106
|
+
}
|
|
107
|
+
return size;
|
|
108
|
+
}
|
|
109
|
+
function retryDelay(attempt) {
|
|
110
|
+
if (attempt >= RETRY_MAX_ATTEMPTS) return RETRY_COOLDOWN_MS;
|
|
111
|
+
const ceiling = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
112
|
+
return Math.round(ceiling * (.5 + Math.random() * .5));
|
|
113
|
+
}
|
|
111
114
|
async function gzip(json) {
|
|
112
115
|
const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
113
116
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
114
117
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
var ReplayUploader = class {
|
|
119
|
+
options;
|
|
120
|
+
queue = [];
|
|
121
|
+
/** Bytes of every queued batch, which count against the replay budget. */
|
|
122
|
+
bytes = 0;
|
|
123
|
+
sending;
|
|
124
|
+
lowLatency = false;
|
|
125
|
+
retryTimer;
|
|
126
|
+
attempt = 0;
|
|
127
|
+
constructor(options) {
|
|
128
|
+
this.options = options;
|
|
129
|
+
}
|
|
130
|
+
get length() {
|
|
131
|
+
return this.queue.length;
|
|
132
|
+
}
|
|
133
|
+
add(batch) {
|
|
134
|
+
this.queue.push(batch);
|
|
135
|
+
this.bytes += jsonBytes(batch);
|
|
136
|
+
}
|
|
137
|
+
clear() {
|
|
138
|
+
this.queue.length = 0;
|
|
139
|
+
this.bytes = 0;
|
|
140
|
+
this.cancelRetry();
|
|
141
|
+
}
|
|
142
|
+
cancelRetry() {
|
|
143
|
+
clearTimeout(this.retryTimer);
|
|
144
|
+
this.retryTimer = void 0;
|
|
145
|
+
this.attempt = 0;
|
|
146
|
+
}
|
|
147
|
+
/** Runs a scheduled retry now, keeping the backoff of earlier failures. */
|
|
148
|
+
retryNow() {
|
|
149
|
+
clearTimeout(this.retryTimer);
|
|
150
|
+
this.retryTimer = void 0;
|
|
151
|
+
if (this.attempt === RETRY_MAX_ATTEMPTS) this.attempt = 0;
|
|
152
|
+
return this.send(false);
|
|
153
|
+
}
|
|
154
|
+
/** Sends the queue in order. Waits out a scheduled retry instead. */
|
|
155
|
+
send(lowLatency) {
|
|
156
|
+
if (!this.sending && (!this.queue.length || this.retryTimer)) return Promise.resolve();
|
|
157
|
+
this.lowLatency ||= lowLatency;
|
|
158
|
+
this.sending ??= this.drain();
|
|
159
|
+
return this.sending;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Hands every batch to the browser at once, newest first, since the page may
|
|
163
|
+
* be gone before an awaited request settles. Resending a batch that is
|
|
164
|
+
* already in flight is idempotent on the server.
|
|
165
|
+
*/
|
|
166
|
+
sendAll() {
|
|
167
|
+
for (const batch of [...this.queue].reverse()) this.post(batch, true).then((sent) => sent && this.remove(batch));
|
|
168
|
+
}
|
|
169
|
+
async drain() {
|
|
170
|
+
try {
|
|
171
|
+
for (let batch = this.queue[0]; batch; batch = this.queue[0]) {
|
|
172
|
+
if (!await this.post(batch, this.lowLatency)) {
|
|
173
|
+
this.options.log(`Failed to send replay batch ${batch.sequence}`);
|
|
174
|
+
if (this.options.canRetry()) this.scheduleRetry();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.attempt = 0;
|
|
178
|
+
this.remove(batch);
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
this.sending = void 0;
|
|
182
|
+
this.lowLatency = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
scheduleRetry() {
|
|
186
|
+
this.attempt = Math.min(this.attempt + 1, RETRY_MAX_ATTEMPTS);
|
|
187
|
+
const delay = retryDelay(this.attempt);
|
|
188
|
+
this.options.log(`Retrying replay delivery in ${delay}ms`);
|
|
189
|
+
this.retryTimer = setTimeout(() => void this.retryNow(), delay);
|
|
190
|
+
}
|
|
191
|
+
remove(batch) {
|
|
192
|
+
const index = this.queue.indexOf(batch);
|
|
193
|
+
if (index < 0) return;
|
|
194
|
+
this.queue.splice(index, 1);
|
|
195
|
+
this.bytes -= jsonBytes(batch);
|
|
196
|
+
this.options.log(`Sent replay batch ${batch.sequence}`);
|
|
197
|
+
}
|
|
198
|
+
async post(batch, lowLatency) {
|
|
199
|
+
const json = JSON.stringify(batch);
|
|
200
|
+
const keepalive = lowLatency && jsonBytes(batch) <= KEEPALIVE_LIMIT_BYTES;
|
|
201
|
+
let data = json;
|
|
202
|
+
let contentType = "application/json";
|
|
203
|
+
let query = "";
|
|
204
|
+
if (!lowLatency && this.options.compress) try {
|
|
205
|
+
data = await gzip(json);
|
|
206
|
+
contentType = "application/octet-stream";
|
|
207
|
+
query = "?encoding=gzip";
|
|
208
|
+
} catch {
|
|
209
|
+
this.options.log("Compression failed, using uncompressed");
|
|
210
|
+
}
|
|
211
|
+
return sendData({
|
|
212
|
+
url: `${this.options.endpoint}${query}`,
|
|
213
|
+
data,
|
|
214
|
+
contentType,
|
|
215
|
+
debug: this.options.debug,
|
|
216
|
+
debugPrefix: "[Replay]",
|
|
217
|
+
useBeacon: keepalive,
|
|
218
|
+
keepalive,
|
|
219
|
+
settleRejected: true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
// src/replay.ts
|
|
224
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
225
|
+
const LOW_LATENCY_BATCH_BYTES = 57344;
|
|
226
|
+
const LIMITS = {
|
|
227
|
+
flushInterval: [
|
|
228
|
+
5e3,
|
|
229
|
+
1,
|
|
230
|
+
MAX_TIMEOUT_MS
|
|
231
|
+
],
|
|
232
|
+
maxEvents: [
|
|
233
|
+
1e3,
|
|
234
|
+
1,
|
|
235
|
+
1e5
|
|
236
|
+
],
|
|
237
|
+
maxBatchSizeBytes: [
|
|
238
|
+
921600,
|
|
239
|
+
1,
|
|
240
|
+
33554432
|
|
241
|
+
],
|
|
242
|
+
maxLowLatencyBatchSizeBytes: [
|
|
243
|
+
LOW_LATENCY_BATCH_BYTES,
|
|
244
|
+
1,
|
|
245
|
+
LOW_LATENCY_BATCH_BYTES
|
|
246
|
+
],
|
|
247
|
+
maxPendingBatches: [
|
|
248
|
+
30,
|
|
249
|
+
1,
|
|
250
|
+
1e3
|
|
251
|
+
],
|
|
252
|
+
maxQueueSizeBytes: [
|
|
253
|
+
2097152,
|
|
254
|
+
1,
|
|
255
|
+
33554432
|
|
256
|
+
],
|
|
257
|
+
minReplayLengthMs: [
|
|
258
|
+
3e3,
|
|
259
|
+
0,
|
|
260
|
+
MAX_TIMEOUT_MS
|
|
261
|
+
]
|
|
262
|
+
};
|
|
263
|
+
const RRWEB_FULL_SNAPSHOT = 2;
|
|
264
|
+
const RRWEB_META = 4;
|
|
265
|
+
const RRWEB_CUSTOM = 5;
|
|
266
|
+
const SEQUENCE_KEY = "_faststatsSeqId";
|
|
267
|
+
function limits(options) {
|
|
268
|
+
const entries = Object.entries(LIMITS).map(([name, [fallback, min, max]]) => {
|
|
269
|
+
const value = options[name];
|
|
270
|
+
return [name, value === void 0 || !Number.isFinite(value) || value < min ? fallback : Math.min(Math.trunc(value), max)];
|
|
271
|
+
});
|
|
272
|
+
return Object.fromEntries(entries);
|
|
273
|
+
}
|
|
274
|
+
/** Splits events into chunks of at most `maxBytes`, keeping oversized ones whole. */
|
|
275
|
+
function chunk(events, maxBytes) {
|
|
276
|
+
const chunks = [];
|
|
277
|
+
let current = [];
|
|
278
|
+
let size = 0;
|
|
279
|
+
for (const event of events) {
|
|
280
|
+
const bytes = jsonBytes(event) + 1;
|
|
281
|
+
if (!current.length || size + bytes > maxBytes) {
|
|
282
|
+
current = [];
|
|
283
|
+
chunks.push(current);
|
|
284
|
+
size = 0;
|
|
285
|
+
}
|
|
286
|
+
current.push(event);
|
|
287
|
+
size += bytes;
|
|
288
|
+
}
|
|
289
|
+
return chunks;
|
|
118
290
|
}
|
|
119
291
|
function sessionReplay(options = {}) {
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
debug: context.debug,
|
|
128
|
-
trackHash: context.trackHash,
|
|
129
|
-
cookieless: context.isCookieless()
|
|
130
|
-
});
|
|
131
|
-
context.on("pageChange", ({ url }) => tracker.trackPageChange(url));
|
|
132
|
-
context.on("pageHide", ({ persisted, terminal }) => tracker.onPageHidden(persisted, terminal));
|
|
133
|
-
context.on("pageShow", ({ persisted }) => tracker.onPageShow(persisted));
|
|
134
|
-
context.on("consentChange", ({ cookieless }) => tracker.setCookielessMode(cookieless));
|
|
135
|
-
tracker.start();
|
|
136
|
-
return ({ discard }) => tracker.stop(discard);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
292
|
+
return trackerExtension("session-replay", (context) => new ReplayTracker({
|
|
293
|
+
...options,
|
|
294
|
+
...context
|
|
295
|
+
}), (tracker, context) => {
|
|
296
|
+
context.on("pageChange", ({ url }) => tracker.trackPageChange(url));
|
|
297
|
+
context.on("pageShow", ({ persisted }) => tracker.onPageShow(persisted));
|
|
298
|
+
});
|
|
139
299
|
}
|
|
140
300
|
var ReplayTracker = class {
|
|
141
301
|
options;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
maxBatchSizeBytes;
|
|
147
|
-
maxLowLatencyBatchSizeBytes;
|
|
148
|
-
maxPendingBatches;
|
|
149
|
-
maxQueueSizeBytes;
|
|
150
|
-
minReplayLengthMs;
|
|
151
|
-
events = [];
|
|
152
|
-
pending = [];
|
|
153
|
-
queuedEventsSizeBytes = 0;
|
|
154
|
-
pendingSizeBytes = 0;
|
|
155
|
-
viewId = createId();
|
|
156
|
-
viewUrl = "";
|
|
157
|
-
chunkStartedAt = 0;
|
|
302
|
+
limits;
|
|
303
|
+
uploader;
|
|
304
|
+
throttler;
|
|
305
|
+
cookieless;
|
|
158
306
|
started = false;
|
|
159
|
-
|
|
160
|
-
startTime = 0;
|
|
161
|
-
intervalId = null;
|
|
162
|
-
flushTask = null;
|
|
163
|
-
retryTask = null;
|
|
164
|
-
retryAttempt = 0;
|
|
165
|
-
minLengthFlushTask = null;
|
|
307
|
+
overflowed = false;
|
|
166
308
|
stopRecording;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
309
|
+
stopFollowing;
|
|
310
|
+
intervalTimer;
|
|
311
|
+
flushTimer;
|
|
312
|
+
minLengthTimer;
|
|
313
|
+
viewId = createId();
|
|
314
|
+
viewUrl = "";
|
|
315
|
+
session;
|
|
316
|
+
streamStart = 0;
|
|
317
|
+
chunkStart = 0;
|
|
318
|
+
sequence = 0;
|
|
319
|
+
events = [];
|
|
320
|
+
eventBytes = 0;
|
|
174
321
|
resumed = false;
|
|
175
|
-
|
|
176
|
-
replayEventSequence = 0;
|
|
177
|
-
mutationThrottler;
|
|
322
|
+
unfinalized = false;
|
|
178
323
|
constructor(options) {
|
|
179
324
|
this.options = options;
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
this.mutationThrottler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
|
|
191
|
-
}
|
|
192
|
-
log(...args) {
|
|
193
|
-
if (this.options.debug) console.log("[Replay]", ...args);
|
|
194
|
-
}
|
|
195
|
-
sessionContext() {
|
|
196
|
-
return getSessionContext(this.options.siteKey, this.lastCookielessMode);
|
|
197
|
-
}
|
|
198
|
-
sanitize(url) {
|
|
199
|
-
return sanitizeUrl(url, this.options.trackHash ?? false);
|
|
200
|
-
}
|
|
201
|
-
clearFlushTimers() {
|
|
202
|
-
if (this.flushTask) clearTimeout(this.flushTask);
|
|
203
|
-
if (this.retryTask) clearTimeout(this.retryTask);
|
|
204
|
-
if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
|
|
205
|
-
this.flushTask = null;
|
|
206
|
-
this.retryTask = null;
|
|
207
|
-
this.retryAttempt = 0;
|
|
208
|
-
this.minLengthFlushTask = null;
|
|
209
|
-
}
|
|
210
|
-
dropEvents() {
|
|
211
|
-
this.events.length = 0;
|
|
212
|
-
this.queuedEventsSizeBytes = 0;
|
|
213
|
-
}
|
|
214
|
-
clearQueues() {
|
|
215
|
-
this.needsFinalization = false;
|
|
216
|
-
this.dropEvents();
|
|
217
|
-
this.pending.length = 0;
|
|
218
|
-
this.pendingSizeBytes = 0;
|
|
219
|
-
}
|
|
220
|
-
finalizeSession(context, flushReason) {
|
|
221
|
-
if (!this.needsFinalization) return;
|
|
222
|
-
const batch = this.createBatch([], context, true, flushReason);
|
|
223
|
-
if (batch) this.enqueueBatch(batch);
|
|
224
|
-
}
|
|
225
|
-
setCookielessMode(cookieless) {
|
|
226
|
-
if (this.lastCookielessMode === cookieless) return;
|
|
227
|
-
this.lastCookielessMode = cookieless;
|
|
228
|
-
if (this.started) this.subscribeToSessionRotation();
|
|
229
|
-
this.clearQueues();
|
|
230
|
-
this.clearFlushTimers();
|
|
231
|
-
const context = this.sessionContext();
|
|
232
|
-
this.activeSessionId = context.sessionId;
|
|
233
|
-
this.resetSessionTiming(context);
|
|
234
|
-
this.beginReplayStream();
|
|
325
|
+
this.cookieless = options.cookieless ?? false;
|
|
326
|
+
this.limits = limits(options);
|
|
327
|
+
this.uploader = new ReplayUploader({
|
|
328
|
+
endpoint: `${resolveBaseUrl(options.baseUrl)}${URLS.replay}`,
|
|
329
|
+
compress: (options.compress ?? true) && typeof window !== "undefined" && "CompressionStream" in window,
|
|
330
|
+
debug: options.debug,
|
|
331
|
+
canRetry: () => this.started,
|
|
332
|
+
log: (...details) => this.log(...details)
|
|
333
|
+
});
|
|
334
|
+
this.throttler = options.mutationThrottle?.enabled === false ? null : new ReplayMutationThrottler(options.mutationThrottle);
|
|
235
335
|
}
|
|
236
336
|
start() {
|
|
237
337
|
if (this.started || typeof window === "undefined") return;
|
|
238
338
|
this.started = true;
|
|
239
|
-
this.disposed = false;
|
|
240
|
-
this.initialFlushPending = true;
|
|
241
339
|
this.overflowed = false;
|
|
242
|
-
this.replayEventSequence = 0;
|
|
243
340
|
this.viewId = createId();
|
|
244
341
|
this.viewUrl = this.sanitize(window.location.href);
|
|
245
|
-
|
|
246
|
-
this.
|
|
247
|
-
this.resetSessionTiming(context);
|
|
248
|
-
this.subscribeToSessionRotation();
|
|
342
|
+
this.beginStream(this.currentSession());
|
|
343
|
+
this.followSession();
|
|
249
344
|
this.beginRecording();
|
|
250
|
-
this.
|
|
345
|
+
this.intervalTimer = setInterval(() => this.requestFlush("interval"), this.limits.flushInterval);
|
|
251
346
|
this.log("Recording started");
|
|
252
347
|
}
|
|
348
|
+
stop(discard = false) {
|
|
349
|
+
if (!this.started) return;
|
|
350
|
+
this.started = false;
|
|
351
|
+
this.stopFollowing?.();
|
|
352
|
+
this.stopRecording?.();
|
|
353
|
+
this.stopRecording = void 0;
|
|
354
|
+
clearInterval(this.intervalTimer);
|
|
355
|
+
clearTimeout(this.flushTimer);
|
|
356
|
+
clearTimeout(this.minLengthTimer);
|
|
357
|
+
this.flushTimer = void 0;
|
|
358
|
+
this.uploader.cancelRetry();
|
|
359
|
+
if (discard) {
|
|
360
|
+
this.dropEvents();
|
|
361
|
+
this.uploader.clear();
|
|
362
|
+
this.log("Recording discarded");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.finish("stop");
|
|
366
|
+
this.log("Recording stopped");
|
|
367
|
+
}
|
|
368
|
+
/** Nothing captured under the old identity may be sent under the new one. */
|
|
369
|
+
setCookielessMode(cookieless) {
|
|
370
|
+
if (this.cookieless === cookieless) return;
|
|
371
|
+
this.cookieless = cookieless;
|
|
372
|
+
clearTimeout(this.flushTimer);
|
|
373
|
+
this.flushTimer = void 0;
|
|
374
|
+
this.uploader.clear();
|
|
375
|
+
this.dropEvents();
|
|
376
|
+
this.unfinalized = false;
|
|
377
|
+
if (!this.started) return;
|
|
378
|
+
this.beginStream(this.currentSession());
|
|
379
|
+
this.followSession();
|
|
380
|
+
this.takeFullSnapshot();
|
|
381
|
+
}
|
|
253
382
|
trackPageChange(url) {
|
|
254
383
|
if (!this.started) return;
|
|
255
384
|
this.viewId = createId();
|
|
256
|
-
|
|
257
|
-
this.viewUrl = href;
|
|
385
|
+
this.viewUrl = this.sanitize(url ?? window.location.href);
|
|
258
386
|
this.onEvent({
|
|
259
|
-
type:
|
|
387
|
+
type: RRWEB_CUSTOM,
|
|
260
388
|
timestamp: Date.now(),
|
|
261
389
|
data: {
|
|
262
390
|
tag: "faststats:view",
|
|
263
391
|
payload: {
|
|
264
|
-
href,
|
|
392
|
+
href: this.viewUrl,
|
|
265
393
|
viewId: this.viewId
|
|
266
394
|
}
|
|
267
395
|
}
|
|
268
396
|
});
|
|
269
397
|
}
|
|
270
398
|
onPageHidden(persisted = false, terminal = false) {
|
|
271
|
-
if (persisted) return;
|
|
272
|
-
if (terminal)
|
|
273
|
-
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
this.flush(true, false, "pageHidden");
|
|
399
|
+
if (persisted || !this.started) return;
|
|
400
|
+
if (terminal) this.finish("pageHidden");
|
|
401
|
+
else this.flush("pageHidden", true);
|
|
277
402
|
}
|
|
278
403
|
onPageShow(persisted = false) {
|
|
279
404
|
if (!persisted || !this.started) return;
|
|
280
|
-
|
|
405
|
+
this.uploader.retryNow();
|
|
281
406
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
})
|
|
298
|
-
if (this.options.recordNetwork === true) {
|
|
299
|
-
const options = this.options.networkOptions;
|
|
300
|
-
const base = resolveBaseUrl(this.options.baseUrl);
|
|
301
|
-
const endpoints = new Set(Object.values(URLS).map((path) => {
|
|
302
|
-
const url = new URL(`${base}${path}`, window.location.href);
|
|
303
|
-
return `${url.origin}${url.pathname}`;
|
|
304
|
-
}));
|
|
305
|
-
plugins.push(getRecordNetworkPlugin({
|
|
306
|
-
...options,
|
|
307
|
-
transformRequestFn: (request) => {
|
|
308
|
-
const url = new URL(request.name, window.location.href);
|
|
309
|
-
if (endpoints.has(`${url.origin}${url.pathname}`)) return;
|
|
310
|
-
return options?.transformRequestFn ? options.transformRequestFn(request) : request;
|
|
311
|
-
}
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
return plugins;
|
|
407
|
+
log(...details) {
|
|
408
|
+
if (this.options.debug) console.log("[Replay]", ...details);
|
|
409
|
+
}
|
|
410
|
+
sanitize(url) {
|
|
411
|
+
return sanitizeUrl(url, this.options.trackHash ?? false);
|
|
412
|
+
}
|
|
413
|
+
currentSession() {
|
|
414
|
+
return getSessionContext(this.options.siteKey, this.cookieless);
|
|
415
|
+
}
|
|
416
|
+
followSession() {
|
|
417
|
+
this.stopFollowing?.();
|
|
418
|
+
this.stopFollowing = onSessionRotated(this.options.siteKey, this.cookieless, (_prev, next) => {
|
|
419
|
+
if (!this.started) return;
|
|
420
|
+
this.rotate(next);
|
|
421
|
+
if (this.uploader.length) queueMicrotask(() => void this.flush("sessionRotate"));
|
|
422
|
+
});
|
|
315
423
|
}
|
|
316
424
|
beginRecording() {
|
|
317
425
|
this.stopRecording = record({
|
|
@@ -349,46 +457,42 @@ var ReplayTracker = class {
|
|
|
349
457
|
plugins: this.recordingPlugins()
|
|
350
458
|
});
|
|
351
459
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
460
|
+
recordingPlugins() {
|
|
461
|
+
const plugins = [];
|
|
462
|
+
if (this.options.recordConsole === true) plugins.push(getRecordConsolePlugin({
|
|
463
|
+
level: this.options.consoleLevel ?? [
|
|
464
|
+
"error",
|
|
465
|
+
"warn",
|
|
466
|
+
"info",
|
|
467
|
+
"log"
|
|
468
|
+
],
|
|
469
|
+
lengthThreshold: this.options.consoleLengthThreshold ?? 200,
|
|
470
|
+
stringifyOptions: {
|
|
471
|
+
numOfKeysLimit: 25,
|
|
472
|
+
depthOfLimit: 3,
|
|
473
|
+
stringLengthLimit: this.options.consoleStringLengthLimit ?? 500
|
|
474
|
+
}
|
|
475
|
+
}));
|
|
476
|
+
if (this.options.recordNetwork === true) {
|
|
477
|
+
const options = this.options.networkOptions;
|
|
478
|
+
const base = resolveBaseUrl(this.options.baseUrl);
|
|
479
|
+
const route = (url) => {
|
|
480
|
+
const { origin, pathname } = new URL(url, window.location.href);
|
|
481
|
+
return origin + pathname;
|
|
482
|
+
};
|
|
483
|
+
const endpoints = new Set(Object.values(URLS).map((path) => route(base + path)));
|
|
484
|
+
plugins.push(getRecordNetworkPlugin({
|
|
485
|
+
...options,
|
|
486
|
+
transformRequestFn: (request) => {
|
|
487
|
+
if (endpoints.has(route(request.name))) return;
|
|
488
|
+
const transform = options?.transformRequestFn;
|
|
489
|
+
return transform ? transform(request) : request;
|
|
490
|
+
}
|
|
491
|
+
}));
|
|
367
492
|
}
|
|
368
|
-
|
|
369
|
-
this.log("Recording stopped");
|
|
493
|
+
return plugins;
|
|
370
494
|
}
|
|
371
|
-
|
|
372
|
-
if (!this.started) return;
|
|
373
|
-
this.applySessionRotation(prev, next);
|
|
374
|
-
if (this.pending.length > 0) queueMicrotask(() => void this.flush(false, false, "sessionRotate"));
|
|
375
|
-
}
|
|
376
|
-
subscribeToSessionRotation() {
|
|
377
|
-
this.unsubscribeRotation?.();
|
|
378
|
-
this.unsubscribeRotation = onSessionRotated(this.options.siteKey, this.lastCookielessMode, (prev, next) => this.onSessionRotated(prev, next));
|
|
379
|
-
}
|
|
380
|
-
applySessionRotation(prev, next) {
|
|
381
|
-
if (this.activeSessionId === next.sessionId) return;
|
|
382
|
-
if (this.hasReachedMinLength()) this.enqueueEventsIfReady(prev, "sessionRotate");
|
|
383
|
-
else this.dropEvents();
|
|
384
|
-
this.finalizeSession(prev, "sessionRotate");
|
|
385
|
-
this.needsFinalization = false;
|
|
386
|
-
this.activeSessionId = next.sessionId;
|
|
387
|
-
this.resetSessionTiming(next);
|
|
388
|
-
this.beginReplayStream();
|
|
389
|
-
}
|
|
390
|
-
beginReplayStream() {
|
|
391
|
-
this.replayEventSequence = 0;
|
|
495
|
+
takeFullSnapshot() {
|
|
392
496
|
const stopRecording = this.stopRecording;
|
|
393
497
|
if (!stopRecording) return;
|
|
394
498
|
try {
|
|
@@ -397,7 +501,6 @@ var ReplayTracker = class {
|
|
|
397
501
|
this.stopRecording = void 0;
|
|
398
502
|
try {
|
|
399
503
|
stopRecording();
|
|
400
|
-
this.replayEventSequence = 0;
|
|
401
504
|
this.beginRecording();
|
|
402
505
|
} catch (restartError) {
|
|
403
506
|
this.log("Failed to restart replay stream", {
|
|
@@ -407,262 +510,139 @@ var ReplayTracker = class {
|
|
|
407
510
|
}
|
|
408
511
|
}
|
|
409
512
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
this.
|
|
413
|
-
this.
|
|
414
|
-
this.
|
|
415
|
-
this.chunkStartedAt = Date.now();
|
|
416
|
-
this.initialFlushPending = true;
|
|
513
|
+
stopCapture(reason) {
|
|
514
|
+
this.log(reason);
|
|
515
|
+
this.overflowed = true;
|
|
516
|
+
this.stopRecording?.();
|
|
517
|
+
this.stopRecording = void 0;
|
|
417
518
|
}
|
|
418
519
|
onEvent = (event) => {
|
|
419
520
|
if (this.overflowed) return;
|
|
420
|
-
|
|
421
|
-
if (event.type ===
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
...
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const throttled = this.mutationThrottler?.throttle(capturedEvent);
|
|
434
|
-
if (this.mutationThrottler && !throttled) return;
|
|
435
|
-
if (throttled) capturedEvent = throttled;
|
|
521
|
+
const { data } = event;
|
|
522
|
+
if (event.type === RRWEB_META && typeof data?.href === "string") event = {
|
|
523
|
+
...event,
|
|
524
|
+
data: {
|
|
525
|
+
...data,
|
|
526
|
+
href: this.sanitize(data.href)
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
if (event.type === RRWEB_FULL_SNAPSHOT) this.throttler?.reset();
|
|
530
|
+
else if (this.throttler) {
|
|
531
|
+
const throttled = this.throttler.throttle(event);
|
|
532
|
+
if (!throttled) return;
|
|
533
|
+
event = throttled;
|
|
436
534
|
}
|
|
437
|
-
const
|
|
438
|
-
...
|
|
439
|
-
[
|
|
535
|
+
const sequenced = {
|
|
536
|
+
...event,
|
|
537
|
+
[SEQUENCE_KEY]: ++this.sequence
|
|
440
538
|
};
|
|
441
|
-
const
|
|
442
|
-
if (this.
|
|
539
|
+
const bytes = jsonBytes(sequenced) + 1;
|
|
540
|
+
if (this.uploader.bytes + this.eventBytes + bytes > this.limits.maxQueueSizeBytes) {
|
|
443
541
|
this.stopCapture("Replay buffer limit reached");
|
|
444
542
|
return;
|
|
445
543
|
}
|
|
446
|
-
this.events.push(
|
|
447
|
-
this.
|
|
448
|
-
|
|
449
|
-
if (this.
|
|
450
|
-
else if (this.queuedEventsSizeBytes >= this.maxBatchSizeBytes) reason = "maxBytes";
|
|
451
|
-
if (reason) this.requestFlush(reason);
|
|
452
|
-
else this.scheduleMinLengthFlush();
|
|
544
|
+
this.events.push(sequenced);
|
|
545
|
+
this.eventBytes += bytes;
|
|
546
|
+
if (this.events.length >= this.limits.maxEvents) this.requestFlush("maxEvents");
|
|
547
|
+
else if (this.eventBytes >= this.limits.maxBatchSizeBytes) this.requestFlush("maxBytes");
|
|
453
548
|
};
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.
|
|
549
|
+
beginStream(session) {
|
|
550
|
+
this.session = session;
|
|
551
|
+
this.dropEvents();
|
|
552
|
+
this.sequence = 0;
|
|
553
|
+
this.unfinalized = false;
|
|
554
|
+
this.resumed = hasReplaySequence(this.options.siteKey, session, this.cookieless);
|
|
555
|
+
this.streamStart = this.chunkStart = Date.now();
|
|
556
|
+
clearTimeout(this.minLengthTimer);
|
|
557
|
+
if (this.started && !this.reachedMinLength()) this.minLengthTimer = setTimeout(() => this.requestFlush("minLength"), this.limits.minReplayLengthMs);
|
|
558
|
+
}
|
|
559
|
+
rotate(next) {
|
|
560
|
+
if (next.sessionId === this.session.sessionId) return;
|
|
561
|
+
this.batchEvents("sessionRotate");
|
|
562
|
+
this.endStream("sessionRotate");
|
|
563
|
+
this.beginStream(next);
|
|
564
|
+
this.takeFullSnapshot();
|
|
565
|
+
}
|
|
566
|
+
reachedMinLength() {
|
|
567
|
+
return this.resumed || Date.now() - this.streamStart >= this.limits.minReplayLengthMs;
|
|
462
568
|
}
|
|
463
|
-
|
|
464
|
-
|
|
569
|
+
dropEvents() {
|
|
570
|
+
this.events = [];
|
|
571
|
+
this.eventBytes = 0;
|
|
465
572
|
}
|
|
466
573
|
requestFlush(reason) {
|
|
467
|
-
if (this.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
scheduleMinLengthFlush() {
|
|
478
|
-
if (!this.initialFlushPending || this.events.length === 0 || this.minReplayLengthMs <= 0) return;
|
|
479
|
-
if (this.hasReachedMinLength()) {
|
|
480
|
-
this.initialFlushPending = false;
|
|
481
|
-
this.requestFlush("minLength");
|
|
482
|
-
return;
|
|
574
|
+
if (this.flushTimer || !this.events.length) return;
|
|
575
|
+
this.flushTimer = setTimeout(() => {
|
|
576
|
+
this.flushTimer = void 0;
|
|
577
|
+
this.flush(reason);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
async flush(reason, lowLatency = false) {
|
|
581
|
+
if (this.events.length) {
|
|
582
|
+
this.rotate(touchActivity(this.options.siteKey, this.cookieless));
|
|
583
|
+
this.batchEvents(reason, lowLatency);
|
|
483
584
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
this.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
this.scheduleMinLengthFlush();
|
|
495
|
-
return;
|
|
585
|
+
await this.uploader.send(lowLatency);
|
|
586
|
+
}
|
|
587
|
+
finish(reason) {
|
|
588
|
+
if (this.reachedMinLength()) {
|
|
589
|
+
if (this.events.length) this.rotate(touchActivity(this.options.siteKey, this.cookieless));
|
|
590
|
+
this.batchEvents(reason, true);
|
|
591
|
+
this.endStream(reason);
|
|
592
|
+
} else {
|
|
593
|
+
this.log(`Recording too short (${Date.now() - this.streamStart}ms), discarding events`);
|
|
594
|
+
this.dropEvents();
|
|
496
595
|
}
|
|
497
|
-
|
|
498
|
-
|
|
596
|
+
this.uploader.sendAll();
|
|
597
|
+
}
|
|
598
|
+
batchEvents(reason, lowLatency = false) {
|
|
599
|
+
if (!this.events.length || !this.reachedMinLength()) return;
|
|
600
|
+
const maxBytes = lowLatency ? this.limits.maxLowLatencyBatchSizeBytes : this.limits.maxBatchSizeBytes;
|
|
601
|
+
const events = this.events;
|
|
602
|
+
this.dropEvents();
|
|
603
|
+
for (const batch of chunk(events, maxBytes)) {
|
|
604
|
+
if (this.uploader.length >= this.limits.maxPendingBatches) {
|
|
499
605
|
this.stopCapture("Replay queue limit reached");
|
|
500
|
-
|
|
606
|
+
return;
|
|
501
607
|
}
|
|
502
|
-
|
|
503
|
-
if (!batch) break;
|
|
504
|
-
this.enqueueBatch(batch);
|
|
608
|
+
if (!this.enqueue(batch, reason)) return;
|
|
505
609
|
}
|
|
506
|
-
this.queuedEventsSizeBytes = 0;
|
|
507
|
-
}
|
|
508
|
-
takeEventBatches(maxBatchSizeBytes) {
|
|
509
|
-
const batches = [];
|
|
510
|
-
let current = [];
|
|
511
|
-
let currentSize = 0;
|
|
512
|
-
for (const event of this.events.splice(0)) {
|
|
513
|
-
const eventSize = this.eventSizeBytes(event);
|
|
514
|
-
if (current.length > 0 && currentSize + eventSize > maxBatchSizeBytes) {
|
|
515
|
-
batches.push(current);
|
|
516
|
-
current = [];
|
|
517
|
-
currentSize = 0;
|
|
518
|
-
}
|
|
519
|
-
current.push(event);
|
|
520
|
-
currentSize += eventSize;
|
|
521
|
-
}
|
|
522
|
-
if (current.length > 0) batches.push(current);
|
|
523
|
-
return batches;
|
|
524
610
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
611
|
+
endStream(reason) {
|
|
612
|
+
if (this.unfinalized) this.enqueue([], reason, true);
|
|
613
|
+
}
|
|
614
|
+
enqueue(events, reason, isFinal = false) {
|
|
615
|
+
const { siteKey } = this.options;
|
|
616
|
+
const { sessionId, windowId, sessionStart } = this.session;
|
|
617
|
+
const sequence = nextReplaySequence(siteKey, this.session, this.cookieless);
|
|
528
618
|
if (sequence === void 0) {
|
|
529
619
|
this.stopCapture("Replay sequence state is invalid or exhausted");
|
|
530
|
-
return;
|
|
620
|
+
return false;
|
|
531
621
|
}
|
|
532
|
-
const
|
|
622
|
+
const identifier = getAnonymousId(siteKey, this.cookieless);
|
|
623
|
+
const chunkStartedAt = this.chunkStart;
|
|
533
624
|
const now = Date.now();
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
625
|
+
this.chunkStart = now;
|
|
626
|
+
this.unfinalized = !isFinal;
|
|
627
|
+
this.uploader.add({
|
|
628
|
+
token: siteKey,
|
|
629
|
+
sessionId,
|
|
630
|
+
windowId,
|
|
539
631
|
viewId: this.viewId,
|
|
540
|
-
sessionStart
|
|
632
|
+
sessionStart,
|
|
541
633
|
chunkStartedAt,
|
|
542
634
|
chunkEndedAt: now,
|
|
543
635
|
...identifier ? { identifier } : {},
|
|
544
|
-
batchId: `${
|
|
636
|
+
batchId: `${sessionId}-${sequence}-${chunkStartedAt}`,
|
|
545
637
|
sequence,
|
|
546
638
|
sequenceContractVersion: 1,
|
|
547
639
|
timestamp: now,
|
|
548
|
-
url: this.viewUrl
|
|
640
|
+
url: this.viewUrl,
|
|
549
641
|
...isFinal ? { isFinal: true } : {},
|
|
550
|
-
flushReason,
|
|
642
|
+
flushReason: reason,
|
|
551
643
|
events
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
eventSizeBytes(event) {
|
|
555
|
-
let size = eventSizeCache.get(event);
|
|
556
|
-
if (size === void 0) {
|
|
557
|
-
size = textEncoder.encode(JSON.stringify(event)).byteLength + 1;
|
|
558
|
-
eventSizeCache.set(event, size);
|
|
559
|
-
}
|
|
560
|
-
return size;
|
|
561
|
-
}
|
|
562
|
-
batchSizeBytes(batch) {
|
|
563
|
-
const cached = batchSizeCache.get(batch);
|
|
564
|
-
if (cached !== void 0) return cached;
|
|
565
|
-
const size = textEncoder.encode(JSON.stringify(batch)).byteLength;
|
|
566
|
-
batchSizeCache.set(batch, size);
|
|
567
|
-
return size;
|
|
568
|
-
}
|
|
569
|
-
removePending(batch) {
|
|
570
|
-
const index = this.pending.indexOf(batch);
|
|
571
|
-
if (index < 0) return;
|
|
572
|
-
this.pending.splice(index, 1);
|
|
573
|
-
this.pendingSizeBytes = Math.max(0, this.pendingSizeBytes - this.batchSizeBytes(batch));
|
|
574
|
-
this.log(`Sent replay batch ${batch.sequence}`);
|
|
575
|
-
}
|
|
576
|
-
stopCapture(reason) {
|
|
577
|
-
this.log(reason);
|
|
578
|
-
this.overflowed = true;
|
|
579
|
-
this.stopRecording?.();
|
|
580
|
-
this.stopRecording = void 0;
|
|
581
|
-
}
|
|
582
|
-
enqueueBatch(batch) {
|
|
583
|
-
this.needsFinalization = !batch.isFinal;
|
|
584
|
-
this.pending.push(batch);
|
|
585
|
-
this.pendingSizeBytes += this.batchSizeBytes(batch);
|
|
586
|
-
}
|
|
587
|
-
async flush(lowLatency, isFinal = false, flushReason = "manual") {
|
|
588
|
-
if (this.events.length === 0 && !isFinal) {
|
|
589
|
-
await this.sendPending(lowLatency);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const rotation = touchActivity(this.options.siteKey, this.lastCookielessMode);
|
|
593
|
-
if (rotation) this.applySessionRotation(rotation.prev, rotation.next);
|
|
594
|
-
const context = this.sessionContext();
|
|
595
|
-
this.enqueueEventsIfReady(context, flushReason, lowLatency ? this.maxLowLatencyBatchSizeBytes : this.maxBatchSizeBytes);
|
|
596
|
-
if (!isFinal) return this.sendPending(lowLatency);
|
|
597
|
-
this.finalizeSession(context, flushReason);
|
|
598
|
-
for (const batch of [...this.pending].reverse()) this.sendBatch(batch, true).then((sent) => sent && this.removePending(batch));
|
|
599
|
-
}
|
|
600
|
-
async sendPending(lowLatency) {
|
|
601
|
-
if (this.retryTask) return;
|
|
602
|
-
if (this.sending) {
|
|
603
|
-
this.queuedFlush = !!this.queuedFlush || lowLatency;
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
this.sending = true;
|
|
607
|
-
try {
|
|
608
|
-
while (this.pending.length > 0) {
|
|
609
|
-
const batch = this.pending[0];
|
|
610
|
-
if (!await this.sendBatch(batch, lowLatency)) {
|
|
611
|
-
this.log(`Failed to send replay batch ${batch.sequence}`);
|
|
612
|
-
if (!this.disposed) this.scheduleRetry();
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
this.retryAttempt = 0;
|
|
616
|
-
this.removePending(batch);
|
|
617
|
-
if (this.queuedFlush) break;
|
|
618
|
-
}
|
|
619
|
-
} finally {
|
|
620
|
-
this.sending = false;
|
|
621
|
-
if (this.queuedFlush !== null) {
|
|
622
|
-
const lowLatency = this.queuedFlush;
|
|
623
|
-
this.queuedFlush = null;
|
|
624
|
-
this.sendPending(lowLatency);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
scheduleRetry() {
|
|
629
|
-
if (this.retryTask) return;
|
|
630
|
-
this.retryAttempt = Math.min(this.retryAttempt + 1, RETRY_MAX_ATTEMPTS);
|
|
631
|
-
const delay = this.retryDelay(this.retryAttempt);
|
|
632
|
-
this.log(this.retryAttempt === RETRY_MAX_ATTEMPTS ? `Replay delivery paused for ${delay}ms` : `Retrying replay delivery in ${delay}ms`);
|
|
633
|
-
this.retryTask = setTimeout(() => {
|
|
634
|
-
this.retryTask = null;
|
|
635
|
-
if (this.retryAttempt === RETRY_MAX_ATTEMPTS) this.retryAttempt = 0;
|
|
636
|
-
this.sendPending(false);
|
|
637
|
-
}, delay);
|
|
638
|
-
}
|
|
639
|
-
retryDelay(attempt) {
|
|
640
|
-
if (attempt >= RETRY_MAX_ATTEMPTS) return RETRY_COOLDOWN_MS;
|
|
641
|
-
const ceiling = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
642
|
-
return Math.round(ceiling * (.5 + Math.random() * .5));
|
|
643
|
-
}
|
|
644
|
-
async sendBatch(batch, lowLatency) {
|
|
645
|
-
const json = JSON.stringify(batch);
|
|
646
|
-
const keepalive = lowLatency && this.batchSizeBytes(batch) <= KEEPALIVE_LIMIT_BYTES;
|
|
647
|
-
const send = (data, contentType, query = "") => sendData({
|
|
648
|
-
url: `${this.endpoint}${query}`,
|
|
649
|
-
data,
|
|
650
|
-
contentType,
|
|
651
|
-
debug: this.options.debug,
|
|
652
|
-
debugPrefix: "[Replay]",
|
|
653
|
-
useBeacon: keepalive,
|
|
654
|
-
keepalive,
|
|
655
|
-
settleRejected: true
|
|
656
644
|
});
|
|
657
|
-
|
|
658
|
-
const compressed = await gzip(json);
|
|
659
|
-
this.log(`Compressed ${json.length}B -> ${compressed.byteLength}B (${Math.round(compressed.byteLength / json.length * 100)}%)`);
|
|
660
|
-
return send(compressed, "application/octet-stream", "?encoding=gzip");
|
|
661
|
-
} catch {
|
|
662
|
-
this.log("Compression failed, using uncompressed");
|
|
663
|
-
}
|
|
664
|
-
return send(json, "application/json");
|
|
645
|
+
return true;
|
|
665
646
|
}
|
|
666
647
|
};
|
|
667
|
-
//#endregion
|
|
668
648
|
export { ReplayTracker as default, sessionReplay };
|