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