@obsrviq/tracker 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/index.js +513 -100
- package/dist/index.js.map +1 -1
- package/dist/obsrviq.global.js +42 -42
- package/dist/obsrviq.global.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +4 -0
- package/src/index.ts +7 -1
- package/src/network.ts +111 -16
- package/src/ratelimit.ts +24 -0
- package/src/recorder.ts +20 -1
- package/src/serialize.ts +63 -7
- package/src/trace.ts +100 -0
- package/src/transport.ts +340 -68
- package/src/util.ts +15 -1
- package/src/websocket.ts +37 -12
package/dist/index.js
CHANGED
|
@@ -88,24 +88,23 @@ function redactBatchSensitiveKeys(events) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
function maskValue(value, depth = 0) {
|
|
92
|
-
if (depth > 6) return "\xABdepth\xBB";
|
|
93
|
-
if (typeof value === "string") return maskString(value);
|
|
94
|
-
if (value === null || typeof value !== "object") return value;
|
|
95
|
-
if (Array.isArray(value)) return value.map((v) => maskValue(v, depth + 1));
|
|
96
|
-
const out = {};
|
|
97
|
-
for (const [k, v] of Object.entries(value)) {
|
|
98
|
-
out[k] = isSensitiveKeyName(k) ? "\xABredacted\xBB" : maskValue(v, depth + 1);
|
|
99
|
-
}
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
91
|
|
|
103
92
|
// src/transport.ts
|
|
104
93
|
var MAX_BUFFER_EVENTS = 5e3;
|
|
94
|
+
var MAX_BATCH_JSON_BYTES = 4 * 1024 * 1024;
|
|
95
|
+
var BEACON_PIECE_BYTES = 56 * 1024;
|
|
96
|
+
var SPILL_KEY = "obsrviq.spill";
|
|
97
|
+
var SPILL_MAX_BYTES = 15e5;
|
|
98
|
+
var SPILL_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
99
|
+
var RR_FULL_SNAPSHOT = 2;
|
|
100
|
+
var RR_META = 4;
|
|
105
101
|
var Transport = class {
|
|
106
102
|
constructor(opts) {
|
|
107
103
|
this.opts = opts;
|
|
108
104
|
this.buffer = [];
|
|
105
|
+
/** Floor set by an unload flush that consumed seqs out-of-band (beacon pieces),
|
|
106
|
+
* so the normal path can never re-use one of them. */
|
|
107
|
+
this.seqFloor = 0;
|
|
109
108
|
this.sending = false;
|
|
110
109
|
this.retryDelay = 1e3;
|
|
111
110
|
this.stopped = false;
|
|
@@ -121,29 +120,107 @@ var Transport = class {
|
|
|
121
120
|
this.timer = setInterval(() => void this.flush("timer"), this.opts.flushIntervalMs);
|
|
122
121
|
document.addEventListener("visibilitychange", this.onVisibility, { capture: true });
|
|
123
122
|
window.addEventListener("pagehide", this.onPageHide, { capture: true });
|
|
123
|
+
this.resendSpill();
|
|
124
124
|
}
|
|
125
125
|
enqueue(event) {
|
|
126
126
|
if (this.stopped) return;
|
|
127
127
|
this.buffer.push(event);
|
|
128
128
|
if (this.buffer.length >= MAX_BUFFER_EVENTS) {
|
|
129
129
|
void this.flush("backpressure");
|
|
130
|
-
if (this.buffer.length >= MAX_BUFFER_EVENTS)
|
|
131
|
-
|
|
130
|
+
if (this.buffer.length >= MAX_BUFFER_EVENTS) this.shedDomBacklog();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Drop buffered DOM events WITHOUT breaking rrweb's delta chain. Deltas only
|
|
134
|
+
* apply on top of everything before them, so arbitrary dropping corrupts the
|
|
135
|
+
* replay until the next 30s checkpoint. Instead: if the buffer holds a
|
|
136
|
+
* FullSnapshot, drop only the DOM events BEFORE it (the snapshot re-anchors
|
|
137
|
+
* everything after). Otherwise drop ALL buffered DOM and ask the recorder for
|
|
138
|
+
* a fresh FullSnapshot so the stream re-anchors immediately. Non-DOM events
|
|
139
|
+
* (console/network/errors/…) are independent and kept. */
|
|
140
|
+
shedDomBacklog() {
|
|
141
|
+
let snapIdx = -1;
|
|
142
|
+
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
|
143
|
+
const e = this.buffer[i];
|
|
144
|
+
if (e?.type === "dom" && e.data?.type === RR_FULL_SNAPSHOT) {
|
|
145
|
+
snapIdx = i;
|
|
146
|
+
break;
|
|
132
147
|
}
|
|
133
148
|
}
|
|
149
|
+
if (snapIdx > 0) {
|
|
150
|
+
let keepFrom = snapIdx;
|
|
151
|
+
const prev = this.buffer[snapIdx - 1];
|
|
152
|
+
if (prev?.type === "dom" && prev.data?.type === RR_META) {
|
|
153
|
+
keepFrom = snapIdx - 1;
|
|
154
|
+
}
|
|
155
|
+
const kept = this.buffer.filter((e, i) => e.type !== "dom" || i >= keepFrom);
|
|
156
|
+
if (kept.length < this.buffer.length) {
|
|
157
|
+
this.buffer = kept;
|
|
158
|
+
if (this.buffer.length < MAX_BUFFER_EVENTS) return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.buffer = this.buffer.filter((e) => e.type !== "dom");
|
|
162
|
+
if (this.buffer.length >= MAX_BUFFER_EVENTS) {
|
|
163
|
+
this.buffer = this.buffer.slice(-Math.floor(MAX_BUFFER_EVENTS / 2));
|
|
164
|
+
}
|
|
165
|
+
this.opts.onDomDrop?.();
|
|
134
166
|
}
|
|
135
167
|
/** Build, compress, and send the current buffer. Returns true on success. */
|
|
136
168
|
async flush(reason) {
|
|
169
|
+
if (reason === "pagehide" || reason === "hidden") return this.flushUnload();
|
|
137
170
|
if (this.sending || this.buffer.length === 0) return true;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!this.stopped) setTimeout(() => void this.flush("timer"), 200);
|
|
171
|
+
if (isMaskConfigPending() && !this.stopped) {
|
|
172
|
+
setTimeout(() => void this.flush("timer"), 200);
|
|
141
173
|
return true;
|
|
142
174
|
}
|
|
143
175
|
this.sending = true;
|
|
144
176
|
const events = this.buffer;
|
|
145
177
|
this.buffer = [];
|
|
146
178
|
redactBatchSensitiveKeys(events);
|
|
179
|
+
let ok;
|
|
180
|
+
try {
|
|
181
|
+
ok = await this.sendEvents(events);
|
|
182
|
+
} finally {
|
|
183
|
+
this.sending = false;
|
|
184
|
+
}
|
|
185
|
+
if (ok && this.stopped && this.buffer.length > 0) void this.flush("manual");
|
|
186
|
+
return ok;
|
|
187
|
+
}
|
|
188
|
+
/** Send events as one or more seq-stamped batches: oversized batches split in
|
|
189
|
+
* half (byte cap here, or a server 413), poison batches (other 4xx) are dropped
|
|
190
|
+
* instead of retried forever, and transient failures re-buffer + back off. */
|
|
191
|
+
async sendEvents(events) {
|
|
192
|
+
const queue = [events];
|
|
193
|
+
while (queue.length > 0) {
|
|
194
|
+
const slice = queue[0];
|
|
195
|
+
const result = await this.dispatch(slice);
|
|
196
|
+
if (result === "ok") {
|
|
197
|
+
queue.shift();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (result === "too_large" && slice.length > 1) {
|
|
201
|
+
const mid = Math.ceil(slice.length / 2);
|
|
202
|
+
queue.splice(0, 1, slice.slice(0, mid), slice.slice(mid));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (result === "fatal" || result === "too_large") {
|
|
206
|
+
queue.shift();
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
this.buffer = [].concat(...queue, this.buffer);
|
|
210
|
+
if (this.buffer.length >= MAX_BUFFER_EVENTS) this.shedDomBacklog();
|
|
211
|
+
if (!this.stopped) {
|
|
212
|
+
const jitter = this.retryDelay * (0.5 + Math.random());
|
|
213
|
+
this.retryDelay = Math.min(this.retryDelay * 2, 3e4);
|
|
214
|
+
setTimeout(() => void this.flush("timer"), jitter);
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
this.retryDelay = 1e3;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/** One batch → one network attempt. Owns seq assignment/advance. */
|
|
222
|
+
async dispatch(events) {
|
|
223
|
+
if (this.seqFloor > this.seq) this.seq = this.seqFloor;
|
|
147
224
|
let batch = {
|
|
148
225
|
siteKey: this.opts.siteKey,
|
|
149
226
|
sessionId: this.opts.sessionId,
|
|
@@ -158,73 +235,165 @@ var Transport = class {
|
|
|
158
235
|
batch = null;
|
|
159
236
|
}
|
|
160
237
|
}
|
|
161
|
-
if (!batch)
|
|
162
|
-
this.sending = false;
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
this.opts.onSeqDispatch?.(this.seq + 1);
|
|
238
|
+
if (!batch) return "ok";
|
|
166
239
|
const json = JSON.stringify(batch);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
this.buffer = events.slice(-MAX_BUFFER_EVENTS).concat(this.buffer);
|
|
177
|
-
this.sending = false;
|
|
178
|
-
if (!this.stopped) {
|
|
179
|
-
const jitter = this.retryDelay * (0.5 + Math.random());
|
|
180
|
-
this.retryDelay = Math.min(this.retryDelay * 2, 3e4);
|
|
181
|
-
setTimeout(() => void this.flush("timer"), jitter);
|
|
182
|
-
}
|
|
183
|
-
return false;
|
|
240
|
+
if (json.length > MAX_BATCH_JSON_BYTES && events.length > 1) return "too_large";
|
|
241
|
+
this.opts.onSeqDispatch?.(this.seq + 1);
|
|
242
|
+
const gz = await gzipBytes(new TextEncoder().encode(json));
|
|
243
|
+
const result = await this.send(gz);
|
|
244
|
+
this.opts.onFlush?.({ bytes: gz.byteLength, events: events.length, ok: result === "ok" });
|
|
245
|
+
if (result === "ok") this.seq = Math.max(this.seq + 1, this.seqFloor);
|
|
246
|
+
return result;
|
|
184
247
|
}
|
|
185
|
-
async send(bytes
|
|
248
|
+
async send(bytes) {
|
|
186
249
|
const url = `${this.opts.endpoint.replace(/\/$/, "")}/v1/batch`;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
250
|
+
const ctrl = new AbortController();
|
|
251
|
+
const timeout = setTimeout(() => ctrl.abort(), 15e3);
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(url, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: bytes,
|
|
256
|
+
headers: {
|
|
257
|
+
"content-type": "application/octet-stream",
|
|
258
|
+
"x-obsrviq-key": this.opts.siteKey
|
|
259
|
+
},
|
|
260
|
+
credentials: "omit",
|
|
261
|
+
mode: "cors",
|
|
262
|
+
signal: ctrl.signal
|
|
263
|
+
});
|
|
264
|
+
if (res.ok) return "ok";
|
|
265
|
+
if (res.status === 413) return "too_large";
|
|
266
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) return "fatal";
|
|
267
|
+
return "retry";
|
|
268
|
+
} catch {
|
|
269
|
+
return "retry";
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timeout);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Unload flush (pagehide / tab hidden). Fully synchronous — the page may be
|
|
275
|
+
* gone before any await resolves. Ignores the `sending` guard (that in-flight
|
|
276
|
+
* fetch is about to be cancelled; its seq is skipped via seqFloor), chunks the
|
|
277
|
+
* tail into beacon-sized pieces (sendBeacon caps bodies at ~64KB — one big
|
|
278
|
+
* batch would be rejected whole), and spills anything the beacon queue refuses
|
|
279
|
+
* to localStorage for the next page load to resend. */
|
|
280
|
+
flushUnload() {
|
|
281
|
+
if (this.buffer.length === 0) return true;
|
|
282
|
+
const events = this.buffer;
|
|
283
|
+
this.buffer = [];
|
|
284
|
+
redactBatchSensitiveKeys(events);
|
|
285
|
+
let seq = Math.max(this.sending ? this.seq + 1 : this.seq, this.seqFloor);
|
|
286
|
+
const pieces = [];
|
|
287
|
+
const stack = [events];
|
|
288
|
+
while (stack.length > 0) {
|
|
289
|
+
const slice = stack.shift();
|
|
290
|
+
let batch = {
|
|
291
|
+
siteKey: this.opts.siteKey,
|
|
292
|
+
sessionId: this.opts.sessionId,
|
|
293
|
+
seq,
|
|
294
|
+
meta: this.opts.getMeta(),
|
|
295
|
+
events: slice
|
|
296
|
+
};
|
|
297
|
+
if (this.opts.beforeSend) {
|
|
195
298
|
try {
|
|
196
|
-
|
|
299
|
+
batch = this.opts.beforeSend(batch);
|
|
197
300
|
} catch {
|
|
301
|
+
batch = null;
|
|
198
302
|
}
|
|
199
303
|
}
|
|
304
|
+
if (!batch) continue;
|
|
305
|
+
const gz = gzipSync(new TextEncoder().encode(JSON.stringify(batch)));
|
|
306
|
+
if (gz.byteLength > BEACON_PIECE_BYTES && slice.length > 1) {
|
|
307
|
+
const mid = Math.ceil(slice.length / 2);
|
|
308
|
+
stack.unshift(slice.slice(0, mid), slice.slice(mid));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
pieces.push({ gz, count: slice.length });
|
|
312
|
+
seq++;
|
|
313
|
+
}
|
|
314
|
+
this.seqFloor = seq;
|
|
315
|
+
this.opts.onSeqDispatch?.(seq);
|
|
316
|
+
for (const { gz, count } of pieces) {
|
|
317
|
+
const sent = gz.byteLength <= BEACON_PIECE_BYTES && this.sendUnload(gz);
|
|
318
|
+
if (!sent) this.spill(gz);
|
|
319
|
+
this.opts.onFlush?.({ bytes: gz.byteLength, events: count, ok: sent });
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
/** Fire-and-forget delivery that survives navigation. True = queued. */
|
|
324
|
+
sendUnload(bytes) {
|
|
325
|
+
const url = `${this.opts.endpoint.replace(/\/$/, "")}/v1/batch`;
|
|
326
|
+
const blob = new Blob([bytes], { type: "application/octet-stream" });
|
|
327
|
+
if (typeof navigator.sendBeacon === "function") {
|
|
200
328
|
try {
|
|
201
|
-
|
|
202
|
-
() => {
|
|
203
|
-
}
|
|
204
|
-
);
|
|
205
|
-
return true;
|
|
329
|
+
if (navigator.sendBeacon(url, blob)) return true;
|
|
206
330
|
} catch {
|
|
207
|
-
return false;
|
|
208
331
|
}
|
|
209
332
|
}
|
|
210
|
-
const ctrl = new AbortController();
|
|
211
|
-
const timeout = setTimeout(() => ctrl.abort(), 15e3);
|
|
212
333
|
try {
|
|
213
|
-
|
|
334
|
+
void fetch(url, {
|
|
214
335
|
method: "POST",
|
|
215
|
-
body,
|
|
216
|
-
headers
|
|
336
|
+
body: bytes,
|
|
337
|
+
headers: {
|
|
338
|
+
"content-type": "application/octet-stream",
|
|
339
|
+
"x-obsrviq-key": this.opts.siteKey
|
|
340
|
+
},
|
|
341
|
+
keepalive: true,
|
|
217
342
|
credentials: "omit",
|
|
218
|
-
mode: "cors"
|
|
219
|
-
|
|
343
|
+
mode: "cors"
|
|
344
|
+
}).catch(() => {
|
|
220
345
|
});
|
|
221
|
-
return
|
|
346
|
+
return true;
|
|
222
347
|
} catch {
|
|
223
348
|
return false;
|
|
224
|
-
} finally {
|
|
225
|
-
clearTimeout(timeout);
|
|
226
349
|
}
|
|
227
350
|
}
|
|
351
|
+
/** Append one gzipped batch to the localStorage spill (bounded, oldest-first
|
|
352
|
+
* eviction). Quota/private-mode failures mean the tail is lost — same as
|
|
353
|
+
* before spilling existed. */
|
|
354
|
+
spill(gz) {
|
|
355
|
+
try {
|
|
356
|
+
const raw = window.localStorage.getItem(SPILL_KEY);
|
|
357
|
+
const entries = raw ? JSON.parse(raw) : [];
|
|
358
|
+
entries.push({ k: this.opts.siteKey, b: b64encode(gz), ts: Date.now() });
|
|
359
|
+
let total = entries.reduce((n, e) => n + e.b.length, 0);
|
|
360
|
+
while (entries.length > 0 && total > SPILL_MAX_BYTES) {
|
|
361
|
+
total -= entries[0].b.length;
|
|
362
|
+
entries.shift();
|
|
363
|
+
}
|
|
364
|
+
window.localStorage.setItem(SPILL_KEY, JSON.stringify(entries));
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** Resend batches a previous page's unload spilled. Entries are claimed
|
|
369
|
+
* (removed) up front so two tabs can't double-send; still-transient failures
|
|
370
|
+
* are re-spilled for the next load. Fire-and-forget. */
|
|
371
|
+
resendSpill() {
|
|
372
|
+
let mine = [];
|
|
373
|
+
try {
|
|
374
|
+
const raw = window.localStorage.getItem(SPILL_KEY);
|
|
375
|
+
if (!raw) return;
|
|
376
|
+
const entries = JSON.parse(raw);
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
const fresh = entries.filter((e) => e && typeof e.b === "string" && now - e.ts < SPILL_MAX_AGE_MS);
|
|
379
|
+
mine = fresh.filter((e) => e.k === this.opts.siteKey);
|
|
380
|
+
const others = fresh.filter((e) => e.k !== this.opts.siteKey);
|
|
381
|
+
if (others.length > 0) window.localStorage.setItem(SPILL_KEY, JSON.stringify(others));
|
|
382
|
+
else window.localStorage.removeItem(SPILL_KEY);
|
|
383
|
+
} catch {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (mine.length === 0) return;
|
|
387
|
+
void (async () => {
|
|
388
|
+
for (const entry of mine) {
|
|
389
|
+
try {
|
|
390
|
+
const result = await this.send(b64decode(entry.b));
|
|
391
|
+
if (result === "retry") this.spill(b64decode(entry.b));
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
}
|
|
228
397
|
stop() {
|
|
229
398
|
this.stopped = true;
|
|
230
399
|
if (this.timer) clearInterval(this.timer);
|
|
@@ -233,6 +402,29 @@ var Transport = class {
|
|
|
233
402
|
void this.flush("manual");
|
|
234
403
|
}
|
|
235
404
|
};
|
|
405
|
+
async function gzipBytes(bytes) {
|
|
406
|
+
if (typeof CompressionStream !== "undefined") {
|
|
407
|
+
try {
|
|
408
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
409
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return gzipSync(bytes);
|
|
414
|
+
}
|
|
415
|
+
function b64encode(bytes) {
|
|
416
|
+
let bin = "";
|
|
417
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
418
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
419
|
+
}
|
|
420
|
+
return btoa(bin);
|
|
421
|
+
}
|
|
422
|
+
function b64decode(s) {
|
|
423
|
+
const bin = atob(s);
|
|
424
|
+
const out = new Uint8Array(bin.length);
|
|
425
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
236
428
|
|
|
237
429
|
// src/util.ts
|
|
238
430
|
function uuid() {
|
|
@@ -283,6 +475,13 @@ async function sha256Hex(input) {
|
|
|
283
475
|
function detectDevice() {
|
|
284
476
|
const ua = navigator.userAgent;
|
|
285
477
|
const viewport = { w: window.innerWidth, h: window.innerHeight };
|
|
478
|
+
let tz;
|
|
479
|
+
let tzOffset;
|
|
480
|
+
try {
|
|
481
|
+
tz = Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
|
|
482
|
+
tzOffset = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
286
485
|
let type = "desktop";
|
|
287
486
|
if (/iPad|Tablet/i.test(ua)) type = "tablet";
|
|
288
487
|
else if (/Mobi|Android|iPhone/i.test(ua)) type = "mobile";
|
|
@@ -297,7 +496,7 @@ function detectDevice() {
|
|
|
297
496
|
else if (/Chrome\//.test(ua)) browser = "Chrome";
|
|
298
497
|
else if (/Firefox\//.test(ua)) browser = "Firefox";
|
|
299
498
|
else if (/Safari\//.test(ua)) browser = "Safari";
|
|
300
|
-
return { type, os, browser, viewport };
|
|
499
|
+
return { type, os, browser, viewport, tz, tzOffset };
|
|
301
500
|
}
|
|
302
501
|
function privacySignalsOptOut() {
|
|
303
502
|
const nav = navigator;
|
|
@@ -311,11 +510,13 @@ var INCREMENTAL = 3;
|
|
|
311
510
|
function instrumentDom(ctx) {
|
|
312
511
|
const stop = record({
|
|
313
512
|
emit(event) {
|
|
513
|
+
const t = ctx.clock.fromPerf(performanceNow());
|
|
514
|
+
event.timestamp = ctx.clock.startedAt + t;
|
|
314
515
|
const dom = {
|
|
315
516
|
id: uuid(),
|
|
316
517
|
sessionId: ctx.sessionId,
|
|
317
518
|
type: "dom",
|
|
318
|
-
t
|
|
519
|
+
t,
|
|
319
520
|
ts: ctx.clock.wall(),
|
|
320
521
|
data: event
|
|
321
522
|
};
|
|
@@ -346,12 +547,19 @@ function instrumentDom(ctx) {
|
|
|
346
547
|
}
|
|
347
548
|
};
|
|
348
549
|
}
|
|
550
|
+
function takeFullSnapshot() {
|
|
551
|
+
try {
|
|
552
|
+
record.takeFullSnapshot(true);
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
}
|
|
349
556
|
function performanceNow() {
|
|
350
557
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
351
558
|
}
|
|
352
559
|
|
|
353
560
|
// src/serialize.ts
|
|
354
561
|
var DEFAULT_MAX_BYTES = 8 * 1024;
|
|
562
|
+
var MAX_WALK_NODES = 2e3;
|
|
355
563
|
function byteLen(s) {
|
|
356
564
|
return new Blob([s]).size;
|
|
357
565
|
}
|
|
@@ -367,9 +575,11 @@ function serializeArg(arg, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
367
575
|
return { kind: "primitive", value: arg };
|
|
368
576
|
}
|
|
369
577
|
if (type === "string") {
|
|
370
|
-
const
|
|
578
|
+
const s = arg;
|
|
579
|
+
const cut = s.length > maxBytes * 2 ? s.slice(0, maxBytes * 2) : s;
|
|
580
|
+
const masked = maskString(cut);
|
|
371
581
|
const { value, truncated } = truncate(masked, maxBytes);
|
|
372
|
-
return { kind: "string", value, truncated };
|
|
582
|
+
return { kind: "string", value, truncated: truncated || cut !== s };
|
|
373
583
|
}
|
|
374
584
|
if (arg instanceof Error) {
|
|
375
585
|
return {
|
|
@@ -383,21 +593,56 @@ function serializeArg(arg, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
383
593
|
return { kind: "dom", selector: cssPath(arg) };
|
|
384
594
|
}
|
|
385
595
|
if (type === "object" || type === "function" || type === "symbol" || type === "bigint") {
|
|
386
|
-
const
|
|
596
|
+
const budget = { nodes: MAX_WALK_NODES, bytes: maxBytes * 2, exhausted: false };
|
|
597
|
+
const masked = maskValueBudgeted(arg, budget);
|
|
387
598
|
let json;
|
|
388
|
-
let truncated =
|
|
599
|
+
let truncated = budget.exhausted;
|
|
389
600
|
try {
|
|
390
601
|
const full = JSON.stringify(masked, safeReplacer());
|
|
391
602
|
const cut = truncate(full ?? "undefined", maxBytes);
|
|
392
603
|
json = cut.value;
|
|
393
|
-
truncated = cut.truncated;
|
|
604
|
+
truncated = truncated || cut.truncated;
|
|
394
605
|
} catch {
|
|
395
606
|
json = void 0;
|
|
396
607
|
}
|
|
397
|
-
return { kind: "object", preview: previewOf(
|
|
608
|
+
return { kind: "object", preview: previewOf(arg), json, truncated };
|
|
398
609
|
}
|
|
399
610
|
return { kind: "primitive", value: String(arg) };
|
|
400
611
|
}
|
|
612
|
+
function maskValueBudgeted(value, budget, depth = 0) {
|
|
613
|
+
if (budget.exhausted) return "\xABtruncated\xBB";
|
|
614
|
+
if (depth > 6) return "\xABdepth\xBB";
|
|
615
|
+
if (--budget.nodes <= 0) budget.exhausted = true;
|
|
616
|
+
if (typeof value === "string") {
|
|
617
|
+
const cut = value.length > budget.bytes ? value.slice(0, Math.max(0, budget.bytes)) : value;
|
|
618
|
+
if (cut !== value) budget.exhausted = true;
|
|
619
|
+
const masked = maskString(cut);
|
|
620
|
+
if ((budget.bytes -= masked.length + 2) <= 0) budget.exhausted = true;
|
|
621
|
+
return masked;
|
|
622
|
+
}
|
|
623
|
+
if (value === null || typeof value !== "object") return value;
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
const out2 = [];
|
|
626
|
+
for (const v of value) {
|
|
627
|
+
if (budget.exhausted) {
|
|
628
|
+
out2.push("\xABtruncated\xBB");
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
out2.push(maskValueBudgeted(v, budget, depth + 1));
|
|
632
|
+
}
|
|
633
|
+
return out2;
|
|
634
|
+
}
|
|
635
|
+
const out = {};
|
|
636
|
+
for (const k of Object.keys(value)) {
|
|
637
|
+
if (budget.exhausted) {
|
|
638
|
+
out["\u2026"] = "\xABtruncated\xBB";
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if ((budget.bytes -= k.length + 4) <= 0) budget.exhausted = true;
|
|
642
|
+
out[k] = isSensitiveKeyName(k) ? "\xABredacted\xBB" : maskValueBudgeted(value[k], budget, depth + 1);
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
401
646
|
function safeReplacer() {
|
|
402
647
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
403
648
|
return (_key, value) => {
|
|
@@ -531,8 +776,83 @@ function errorToConsoleEvent(ctx, err) {
|
|
|
531
776
|
};
|
|
532
777
|
}
|
|
533
778
|
|
|
779
|
+
// src/ratelimit.ts
|
|
780
|
+
function tokenBucket(rate2, burst) {
|
|
781
|
+
let tokens = burst;
|
|
782
|
+
let last = Date.now();
|
|
783
|
+
return {
|
|
784
|
+
take() {
|
|
785
|
+
const now = Date.now();
|
|
786
|
+
tokens = Math.min(burst, tokens + (now - last) / 1e3 * rate2);
|
|
787
|
+
last = now;
|
|
788
|
+
if (tokens < 1) return false;
|
|
789
|
+
tokens -= 1;
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/trace.ts
|
|
796
|
+
function randomHex(bytes) {
|
|
797
|
+
const arr = new Uint8Array(bytes);
|
|
798
|
+
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
799
|
+
crypto.getRandomValues(arr);
|
|
800
|
+
} else {
|
|
801
|
+
for (let i = 0; i < bytes; i++) arr[i] = Math.random() * 256 | 0;
|
|
802
|
+
}
|
|
803
|
+
let s = "";
|
|
804
|
+
for (let i = 0; i < bytes; i++) s += arr[i].toString(16).padStart(2, "0");
|
|
805
|
+
return s;
|
|
806
|
+
}
|
|
807
|
+
function newTrace() {
|
|
808
|
+
const traceId = randomHex(16);
|
|
809
|
+
const spanId = randomHex(8);
|
|
810
|
+
return { traceId, spanId, header: `00-${traceId}-${spanId}-01` };
|
|
811
|
+
}
|
|
812
|
+
function traceTarget(rawUrl, cfg) {
|
|
813
|
+
if (!cfg.tracePropagation) return false;
|
|
814
|
+
let u;
|
|
815
|
+
try {
|
|
816
|
+
u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
|
|
817
|
+
} catch {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
821
|
+
if (typeof location !== "undefined" && u.origin === location.origin) return true;
|
|
822
|
+
return hostAllowed(u.hostname, cfg.traceHosts);
|
|
823
|
+
}
|
|
824
|
+
function hostAllowed(hostname, hosts) {
|
|
825
|
+
const h = hostname.toLowerCase();
|
|
826
|
+
for (const raw of hosts) {
|
|
827
|
+
let entry = String(raw).trim().toLowerCase();
|
|
828
|
+
if (!entry) continue;
|
|
829
|
+
if (entry.includes("://")) {
|
|
830
|
+
try {
|
|
831
|
+
entry = new URL(entry).hostname;
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
entry = entry.replace(/^\*?\./, "");
|
|
836
|
+
if (h === entry || h.endsWith("." + entry)) return true;
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
function injectFetchTrace(input, init, header) {
|
|
841
|
+
const hasInitHeaders = !!init && "headers" in init && init.headers != null;
|
|
842
|
+
if (input instanceof Request && !hasInitHeaders) {
|
|
843
|
+
const h2 = new Headers(input.headers);
|
|
844
|
+
if (!h2.has("traceparent")) h2.set("traceparent", header);
|
|
845
|
+
return { input: new Request(input, { headers: h2 }), init };
|
|
846
|
+
}
|
|
847
|
+
const h = new Headers(init?.headers ?? void 0);
|
|
848
|
+
if (!h.has("traceparent")) h.set("traceparent", header);
|
|
849
|
+
return { input, init: { ...init || {}, headers: h } };
|
|
850
|
+
}
|
|
851
|
+
|
|
534
852
|
// src/network.ts
|
|
535
853
|
var BODY_CAP = 4 * 1024;
|
|
854
|
+
var NET_RATE = 50;
|
|
855
|
+
var NET_BURST = 100;
|
|
536
856
|
var HEADER_ALLOW = /* @__PURE__ */ new Set([
|
|
537
857
|
"content-type",
|
|
538
858
|
"content-length",
|
|
@@ -544,6 +864,7 @@ var HEADER_ALLOW = /* @__PURE__ */ new Set([
|
|
|
544
864
|
"content-encoding",
|
|
545
865
|
"x-request-id",
|
|
546
866
|
"x-correlation-id",
|
|
867
|
+
"traceparent",
|
|
547
868
|
"server",
|
|
548
869
|
"vary"
|
|
549
870
|
]);
|
|
@@ -574,15 +895,48 @@ function pickHeaders(redact, entries) {
|
|
|
574
895
|
}
|
|
575
896
|
function bodyPreview(text, contentType) {
|
|
576
897
|
if (text == null) return void 0;
|
|
577
|
-
const
|
|
578
|
-
const
|
|
898
|
+
const cut = text.length > BODY_CAP * 2 ? text.slice(0, BODY_CAP * 2) : text;
|
|
899
|
+
const masked = maskString(cut);
|
|
900
|
+
const truncated = cut !== text || masked.length > BODY_CAP;
|
|
579
901
|
return {
|
|
580
902
|
contentType,
|
|
581
903
|
preview: truncated ? masked.slice(0, BODY_CAP) : masked,
|
|
582
904
|
truncated,
|
|
583
|
-
redacted: masked !==
|
|
905
|
+
redacted: masked !== cut
|
|
584
906
|
};
|
|
585
907
|
}
|
|
908
|
+
async function readTextCapped(res, cap) {
|
|
909
|
+
const body = res.body;
|
|
910
|
+
if (!body || typeof body.getReader !== "function") {
|
|
911
|
+
return { text: await res.text(), overflow: false };
|
|
912
|
+
}
|
|
913
|
+
const reader = body.getReader();
|
|
914
|
+
const decoder = new TextDecoder();
|
|
915
|
+
let text = "";
|
|
916
|
+
let bytes = 0;
|
|
917
|
+
let overflow = false;
|
|
918
|
+
try {
|
|
919
|
+
for (; ; ) {
|
|
920
|
+
const { done, value } = await reader.read();
|
|
921
|
+
if (done || !value) {
|
|
922
|
+
text += decoder.decode();
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
const remaining = cap + 1 - bytes;
|
|
926
|
+
if (value.byteLength >= remaining) {
|
|
927
|
+
text += decoder.decode(value.subarray(0, remaining), { stream: true });
|
|
928
|
+
overflow = true;
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
text += decoder.decode(value, { stream: true });
|
|
932
|
+
bytes += value.byteLength;
|
|
933
|
+
}
|
|
934
|
+
} finally {
|
|
935
|
+
void reader.cancel().catch(() => {
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return { text, overflow };
|
|
939
|
+
}
|
|
586
940
|
function perfNow() {
|
|
587
941
|
return typeof performance !== "undefined" ? performance.now() : 0;
|
|
588
942
|
}
|
|
@@ -702,6 +1056,19 @@ function createTimingResolver() {
|
|
|
702
1056
|
}
|
|
703
1057
|
function instrumentNetwork(ctx) {
|
|
704
1058
|
const timing = createTimingResolver();
|
|
1059
|
+
const bucket = tokenBucket(NET_RATE, NET_BURST);
|
|
1060
|
+
let dropped = 0;
|
|
1061
|
+
const emitNet = (ev) => {
|
|
1062
|
+
if (!bucket.take()) {
|
|
1063
|
+
dropped++;
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (dropped > 0) {
|
|
1067
|
+
ev.droppedEvents = dropped;
|
|
1068
|
+
dropped = 0;
|
|
1069
|
+
}
|
|
1070
|
+
ctx.emit(ev);
|
|
1071
|
+
};
|
|
705
1072
|
const onPageHide = () => timing.flushPending();
|
|
706
1073
|
const onVisibility = () => {
|
|
707
1074
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") timing.flushPending();
|
|
@@ -709,10 +1076,10 @@ function instrumentNetwork(ctx) {
|
|
|
709
1076
|
if (typeof window !== "undefined") window.addEventListener("pagehide", onPageHide);
|
|
710
1077
|
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
|
|
711
1078
|
const teardowns = [];
|
|
712
|
-
teardowns.push(wrapFetch(ctx, timing));
|
|
713
|
-
teardowns.push(wrapXhr(ctx, timing));
|
|
714
|
-
teardowns.push(wrapBeacon(ctx));
|
|
715
|
-
teardowns.push(observeResources(ctx));
|
|
1079
|
+
teardowns.push(wrapFetch(ctx, timing, emitNet));
|
|
1080
|
+
teardowns.push(wrapXhr(ctx, timing, emitNet));
|
|
1081
|
+
teardowns.push(wrapBeacon(ctx, emitNet));
|
|
1082
|
+
teardowns.push(observeResources(ctx, emitNet));
|
|
716
1083
|
teardowns.push(() => {
|
|
717
1084
|
if (typeof window !== "undefined") window.removeEventListener("pagehide", onPageHide);
|
|
718
1085
|
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
|
|
@@ -720,7 +1087,7 @@ function instrumentNetwork(ctx) {
|
|
|
720
1087
|
});
|
|
721
1088
|
return () => teardowns.forEach((t) => t());
|
|
722
1089
|
}
|
|
723
|
-
function wrapFetch(ctx, tr) {
|
|
1090
|
+
function wrapFetch(ctx, tr, emitNet) {
|
|
724
1091
|
const orig = window.fetch;
|
|
725
1092
|
if (!orig) return () => {
|
|
726
1093
|
};
|
|
@@ -732,6 +1099,17 @@ function wrapFetch(ctx, tr) {
|
|
|
732
1099
|
const perfStart = perfNow();
|
|
733
1100
|
const method = (init?.method || (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
734
1101
|
const url = redactUrl(rawUrl);
|
|
1102
|
+
let traceId;
|
|
1103
|
+
if (traceTarget(rawUrl, ctx.config)) {
|
|
1104
|
+
try {
|
|
1105
|
+
const tr2 = newTrace();
|
|
1106
|
+
const inj = injectFetchTrace(input, init, tr2.header);
|
|
1107
|
+
input = inj.input;
|
|
1108
|
+
init = inj.init;
|
|
1109
|
+
traceId = tr2.traceId;
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
735
1113
|
const reqHeaders = pickHeaders(
|
|
736
1114
|
ctx.config.redactHeaders,
|
|
737
1115
|
headerEntries(init?.headers ?? (input instanceof Request ? input.headers : void 0))
|
|
@@ -754,10 +1132,11 @@ function wrapFetch(ctx, tr) {
|
|
|
754
1132
|
ok: false,
|
|
755
1133
|
resourceType: "fetch",
|
|
756
1134
|
requestHeaders: reqHeaders,
|
|
1135
|
+
traceId,
|
|
757
1136
|
timing,
|
|
758
1137
|
...partial
|
|
759
1138
|
};
|
|
760
|
-
|
|
1139
|
+
emitNet(ev);
|
|
761
1140
|
});
|
|
762
1141
|
};
|
|
763
1142
|
try {
|
|
@@ -768,8 +1147,9 @@ function wrapFetch(ctx, tr) {
|
|
|
768
1147
|
let responseBody;
|
|
769
1148
|
if (ctx.config.captureNetworkBodies && isTexty(res.headers.get("content-type"))) {
|
|
770
1149
|
try {
|
|
771
|
-
const text = await res.clone()
|
|
1150
|
+
const { text, overflow } = await readTextCapped(res.clone(), BODY_CAP);
|
|
772
1151
|
responseBody = bodyPreview(text, res.headers.get("content-type") || void 0);
|
|
1152
|
+
if (responseBody && overflow) responseBody.truncated = true;
|
|
773
1153
|
} catch {
|
|
774
1154
|
}
|
|
775
1155
|
}
|
|
@@ -800,7 +1180,7 @@ function classifyFetchError(err) {
|
|
|
800
1180
|
if (msg.includes("timeout")) return "timeout";
|
|
801
1181
|
return "network";
|
|
802
1182
|
}
|
|
803
|
-
function wrapXhr(ctx, tr) {
|
|
1183
|
+
function wrapXhr(ctx, tr, emitNet) {
|
|
804
1184
|
const XHR = XMLHttpRequest.prototype;
|
|
805
1185
|
const origOpen = XHR.open;
|
|
806
1186
|
const origSend = XHR.send;
|
|
@@ -834,6 +1214,14 @@ function wrapXhr(ctx, tr) {
|
|
|
834
1214
|
meta.startT = ctx.clock.now();
|
|
835
1215
|
meta.perfStart = perfNow();
|
|
836
1216
|
if (ctx.config.captureNetworkBodies) meta.body = stringifyBody(body);
|
|
1217
|
+
if (traceTarget(meta.rawUrl, ctx.config)) {
|
|
1218
|
+
try {
|
|
1219
|
+
const tr2 = newTrace();
|
|
1220
|
+
this.setRequestHeader("traceparent", tr2.header);
|
|
1221
|
+
meta.traceId = tr2.traceId;
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
837
1225
|
const finish = (error) => {
|
|
838
1226
|
const endT = ctx.clock.now();
|
|
839
1227
|
const base = {
|
|
@@ -868,6 +1256,7 @@ function wrapXhr(ctx, tr) {
|
|
|
868
1256
|
ok: status >= 200 && status < 400,
|
|
869
1257
|
resourceType: "xhr",
|
|
870
1258
|
requestHeaders: meta.reqHeaders,
|
|
1259
|
+
traceId: meta.traceId,
|
|
871
1260
|
responseHeaders: respHeaders,
|
|
872
1261
|
responseSize,
|
|
873
1262
|
requestBody: ctx.config.captureNetworkBodies ? bodyPreview(meta.body, void 0) : void 0,
|
|
@@ -875,7 +1264,7 @@ function wrapXhr(ctx, tr) {
|
|
|
875
1264
|
timing,
|
|
876
1265
|
error: error || classifyHttp(status)
|
|
877
1266
|
};
|
|
878
|
-
|
|
1267
|
+
emitNet(ev);
|
|
879
1268
|
});
|
|
880
1269
|
};
|
|
881
1270
|
this.addEventListener("loadend", () => finish());
|
|
@@ -908,7 +1297,7 @@ function parseRawHeaders(ctx, raw) {
|
|
|
908
1297
|
}
|
|
909
1298
|
return pickHeaders(ctx.config.redactHeaders, entries);
|
|
910
1299
|
}
|
|
911
|
-
function wrapBeacon(ctx) {
|
|
1300
|
+
function wrapBeacon(ctx, emitNet) {
|
|
912
1301
|
const orig = navigator.sendBeacon;
|
|
913
1302
|
if (!orig) return () => {
|
|
914
1303
|
};
|
|
@@ -935,7 +1324,7 @@ function wrapBeacon(ctx) {
|
|
|
935
1324
|
timing: { startT, endT: ctx.clock.now(), duration: 0 },
|
|
936
1325
|
error: ok ? void 0 : "network"
|
|
937
1326
|
};
|
|
938
|
-
|
|
1327
|
+
emitNet(ev);
|
|
939
1328
|
}
|
|
940
1329
|
}
|
|
941
1330
|
return ok;
|
|
@@ -955,7 +1344,7 @@ var RESOURCE_TYPE_MAP = {
|
|
|
955
1344
|
video: "media",
|
|
956
1345
|
track: "media"
|
|
957
1346
|
};
|
|
958
|
-
function observeResources(ctx) {
|
|
1347
|
+
function observeResources(ctx, emitNet) {
|
|
959
1348
|
if (typeof PerformanceObserver === "undefined") return () => {
|
|
960
1349
|
};
|
|
961
1350
|
let obs;
|
|
@@ -992,7 +1381,7 @@ function observeResources(ctx) {
|
|
|
992
1381
|
responseSize: entry.transferSize || entry.encodedBodySize || void 0,
|
|
993
1382
|
timing
|
|
994
1383
|
};
|
|
995
|
-
|
|
1384
|
+
emitNet(ev);
|
|
996
1385
|
}
|
|
997
1386
|
});
|
|
998
1387
|
obs.observe({ type: "resource", buffered: true });
|
|
@@ -1033,6 +1422,8 @@ function headerEntries(headers) {
|
|
|
1033
1422
|
|
|
1034
1423
|
// src/websocket.ts
|
|
1035
1424
|
var FRAME_CAP = 4 * 1024;
|
|
1425
|
+
var MSG_RATE = 20;
|
|
1426
|
+
var MSG_BURST = 40;
|
|
1036
1427
|
function redactUrl2(raw) {
|
|
1037
1428
|
try {
|
|
1038
1429
|
const u = new URL(raw, location.href);
|
|
@@ -1053,12 +1444,13 @@ function frameInfo(data, capture) {
|
|
|
1053
1444
|
if (typeof data === "string") {
|
|
1054
1445
|
const info = { encoding: "text", size: byteLen2(data) };
|
|
1055
1446
|
if (capture) {
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1447
|
+
const cut = data.length > FRAME_CAP * 2 ? data.slice(0, FRAME_CAP * 2) : data;
|
|
1448
|
+
const masked = maskString(cut);
|
|
1449
|
+
const truncated = cut !== data || masked.length > FRAME_CAP;
|
|
1058
1450
|
info.body = {
|
|
1059
1451
|
preview: truncated ? masked.slice(0, FRAME_CAP) : masked,
|
|
1060
1452
|
truncated,
|
|
1061
|
-
redacted: masked !==
|
|
1453
|
+
redacted: masked !== cut
|
|
1062
1454
|
};
|
|
1063
1455
|
}
|
|
1064
1456
|
return info;
|
|
@@ -1095,24 +1487,39 @@ function instrumentWebSocket(ctx) {
|
|
|
1095
1487
|
const redUrl = redactUrl2(String(url));
|
|
1096
1488
|
const protos = protocols ? Array.isArray(protocols) ? protocols : [protocols] : void 0;
|
|
1097
1489
|
const capture = ctx.config.captureNetworkBodies;
|
|
1490
|
+
const bucket = tokenBucket(MSG_RATE, MSG_BURST);
|
|
1491
|
+
let droppedFrames = 0;
|
|
1492
|
+
const emitFrame = (dir, data) => {
|
|
1493
|
+
ctx.markActivity();
|
|
1494
|
+
if (!bucket.take()) {
|
|
1495
|
+
droppedFrames++;
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const info = frameInfo(data, capture);
|
|
1499
|
+
const dropped = droppedFrames;
|
|
1500
|
+
droppedFrames = 0;
|
|
1501
|
+
emit({ connId, url: redUrl, kind: "message", dir, ...info, droppedFrames: dropped || void 0 });
|
|
1502
|
+
};
|
|
1098
1503
|
this.addEventListener("open", () => emit({ connId, url: redUrl, kind: "open", protocols: protos }));
|
|
1099
1504
|
this.addEventListener(
|
|
1100
1505
|
"close",
|
|
1101
|
-
(e) => emit({
|
|
1506
|
+
(e) => emit({
|
|
1507
|
+
connId,
|
|
1508
|
+
url: redUrl,
|
|
1509
|
+
kind: "close",
|
|
1510
|
+
code: e.code,
|
|
1511
|
+
reason: e.reason || void 0,
|
|
1512
|
+
wasClean: e.wasClean,
|
|
1513
|
+
droppedFrames: droppedFrames || void 0
|
|
1514
|
+
})
|
|
1102
1515
|
);
|
|
1103
1516
|
this.addEventListener("error", () => emit({ connId, url: redUrl, kind: "error" }));
|
|
1104
|
-
this.addEventListener("message", (e) =>
|
|
1105
|
-
const info = frameInfo(e.data, capture);
|
|
1106
|
-
emit({ connId, url: redUrl, kind: "message", dir: "recv", ...info });
|
|
1107
|
-
ctx.markActivity();
|
|
1108
|
-
});
|
|
1517
|
+
this.addEventListener("message", (e) => emitFrame("recv", e.data));
|
|
1109
1518
|
const origSend = super.send.bind(this);
|
|
1110
1519
|
this.send = (data) => {
|
|
1111
1520
|
origSend(data);
|
|
1112
1521
|
try {
|
|
1113
|
-
|
|
1114
|
-
emit({ connId, url: redUrl, kind: "message", dir: "send", ...info });
|
|
1115
|
-
ctx.markActivity();
|
|
1522
|
+
emitFrame("send", data);
|
|
1116
1523
|
} catch {
|
|
1117
1524
|
}
|
|
1118
1525
|
};
|
|
@@ -1851,6 +2258,8 @@ var DEFAULTS = {
|
|
|
1851
2258
|
noPrivacy: false,
|
|
1852
2259
|
captureNetworkBodies: false,
|
|
1853
2260
|
redactHeaders: ["authorization", "cookie", "set-cookie"],
|
|
2261
|
+
tracePropagation: false,
|
|
2262
|
+
traceHosts: [],
|
|
1854
2263
|
sampleRate: 1,
|
|
1855
2264
|
recordCanvas: false,
|
|
1856
2265
|
endpoint: "https://in.lumera.app",
|
|
@@ -1998,6 +2407,10 @@ var ObsrviqClient = class {
|
|
|
1998
2407
|
this.nextSeq = next;
|
|
1999
2408
|
this.persistSession();
|
|
2000
2409
|
},
|
|
2410
|
+
// Backpressure dropped DOM deltas → re-anchor the rrweb stream immediately
|
|
2411
|
+
// so replay stays coherent (a delta stream with a hole is corrupt until
|
|
2412
|
+
// the next 30s checkout).
|
|
2413
|
+
onDomDrop: () => takeFullSnapshot(),
|
|
2001
2414
|
onFlush: ({ bytes, events, ok }) => {
|
|
2002
2415
|
if (ok) {
|
|
2003
2416
|
this.diag.flushes++;
|