crumbtrail-core 0.1.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/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/bug-widget-LGIRLS76.js +201 -0
- package/dist/index.cjs +4867 -0
- package/dist/index.d.cts +960 -0
- package/dist/index.d.ts +960 -0
- package/dist/index.js +4552 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4867 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/widget/styles.ts
|
|
24
|
+
var WIDGET_CSS;
|
|
25
|
+
var init_styles = __esm({
|
|
26
|
+
"src/widget/styles.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
WIDGET_CSS = `
|
|
29
|
+
:host {
|
|
30
|
+
all: initial;
|
|
31
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
color: #e0e0e0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.bl-trigger {
|
|
37
|
+
position: fixed;
|
|
38
|
+
bottom: 20px;
|
|
39
|
+
right: 20px;
|
|
40
|
+
width: 48px;
|
|
41
|
+
height: 48px;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
background: #dc2626;
|
|
44
|
+
border: none;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
50
|
+
z-index: 2147483647;
|
|
51
|
+
transition: transform 0.15s ease, background 0.15s ease;
|
|
52
|
+
}
|
|
53
|
+
.bl-trigger:hover {
|
|
54
|
+
transform: scale(1.1);
|
|
55
|
+
background: #ef4444;
|
|
56
|
+
}
|
|
57
|
+
.bl-trigger svg {
|
|
58
|
+
width: 24px;
|
|
59
|
+
height: 24px;
|
|
60
|
+
fill: white;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.bl-popover {
|
|
64
|
+
position: fixed;
|
|
65
|
+
bottom: 80px;
|
|
66
|
+
right: 20px;
|
|
67
|
+
width: 320px;
|
|
68
|
+
background: #1e1e1e;
|
|
69
|
+
border: 1px solid #333;
|
|
70
|
+
border-radius: 12px;
|
|
71
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
72
|
+
z-index: 2147483647;
|
|
73
|
+
padding: 16px;
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
.bl-popover.open {
|
|
77
|
+
display: block;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.bl-popover h3 {
|
|
81
|
+
margin: 0 0 12px;
|
|
82
|
+
font-size: 15px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
color: #f5f5f5;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.bl-note {
|
|
88
|
+
width: 100%;
|
|
89
|
+
min-height: 60px;
|
|
90
|
+
background: #2a2a2a;
|
|
91
|
+
border: 1px solid #444;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
color: #e0e0e0;
|
|
94
|
+
padding: 8px 10px;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
font-family: inherit;
|
|
97
|
+
resize: vertical;
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
}
|
|
100
|
+
.bl-note::placeholder {
|
|
101
|
+
color: #777;
|
|
102
|
+
}
|
|
103
|
+
.bl-note:focus {
|
|
104
|
+
outline: none;
|
|
105
|
+
border-color: #dc2626;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.bl-actions {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: 8px;
|
|
111
|
+
margin-top: 12px;
|
|
112
|
+
align-items: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.bl-btn {
|
|
116
|
+
padding: 8px 16px;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
border: none;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
font-size: 13px;
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
transition: background 0.15s ease;
|
|
123
|
+
}
|
|
124
|
+
.bl-submit {
|
|
125
|
+
background: #dc2626;
|
|
126
|
+
color: white;
|
|
127
|
+
flex: 1;
|
|
128
|
+
}
|
|
129
|
+
.bl-submit:hover {
|
|
130
|
+
background: #ef4444;
|
|
131
|
+
}
|
|
132
|
+
.bl-submit:disabled {
|
|
133
|
+
opacity: 0.5;
|
|
134
|
+
cursor: not-allowed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.bl-status {
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
color: #777;
|
|
140
|
+
margin-top: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.bl-hint {
|
|
144
|
+
font-size: 11px;
|
|
145
|
+
color: #555;
|
|
146
|
+
margin-top: 8px;
|
|
147
|
+
text-align: center;
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// src/widget/bug-widget.ts
|
|
154
|
+
var bug_widget_exports = {};
|
|
155
|
+
__export(bug_widget_exports, {
|
|
156
|
+
mountWidget: () => mountWidget
|
|
157
|
+
});
|
|
158
|
+
function mountWidget(logger) {
|
|
159
|
+
const host = document.createElement("div");
|
|
160
|
+
host.id = "crumbtrail-widget";
|
|
161
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
162
|
+
const style = document.createElement("style");
|
|
163
|
+
style.textContent = WIDGET_CSS;
|
|
164
|
+
shadow.appendChild(style);
|
|
165
|
+
const trigger = document.createElement("button");
|
|
166
|
+
trigger.className = "bl-trigger";
|
|
167
|
+
trigger.innerHTML = BUG_SVG;
|
|
168
|
+
trigger.title = "Flag a bug (Ctrl+Shift+B)";
|
|
169
|
+
shadow.appendChild(trigger);
|
|
170
|
+
const popover = document.createElement("div");
|
|
171
|
+
popover.className = "bl-popover";
|
|
172
|
+
popover.innerHTML = `
|
|
173
|
+
<h3>Flag a Bug</h3>
|
|
174
|
+
<textarea class="bl-note" placeholder="What went wrong? (optional)"></textarea>
|
|
175
|
+
<div class="bl-actions">
|
|
176
|
+
<button class="bl-btn bl-submit">Send Bug Report</button>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="bl-status"></div>
|
|
179
|
+
<div class="bl-hint">Ctrl+Shift+B to toggle</div>
|
|
180
|
+
`;
|
|
181
|
+
shadow.appendChild(popover);
|
|
182
|
+
const noteInput = popover.querySelector(".bl-note");
|
|
183
|
+
const submitBtn = popover.querySelector(".bl-submit");
|
|
184
|
+
const statusEl = popover.querySelector(".bl-status");
|
|
185
|
+
let isOpen = false;
|
|
186
|
+
function toggle() {
|
|
187
|
+
isOpen = !isOpen;
|
|
188
|
+
popover.classList.toggle("open", isOpen);
|
|
189
|
+
if (isOpen) {
|
|
190
|
+
noteInput.focus();
|
|
191
|
+
} else {
|
|
192
|
+
reset();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function reset() {
|
|
196
|
+
noteInput.value = "";
|
|
197
|
+
statusEl.textContent = "";
|
|
198
|
+
}
|
|
199
|
+
trigger.addEventListener("click", toggle);
|
|
200
|
+
submitBtn.addEventListener("click", async () => {
|
|
201
|
+
submitBtn.disabled = true;
|
|
202
|
+
statusEl.textContent = "Sending...";
|
|
203
|
+
try {
|
|
204
|
+
const { bugId } = await logger.flagBug({
|
|
205
|
+
note: noteInput.value || void 0
|
|
206
|
+
});
|
|
207
|
+
statusEl.textContent = `Saved: ${bugId}`;
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
toggle();
|
|
210
|
+
submitBtn.disabled = false;
|
|
211
|
+
}, 1500);
|
|
212
|
+
} catch {
|
|
213
|
+
statusEl.textContent = "Failed to send";
|
|
214
|
+
submitBtn.disabled = false;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
function onKeyDown(e) {
|
|
218
|
+
if (e.ctrlKey && e.shiftKey && e.key === "B") {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
toggle();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
document.addEventListener("keydown", onKeyDown);
|
|
224
|
+
document.body.appendChild(host);
|
|
225
|
+
return () => {
|
|
226
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
227
|
+
host.remove();
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
var BUG_SVG;
|
|
231
|
+
var init_bug_widget = __esm({
|
|
232
|
+
"src/widget/bug-widget.ts"() {
|
|
233
|
+
"use strict";
|
|
234
|
+
init_styles();
|
|
235
|
+
BUG_SVG = `<svg viewBox="0 0 24 24"><path d="M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5s-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></svg>`;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// src/index.ts
|
|
240
|
+
var index_exports = {};
|
|
241
|
+
__export(index_exports, {
|
|
242
|
+
BROWSER_REDACTION_POLICY: () => BROWSER_REDACTION_POLICY,
|
|
243
|
+
CRUMBTRAIL_EVENT_KINDS: () => CRUMBTRAIL_EVENT_KINDS,
|
|
244
|
+
CRUMBTRAIL_REQUEST_HEADER: () => CRUMBTRAIL_REQUEST_HEADER,
|
|
245
|
+
CRUMBTRAIL_REQUEST_HEADER_LOWER: () => CRUMBTRAIL_REQUEST_HEADER_LOWER,
|
|
246
|
+
CRUMBTRAIL_REQUEST_ID_MAX_LENGTH: () => CRUMBTRAIL_REQUEST_ID_MAX_LENGTH,
|
|
247
|
+
CRUMBTRAIL_SCHEMA_VERSION: () => CRUMBTRAIL_SCHEMA_VERSION,
|
|
248
|
+
CRUMBTRAIL_SESSION_HEADER: () => CRUMBTRAIL_SESSION_HEADER,
|
|
249
|
+
CRUMBTRAIL_SESSION_HEADER_LOWER: () => CRUMBTRAIL_SESSION_HEADER_LOWER,
|
|
250
|
+
Crumbtrail: () => Crumbtrail,
|
|
251
|
+
DB_DIFF_BULK_EVENT_KIND: () => DB_DIFF_BULK_EVENT_KIND,
|
|
252
|
+
DB_DIFF_EVENT_KIND: () => DB_DIFF_EVENT_KIND,
|
|
253
|
+
DB_READ_BULK_EVENT_KIND: () => DB_READ_BULK_EVENT_KIND,
|
|
254
|
+
DB_READ_EVENT_KIND: () => DB_READ_EVENT_KIND,
|
|
255
|
+
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
256
|
+
DEFAULT_SESSION_STORAGE_KEY: () => DEFAULT_SESSION_STORAGE_KEY,
|
|
257
|
+
EVIDENCE_SCHEMA_VERSION: () => EVIDENCE_SCHEMA_VERSION,
|
|
258
|
+
EVIDENCE_SOURCE_SCHEMA_VERSION: () => EVIDENCE_SOURCE_SCHEMA_VERSION,
|
|
259
|
+
EventBus: () => EventBus,
|
|
260
|
+
FUSION_SCHEMA_VERSION: () => FUSION_SCHEMA_VERSION,
|
|
261
|
+
HttpTransport: () => HttpTransport,
|
|
262
|
+
PRESET_FULL: () => PRESET_FULL,
|
|
263
|
+
PRESET_LIGHT: () => PRESET_LIGHT,
|
|
264
|
+
PRESET_PASSIVE: () => PRESET_PASSIVE,
|
|
265
|
+
REDACTED_STORAGE_KEY: () => REDACTED_STORAGE_KEY,
|
|
266
|
+
REDACTED_VALUE: () => REDACTED_VALUE,
|
|
267
|
+
RingBuffer: () => RingBuffer,
|
|
268
|
+
W3C_TRACEPARENT_HEADER: () => W3C_TRACEPARENT_HEADER,
|
|
269
|
+
WebTargetDescriptorResolver: () => WebTargetDescriptorResolver,
|
|
270
|
+
abandonedFlowDetector: () => abandonedFlowDetector,
|
|
271
|
+
assembleBundle: () => assembleBundle,
|
|
272
|
+
attachRedactionMetadata: () => attachRedactionMetadata,
|
|
273
|
+
buildEnvDelta: () => buildEnvDelta,
|
|
274
|
+
buildEnvSnapshot: () => buildEnvSnapshot,
|
|
275
|
+
canInjectCorrelationHeaders: () => canInjectCorrelationHeaders,
|
|
276
|
+
computeElementPath: () => computeElementPath,
|
|
277
|
+
computeElementSignature: () => computeElementSignature,
|
|
278
|
+
createAutoFlagController: () => createAutoFlagController,
|
|
279
|
+
createCrumbtrailRequestHeaders: () => createCrumbtrailRequestHeaders,
|
|
280
|
+
createWebSessionStore: () => createWebSessionStore,
|
|
281
|
+
environmentCollector: () => environmentCollector,
|
|
282
|
+
errorDetector: () => errorDetector,
|
|
283
|
+
errorSignature: () => errorSignature,
|
|
284
|
+
formatTraceparent: () => formatTraceparent,
|
|
285
|
+
generateRequestId: () => generateRequestId,
|
|
286
|
+
generateSpanId: () => generateSpanId,
|
|
287
|
+
generateTraceContext: () => generateTraceContext,
|
|
288
|
+
generateTraceId: () => generateTraceId,
|
|
289
|
+
hashString: () => hashString,
|
|
290
|
+
inferIntent: () => inferIntent,
|
|
291
|
+
mergeRedactionMetadata: () => mergeRedactionMetadata,
|
|
292
|
+
parseTraceparent: () => parseTraceparent,
|
|
293
|
+
rageClickDetector: () => rageClickDetector,
|
|
294
|
+
redactCookieMap: () => redactCookieMap,
|
|
295
|
+
redactCookieName: () => redactCookieName,
|
|
296
|
+
redactCookieValue: () => redactCookieValue,
|
|
297
|
+
redactHeaders: () => redactHeaders,
|
|
298
|
+
redactInputValue: () => redactInputValue,
|
|
299
|
+
redactNetworkTextBody: () => redactNetworkTextBody,
|
|
300
|
+
redactStorageKey: () => redactStorageKey,
|
|
301
|
+
redactStoredValue: () => redactStoredValue,
|
|
302
|
+
redactTokenLikeString: () => redactTokenLikeString,
|
|
303
|
+
redactUrl: () => redactUrl,
|
|
304
|
+
redactUrlsInText: () => redactUrlsInText,
|
|
305
|
+
redactValue: () => redactValue,
|
|
306
|
+
resolveOutboundCorrelation: () => resolveOutboundCorrelation,
|
|
307
|
+
retryStormDetector: () => retryStormDetector,
|
|
308
|
+
slowResponseDetector: () => slowResponseDetector,
|
|
309
|
+
summarizeBinaryPayload: () => summarizeBinaryPayload,
|
|
310
|
+
summarizeOmittedPayload: () => summarizeOmittedPayload,
|
|
311
|
+
webTargetDescriptorResolver: () => webTargetDescriptorResolver
|
|
312
|
+
});
|
|
313
|
+
module.exports = __toCommonJS(index_exports);
|
|
314
|
+
|
|
315
|
+
// src/event-bus.ts
|
|
316
|
+
var EventBus = class {
|
|
317
|
+
listeners = [];
|
|
318
|
+
taps = [];
|
|
319
|
+
buffer = [];
|
|
320
|
+
flushTimer = null;
|
|
321
|
+
paused = false;
|
|
322
|
+
flushBufferSize = 100;
|
|
323
|
+
emit(event) {
|
|
324
|
+
for (const tap of this.taps) {
|
|
325
|
+
try {
|
|
326
|
+
tap(event);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
this.buffer.push(event);
|
|
331
|
+
if (!this.paused && this.buffer.length >= this.flushBufferSize) {
|
|
332
|
+
this.flush();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Observe every event synchronously at emit time, before batching. Unlike `subscribe`,
|
|
337
|
+
* taps see events immediately (triggers can't wait out a flush interval) and never
|
|
338
|
+
* receive batches.
|
|
339
|
+
*/
|
|
340
|
+
tap(fn) {
|
|
341
|
+
this.taps.push(fn);
|
|
342
|
+
return () => {
|
|
343
|
+
const idx = this.taps.indexOf(fn);
|
|
344
|
+
if (idx !== -1) this.taps.splice(idx, 1);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
subscribe(fn) {
|
|
348
|
+
this.listeners.push(fn);
|
|
349
|
+
return () => {
|
|
350
|
+
const idx = this.listeners.indexOf(fn);
|
|
351
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
flush() {
|
|
355
|
+
if (this.buffer.length === 0) return;
|
|
356
|
+
const batch = this.buffer;
|
|
357
|
+
this.buffer = [];
|
|
358
|
+
for (const listener of this.listeners) {
|
|
359
|
+
listener(batch);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
start(flushIntervalMs, flushBufferSize) {
|
|
363
|
+
this.flushBufferSize = flushBufferSize;
|
|
364
|
+
this.flushTimer = setInterval(() => {
|
|
365
|
+
if (!this.paused) this.flush();
|
|
366
|
+
}, flushIntervalMs);
|
|
367
|
+
}
|
|
368
|
+
stop() {
|
|
369
|
+
if (this.flushTimer !== null) {
|
|
370
|
+
clearInterval(this.flushTimer);
|
|
371
|
+
this.flushTimer = null;
|
|
372
|
+
}
|
|
373
|
+
this.flush();
|
|
374
|
+
}
|
|
375
|
+
pause() {
|
|
376
|
+
this.paused = true;
|
|
377
|
+
}
|
|
378
|
+
resume() {
|
|
379
|
+
this.paused = false;
|
|
380
|
+
this.flush();
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/ring-buffer.ts
|
|
385
|
+
var RingBuffer = class {
|
|
386
|
+
events = [];
|
|
387
|
+
maxMs;
|
|
388
|
+
maxEvents;
|
|
389
|
+
constructor(maxMs = 3e5, maxEvents = 5e4) {
|
|
390
|
+
this.maxMs = maxMs;
|
|
391
|
+
this.maxEvents = maxEvents;
|
|
392
|
+
}
|
|
393
|
+
push(event) {
|
|
394
|
+
this.events.push(event);
|
|
395
|
+
this.evict(event.t);
|
|
396
|
+
}
|
|
397
|
+
pushBatch(events) {
|
|
398
|
+
for (const event of events) {
|
|
399
|
+
this.events.push(event);
|
|
400
|
+
}
|
|
401
|
+
if (events.length > 0) {
|
|
402
|
+
this.evict(events[events.length - 1].t);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
snapshot(windowMs) {
|
|
406
|
+
const now2 = this.events.length > 0 ? this.events[this.events.length - 1].t : Date.now();
|
|
407
|
+
const cutoff = now2 - (windowMs ?? this.maxMs);
|
|
408
|
+
return this.events.filter((e) => e.t >= cutoff);
|
|
409
|
+
}
|
|
410
|
+
clear() {
|
|
411
|
+
this.events = [];
|
|
412
|
+
}
|
|
413
|
+
get size() {
|
|
414
|
+
return this.events.length;
|
|
415
|
+
}
|
|
416
|
+
evict(now2) {
|
|
417
|
+
const cutoff = now2 - this.maxMs;
|
|
418
|
+
while (this.events.length > 0 && this.events[0].t < cutoff) {
|
|
419
|
+
this.events.shift();
|
|
420
|
+
}
|
|
421
|
+
while (this.events.length > this.maxEvents) {
|
|
422
|
+
this.events.shift();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/types.ts
|
|
428
|
+
var CRUMBTRAIL_SCHEMA_VERSION = 1;
|
|
429
|
+
var CRUMBTRAIL_EVENT_KINDS = {
|
|
430
|
+
navigation: "navigation",
|
|
431
|
+
appLifecycle: "app-lifecycle",
|
|
432
|
+
nativeCrash: "native-crash",
|
|
433
|
+
viewSnapshot: "view-snapshot"
|
|
434
|
+
};
|
|
435
|
+
var DB_DIFF_EVENT_KIND = "db.diff";
|
|
436
|
+
var DB_DIFF_BULK_EVENT_KIND = "db.diff.bulk";
|
|
437
|
+
var DB_READ_EVENT_KIND = "db.read";
|
|
438
|
+
var DB_READ_BULK_EVENT_KIND = "db.read.bulk";
|
|
439
|
+
var DEFAULT_CONFIG = {
|
|
440
|
+
console: true,
|
|
441
|
+
network: true,
|
|
442
|
+
interactions: true,
|
|
443
|
+
keystrokes: true,
|
|
444
|
+
scroll: true,
|
|
445
|
+
visibility: true,
|
|
446
|
+
clipboard: true,
|
|
447
|
+
errors: true,
|
|
448
|
+
performance: true,
|
|
449
|
+
cookies: true,
|
|
450
|
+
storage: true,
|
|
451
|
+
video: false,
|
|
452
|
+
audio: false,
|
|
453
|
+
networkMaxBodySize: 51200,
|
|
454
|
+
networkExcludeUrls: [],
|
|
455
|
+
networkCaptureHeaders: true,
|
|
456
|
+
networkCorrelationHeaders: true,
|
|
457
|
+
networkCorrelationAllowedOrigins: [],
|
|
458
|
+
maskInputTypes: ["password", "email", "tel", "number", "search", "url"],
|
|
459
|
+
ignoreSelectors: [],
|
|
460
|
+
keystrokeThrottleMs: 0,
|
|
461
|
+
scrollThrottleMs: 500,
|
|
462
|
+
scrollElements: [],
|
|
463
|
+
clipboardMaxLength: 500,
|
|
464
|
+
captureRawClipboard: false,
|
|
465
|
+
cookiePollIntervalMs: 2e3,
|
|
466
|
+
cookieMaskNames: [],
|
|
467
|
+
cookieValueMaxLength: 500,
|
|
468
|
+
storageValueMaxLength: 500,
|
|
469
|
+
storageExcludeKeys: [],
|
|
470
|
+
captureIdb: true,
|
|
471
|
+
captureCacheApi: true,
|
|
472
|
+
videoBitsPerSecond: 1e6,
|
|
473
|
+
audioBitsPerSecond: 64e3,
|
|
474
|
+
mediaChunkIntervalMs: 1e4,
|
|
475
|
+
ringBufferMs: 3e5,
|
|
476
|
+
ringBufferMaxEvents: 5e4,
|
|
477
|
+
heartbeat: true,
|
|
478
|
+
environment: true,
|
|
479
|
+
widget: false,
|
|
480
|
+
stateMaxBytes: 32768,
|
|
481
|
+
captureRawState: false,
|
|
482
|
+
autoFlagOnError: false,
|
|
483
|
+
autoFlagDebounceMs: 2e3,
|
|
484
|
+
autoFlagMaxPerSession: 10,
|
|
485
|
+
autoFlagOnSignals: false,
|
|
486
|
+
rageClickThreshold: 4,
|
|
487
|
+
rageClickWindowMs: 1500,
|
|
488
|
+
retryStormThreshold: 4,
|
|
489
|
+
retryStormWindowMs: 5e3,
|
|
490
|
+
retryStormFailThreshold: 2,
|
|
491
|
+
slowRequestMs: 3e3,
|
|
492
|
+
slowRequestCount: 3,
|
|
493
|
+
slowRequestWindowMs: 1e4,
|
|
494
|
+
abandonedFlowWindowMs: 3e4,
|
|
495
|
+
abandonedFlowMinInputs: 2,
|
|
496
|
+
domSnapshot: true,
|
|
497
|
+
domSnapshotMaxBytes: 262144,
|
|
498
|
+
captureRawConsole: false,
|
|
499
|
+
captureRawErrors: false,
|
|
500
|
+
transport: "auto",
|
|
501
|
+
httpEndpoint: "http://localhost:9898",
|
|
502
|
+
httpAuthToken: "",
|
|
503
|
+
flushIntervalMs: 5e3,
|
|
504
|
+
flushBufferSize: 100,
|
|
505
|
+
sessionPersistence: "session",
|
|
506
|
+
sessionIdleMs: 18e5
|
|
507
|
+
// 30 minutes
|
|
508
|
+
};
|
|
509
|
+
var PRESET_FULL = {
|
|
510
|
+
widget: true,
|
|
511
|
+
autoFlagOnError: true,
|
|
512
|
+
autoFlagOnSignals: true
|
|
513
|
+
};
|
|
514
|
+
var PRESET_LIGHT = {
|
|
515
|
+
keystrokes: false,
|
|
516
|
+
video: false,
|
|
517
|
+
audio: false,
|
|
518
|
+
clipboard: false,
|
|
519
|
+
cookies: false,
|
|
520
|
+
storage: false,
|
|
521
|
+
performance: false
|
|
522
|
+
};
|
|
523
|
+
var PRESET_PASSIVE = {
|
|
524
|
+
autoFlagOnError: true,
|
|
525
|
+
autoFlagOnSignals: true
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/correlation.ts
|
|
529
|
+
var CRUMBTRAIL_SESSION_HEADER = "X-Crumbtrail-Session-Id";
|
|
530
|
+
var CRUMBTRAIL_REQUEST_HEADER = "X-Crumbtrail-Request-Id";
|
|
531
|
+
var CRUMBTRAIL_SESSION_HEADER_LOWER = CRUMBTRAIL_SESSION_HEADER.toLowerCase();
|
|
532
|
+
var CRUMBTRAIL_REQUEST_HEADER_LOWER = CRUMBTRAIL_REQUEST_HEADER.toLowerCase();
|
|
533
|
+
var REQUEST_ID_PREFIX = "req";
|
|
534
|
+
var REQUEST_ID_RANDOM_LENGTH = 12;
|
|
535
|
+
var CRUMBTRAIL_REQUEST_ID_MAX_LENGTH = 64;
|
|
536
|
+
function randomBase36(length) {
|
|
537
|
+
let value = "";
|
|
538
|
+
while (value.length < length) {
|
|
539
|
+
value += Math.random().toString(36).slice(2);
|
|
540
|
+
}
|
|
541
|
+
return value.slice(0, length);
|
|
542
|
+
}
|
|
543
|
+
function generateRequestId() {
|
|
544
|
+
return `${REQUEST_ID_PREFIX}_${Date.now().toString(36)}_${randomBase36(REQUEST_ID_RANDOM_LENGTH)}`.slice(
|
|
545
|
+
0,
|
|
546
|
+
CRUMBTRAIL_REQUEST_ID_MAX_LENGTH
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function normalizeRequestId(requestId) {
|
|
550
|
+
if (requestId && requestId.length <= CRUMBTRAIL_REQUEST_ID_MAX_LENGTH)
|
|
551
|
+
return requestId;
|
|
552
|
+
return generateRequestId();
|
|
553
|
+
}
|
|
554
|
+
function createCrumbtrailRequestHeaders(sessionId, requestId) {
|
|
555
|
+
return {
|
|
556
|
+
[CRUMBTRAIL_SESSION_HEADER]: sessionId,
|
|
557
|
+
[CRUMBTRAIL_REQUEST_HEADER]: normalizeRequestId(requestId)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
var W3C_TRACEPARENT_HEADER = "traceparent";
|
|
561
|
+
var TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
562
|
+
var SPAN_ID_RE = /^[0-9a-f]{16}$/;
|
|
563
|
+
var FLAGS_RE = /^[0-9a-f]{2}$/;
|
|
564
|
+
function parseTraceparent(value) {
|
|
565
|
+
if (!value) return void 0;
|
|
566
|
+
const parts = value.trim().split("-");
|
|
567
|
+
if (parts.length !== 4) return void 0;
|
|
568
|
+
const [version, traceId, spanId, flags] = parts;
|
|
569
|
+
if (version !== "00") return void 0;
|
|
570
|
+
if (!TRACE_ID_RE.test(traceId) || traceId === "0".repeat(32))
|
|
571
|
+
return void 0;
|
|
572
|
+
if (!SPAN_ID_RE.test(spanId) || spanId === "0".repeat(16)) return void 0;
|
|
573
|
+
if (!FLAGS_RE.test(flags)) return void 0;
|
|
574
|
+
return { traceId, spanId, flags: Number.parseInt(flags, 16) };
|
|
575
|
+
}
|
|
576
|
+
function formatTraceparent(ctx) {
|
|
577
|
+
const flags = (ctx.flags & 255).toString(16).padStart(2, "0");
|
|
578
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
579
|
+
}
|
|
580
|
+
var TRACE_FLAG_SAMPLED = 1;
|
|
581
|
+
function randomHex(bytes) {
|
|
582
|
+
const buf = new Uint8Array(bytes);
|
|
583
|
+
const c = globalThis.crypto;
|
|
584
|
+
if (c?.getRandomValues) {
|
|
585
|
+
c.getRandomValues(buf);
|
|
586
|
+
} else {
|
|
587
|
+
for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256);
|
|
588
|
+
}
|
|
589
|
+
let out = "";
|
|
590
|
+
for (let i = 0; i < bytes; i++) out += buf[i].toString(16).padStart(2, "0");
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
function generateTraceId() {
|
|
594
|
+
let id = randomHex(16);
|
|
595
|
+
if (id === "0".repeat(32)) id = id.slice(0, 31) + "1";
|
|
596
|
+
return id;
|
|
597
|
+
}
|
|
598
|
+
function generateSpanId() {
|
|
599
|
+
let id = randomHex(8);
|
|
600
|
+
if (id === "0".repeat(16)) id = id.slice(0, 15) + "1";
|
|
601
|
+
return id;
|
|
602
|
+
}
|
|
603
|
+
function generateTraceContext() {
|
|
604
|
+
return {
|
|
605
|
+
traceId: generateTraceId(),
|
|
606
|
+
spanId: generateSpanId(),
|
|
607
|
+
flags: TRACE_FLAG_SAMPLED
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function runtimeOrigin() {
|
|
611
|
+
try {
|
|
612
|
+
const origin = globalThis.location?.origin;
|
|
613
|
+
if (!origin || origin === "null") return void 0;
|
|
614
|
+
return origin;
|
|
615
|
+
} catch {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function urlOrigin(value, baseOrigin) {
|
|
620
|
+
try {
|
|
621
|
+
return new URL(value, baseOrigin).origin;
|
|
622
|
+
} catch {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function normalizeAllowedOrigin(value) {
|
|
627
|
+
const trimmed = value.trim();
|
|
628
|
+
if (!trimmed) return void 0;
|
|
629
|
+
if (trimmed === "self") return runtimeOrigin();
|
|
630
|
+
return urlOrigin(trimmed);
|
|
631
|
+
}
|
|
632
|
+
function canInjectCorrelationHeaders(requestUrl, allowedOrigins = []) {
|
|
633
|
+
const currentOrigin = runtimeOrigin();
|
|
634
|
+
const requestOrigin = urlOrigin(requestUrl, currentOrigin);
|
|
635
|
+
if (!requestOrigin) return false;
|
|
636
|
+
if (currentOrigin && requestOrigin === currentOrigin) return true;
|
|
637
|
+
return allowedOrigins.some(
|
|
638
|
+
(origin) => normalizeAllowedOrigin(origin) === requestOrigin
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
function resolveOutboundCorrelation(input) {
|
|
642
|
+
const existing = parseTraceparent(input.existingTraceparent);
|
|
643
|
+
const ctx = existing ?? generateTraceContext();
|
|
644
|
+
const requestId = normalizeRequestId(input.existingRequestId ?? ctx.traceId);
|
|
645
|
+
return {
|
|
646
|
+
sessionId: input.sessionId,
|
|
647
|
+
requestId,
|
|
648
|
+
traceId: ctx.traceId,
|
|
649
|
+
spanId: ctx.spanId,
|
|
650
|
+
traceparent: formatTraceparent(ctx)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/signature.ts
|
|
655
|
+
var STABLE_ID_ATTRS = ["data-bug-id", "data-testid", "data-test", "id"];
|
|
656
|
+
function stableAttr(el) {
|
|
657
|
+
for (const attr of STABLE_ID_ATTRS) {
|
|
658
|
+
const value = el.getAttribute?.(attr);
|
|
659
|
+
if (value) return `${attr}=${value}`;
|
|
660
|
+
}
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
function segment(el) {
|
|
664
|
+
const tag = el.tagName.toLowerCase();
|
|
665
|
+
const attr = stableAttr(el);
|
|
666
|
+
if (attr) return `${tag}[${attr}]`;
|
|
667
|
+
const parent = el.parentElement;
|
|
668
|
+
if (!parent) return tag;
|
|
669
|
+
let index = 1;
|
|
670
|
+
for (const sibling of Array.from(parent.children)) {
|
|
671
|
+
if (sibling === el) break;
|
|
672
|
+
if (sibling.tagName === el.tagName) index++;
|
|
673
|
+
}
|
|
674
|
+
return `${tag}:nth-of-type(${index})`;
|
|
675
|
+
}
|
|
676
|
+
function computeElementPath(el) {
|
|
677
|
+
const parts = [];
|
|
678
|
+
let current = el;
|
|
679
|
+
let guard = 0;
|
|
680
|
+
while (current && guard++ < 50) {
|
|
681
|
+
const seg = segment(current);
|
|
682
|
+
parts.unshift(seg);
|
|
683
|
+
if (seg.includes("[")) break;
|
|
684
|
+
current = current.parentElement;
|
|
685
|
+
}
|
|
686
|
+
return parts.join(">");
|
|
687
|
+
}
|
|
688
|
+
function hashString(input) {
|
|
689
|
+
let hash = 2166136261;
|
|
690
|
+
for (let i = 0; i < input.length; i++) {
|
|
691
|
+
hash ^= input.charCodeAt(i);
|
|
692
|
+
hash = Math.imul(hash, 16777619);
|
|
693
|
+
}
|
|
694
|
+
return (hash >>> 0).toString(36);
|
|
695
|
+
}
|
|
696
|
+
function computeElementSignature(el) {
|
|
697
|
+
const path = computeElementPath(el);
|
|
698
|
+
return { path, sig: hashString(path) };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/signals.ts
|
|
702
|
+
var SIGNATURE_STACK_LINES = 3;
|
|
703
|
+
function errorSignature(event) {
|
|
704
|
+
const msg = typeof event.d.msg === "string" ? event.d.msg : "";
|
|
705
|
+
const stk = typeof event.d.stk === "string" ? event.d.stk : "";
|
|
706
|
+
const frames = stk.split("\n").slice(0, SIGNATURE_STACK_LINES).join("\n");
|
|
707
|
+
return hashString(`${event.k}|${msg}|${frames}`);
|
|
708
|
+
}
|
|
709
|
+
function errorDetector() {
|
|
710
|
+
return {
|
|
711
|
+
inspect(event) {
|
|
712
|
+
if (event.k !== "err" && event.k !== "rej") return null;
|
|
713
|
+
const msg = typeof event.d.msg === "string" ? event.d.msg : void 0;
|
|
714
|
+
return {
|
|
715
|
+
tag: "auto:error",
|
|
716
|
+
key: `err:${errorSignature(event)}`,
|
|
717
|
+
note: msg ? `Auto-captured after error: ${msg}` : "Auto-captured after error"
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function slide(bucket, ts, windowMs) {
|
|
723
|
+
const arr = (bucket ?? []).filter((t) => ts - t < windowMs);
|
|
724
|
+
arr.push(ts);
|
|
725
|
+
return arr;
|
|
726
|
+
}
|
|
727
|
+
function targetKey(el) {
|
|
728
|
+
if (el && typeof el === "object") {
|
|
729
|
+
const r = el;
|
|
730
|
+
for (const field of [
|
|
731
|
+
"sig",
|
|
732
|
+
"ancestryHash",
|
|
733
|
+
"testID",
|
|
734
|
+
"testId",
|
|
735
|
+
"id",
|
|
736
|
+
"path"
|
|
737
|
+
]) {
|
|
738
|
+
const v = r[field];
|
|
739
|
+
if (typeof v === "string" && v) return `${field}:${v}`;
|
|
740
|
+
}
|
|
741
|
+
const tag = typeof r.tag === "string" ? r.tag : "?";
|
|
742
|
+
const txt = typeof r.txt === "string" ? r.txt : typeof r.label === "string" ? r.label : "";
|
|
743
|
+
return `el:${tag}:${txt}`;
|
|
744
|
+
}
|
|
745
|
+
return "el:unknown";
|
|
746
|
+
}
|
|
747
|
+
function rageClickDetector(opts) {
|
|
748
|
+
const hits = /* @__PURE__ */ new Map();
|
|
749
|
+
return {
|
|
750
|
+
inspect(event) {
|
|
751
|
+
if (event.k !== "clk") return null;
|
|
752
|
+
const key = targetKey(event.d.el ?? event.target);
|
|
753
|
+
const arr = slide(hits.get(key), event.t, opts.windowMs);
|
|
754
|
+
if (arr.length >= opts.threshold) {
|
|
755
|
+
hits.set(key, []);
|
|
756
|
+
const label = key.replace(/^[a-zA-Z]+:/, "") || "an element";
|
|
757
|
+
return {
|
|
758
|
+
tag: "auto:rage-click",
|
|
759
|
+
key: `rage:${key}`,
|
|
760
|
+
note: `Auto-captured after ${arr.length} rapid clicks on ${label}`
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
hits.set(key, arr);
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function endpointKey(method, url) {
|
|
769
|
+
const m = typeof method === "string" ? method.toUpperCase() : "GET";
|
|
770
|
+
let u = typeof url === "string" ? url : "";
|
|
771
|
+
const q = u.indexOf("?");
|
|
772
|
+
if (q >= 0) u = u.slice(0, q);
|
|
773
|
+
return `${m} ${u}`;
|
|
774
|
+
}
|
|
775
|
+
var MAX_TRACKED_REQUESTS = 1024;
|
|
776
|
+
function retryStormDetector(opts) {
|
|
777
|
+
const failThreshold = opts.failThreshold ?? 2;
|
|
778
|
+
const endpointOf = /* @__PURE__ */ new Map();
|
|
779
|
+
const reqHits = /* @__PURE__ */ new Map();
|
|
780
|
+
const failHits = /* @__PURE__ */ new Map();
|
|
781
|
+
const tripped = (key, count) => ({
|
|
782
|
+
tag: "auto:retry-storm",
|
|
783
|
+
key: `retry:${key}`,
|
|
784
|
+
note: `Auto-captured after ${count} rapid requests to ${key}`
|
|
785
|
+
});
|
|
786
|
+
return {
|
|
787
|
+
inspect(event) {
|
|
788
|
+
if (event.k === "net.req") {
|
|
789
|
+
const key = endpointKey(event.d.method, event.d.url);
|
|
790
|
+
if (typeof event.d.id === "number") {
|
|
791
|
+
endpointOf.set(event.d.id, key);
|
|
792
|
+
if (endpointOf.size > MAX_TRACKED_REQUESTS) {
|
|
793
|
+
const oldest = endpointOf.keys().next().value;
|
|
794
|
+
if (oldest !== void 0) endpointOf.delete(oldest);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const arr = slide(reqHits.get(key), event.t, opts.windowMs);
|
|
798
|
+
if (arr.length >= opts.threshold) {
|
|
799
|
+
reqHits.set(key, []);
|
|
800
|
+
return tripped(key, arr.length);
|
|
801
|
+
}
|
|
802
|
+
reqHits.set(key, arr);
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
if (event.k === "net.res") {
|
|
806
|
+
const id = typeof event.d.id === "number" ? event.d.id : void 0;
|
|
807
|
+
const key = id !== void 0 ? endpointOf.get(id) : void 0;
|
|
808
|
+
if (id !== void 0) endpointOf.delete(id);
|
|
809
|
+
const st = typeof event.d.st === "number" ? event.d.st : 0;
|
|
810
|
+
if (st < 400) return null;
|
|
811
|
+
if (!key) return null;
|
|
812
|
+
const arr = slide(failHits.get(key), event.t, opts.windowMs);
|
|
813
|
+
if (arr.length >= failThreshold) {
|
|
814
|
+
failHits.set(key, []);
|
|
815
|
+
return tripped(key, arr.length);
|
|
816
|
+
}
|
|
817
|
+
failHits.set(key, arr);
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function slowResponseDetector(opts) {
|
|
825
|
+
let hits = [];
|
|
826
|
+
return {
|
|
827
|
+
inspect(event) {
|
|
828
|
+
if (event.k !== "net.res") return null;
|
|
829
|
+
const dur = typeof event.d.dur === "number" ? event.d.dur : 0;
|
|
830
|
+
if (dur < opts.thresholdMs) return null;
|
|
831
|
+
hits = slide(hits, event.t, opts.windowMs);
|
|
832
|
+
if (hits.length >= opts.count) {
|
|
833
|
+
hits = [];
|
|
834
|
+
return {
|
|
835
|
+
tag: "auto:slow-responses",
|
|
836
|
+
key: "slow:session",
|
|
837
|
+
note: `Auto-captured after ${opts.count}+ responses slower than ${opts.thresholdMs}ms`
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
845
|
+
function abandonedFlowDetector(opts) {
|
|
846
|
+
let inputCount = 0;
|
|
847
|
+
let lastInputAt = -Infinity;
|
|
848
|
+
const reset = () => {
|
|
849
|
+
inputCount = 0;
|
|
850
|
+
lastInputAt = -Infinity;
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
inspect(event) {
|
|
854
|
+
if (event.k === "inp") {
|
|
855
|
+
inputCount += 1;
|
|
856
|
+
lastInputAt = event.t;
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
if (event.k === "net.req") {
|
|
860
|
+
const method = typeof event.d.method === "string" ? event.d.method.toUpperCase() : "GET";
|
|
861
|
+
if (MUTATING_METHODS.has(method)) reset();
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
if (event.k === "vis" && event.d.state === "hidden") {
|
|
865
|
+
if (inputCount >= opts.minInputs && event.t - lastInputAt <= opts.windowMs) {
|
|
866
|
+
const count = inputCount;
|
|
867
|
+
reset();
|
|
868
|
+
return {
|
|
869
|
+
tag: "auto:abandoned-flow",
|
|
870
|
+
key: "abandoned:flow",
|
|
871
|
+
note: `Auto-captured after the page was hidden with ${count} unsubmitted input(s)`
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/auto-flag.ts
|
|
882
|
+
function createAutoFlagController(options) {
|
|
883
|
+
const detectors = options.detectors ?? [errorDetector()];
|
|
884
|
+
const seen = /* @__PURE__ */ new Set();
|
|
885
|
+
let flaggedCount = 0;
|
|
886
|
+
let timer = null;
|
|
887
|
+
let pending;
|
|
888
|
+
let disposed = false;
|
|
889
|
+
const fire = () => {
|
|
890
|
+
timer = null;
|
|
891
|
+
const signal = pending;
|
|
892
|
+
pending = void 0;
|
|
893
|
+
if (!signal) return;
|
|
894
|
+
flaggedCount++;
|
|
895
|
+
options.flag({ tags: [signal.tag], note: signal.note }).catch(() => {
|
|
896
|
+
});
|
|
897
|
+
};
|
|
898
|
+
return {
|
|
899
|
+
handleEvent(event) {
|
|
900
|
+
if (disposed) return;
|
|
901
|
+
if (flaggedCount >= options.maxPerSession) return;
|
|
902
|
+
for (const detector of detectors) {
|
|
903
|
+
const signal = detector.inspect(event);
|
|
904
|
+
if (!signal) continue;
|
|
905
|
+
if (seen.has(signal.key)) continue;
|
|
906
|
+
seen.add(signal.key);
|
|
907
|
+
if (pending === void 0) pending = signal;
|
|
908
|
+
if (timer !== null) clearTimeout(timer);
|
|
909
|
+
timer = setTimeout(fire, options.debounceMs);
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
dispose() {
|
|
913
|
+
disposed = true;
|
|
914
|
+
if (timer !== null) {
|
|
915
|
+
clearTimeout(timer);
|
|
916
|
+
timer = null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/utils.ts
|
|
923
|
+
function safeStringify(value, maxDepth = 3) {
|
|
924
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
925
|
+
function process(val, depth) {
|
|
926
|
+
if (val === null || val === void 0) return val;
|
|
927
|
+
const type = typeof val;
|
|
928
|
+
if (type === "string" || type === "number" || type === "boolean")
|
|
929
|
+
return val;
|
|
930
|
+
if (type === "bigint") return val.toString();
|
|
931
|
+
if (type === "symbol") return val.toString();
|
|
932
|
+
if (type === "function")
|
|
933
|
+
return `[Function: ${val.name || "anonymous"}]`;
|
|
934
|
+
if (seen.has(val)) return "[Circular]";
|
|
935
|
+
if (depth > maxDepth) {
|
|
936
|
+
return Array.isArray(val) ? `[Array(${val.length})]` : "[Object]";
|
|
937
|
+
}
|
|
938
|
+
seen.add(val);
|
|
939
|
+
if (val instanceof Error) {
|
|
940
|
+
return { name: val.name, message: val.message, stack: val.stack };
|
|
941
|
+
}
|
|
942
|
+
if (Array.isArray(val)) {
|
|
943
|
+
return val.map((v) => process(v, depth + 1));
|
|
944
|
+
}
|
|
945
|
+
const out = {};
|
|
946
|
+
for (const k of Object.keys(val)) {
|
|
947
|
+
out[k] = process(val[k], depth + 1);
|
|
948
|
+
}
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
return JSON.stringify(process(value, 0));
|
|
953
|
+
} catch {
|
|
954
|
+
try {
|
|
955
|
+
return String(value);
|
|
956
|
+
} catch {
|
|
957
|
+
return "[Unserializable]";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function truncate(str, maxLen) {
|
|
962
|
+
if (str.length <= maxLen) return str;
|
|
963
|
+
return str.slice(0, maxLen);
|
|
964
|
+
}
|
|
965
|
+
function describeElement(el) {
|
|
966
|
+
const desc = { tag: el.tagName };
|
|
967
|
+
if (el.id) desc.id = el.id;
|
|
968
|
+
if (el.className && typeof el.className === "string") {
|
|
969
|
+
desc.cls = truncate(el.className, 200);
|
|
970
|
+
}
|
|
971
|
+
if (el instanceof HTMLElement) {
|
|
972
|
+
const txt = el.innerText ?? el.textContent;
|
|
973
|
+
if (txt) desc.txt = truncate(txt.trim(), 100);
|
|
974
|
+
}
|
|
975
|
+
if (el instanceof HTMLAnchorElement) {
|
|
976
|
+
desc.href = el.href;
|
|
977
|
+
}
|
|
978
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
979
|
+
if (el.name) desc.name = el.name;
|
|
980
|
+
}
|
|
981
|
+
if (el instanceof HTMLInputElement) {
|
|
982
|
+
if (el.type) desc.type = el.type;
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const signature = computeElementSignature(el);
|
|
986
|
+
desc.sig = signature.sig;
|
|
987
|
+
desc.path = signature.path;
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
return desc;
|
|
991
|
+
}
|
|
992
|
+
function generateSessionId() {
|
|
993
|
+
const d = /* @__PURE__ */ new Date();
|
|
994
|
+
const p = (n, len = 2) => String(n).padStart(len, "0");
|
|
995
|
+
return `ses_${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}_${randomHex2(6)}`;
|
|
996
|
+
}
|
|
997
|
+
function randomHex2(byteLength) {
|
|
998
|
+
const bytes = new Uint8Array(byteLength);
|
|
999
|
+
if (!globalThis.crypto?.getRandomValues) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
"Crumbtrail requires crypto.getRandomValues to generate session IDs safely"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1005
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
1006
|
+
""
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
function now() {
|
|
1010
|
+
return Date.now();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/transports/http.ts
|
|
1014
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
1015
|
+
function utf8ByteLength(text) {
|
|
1016
|
+
if (typeof TextEncoder !== "undefined") {
|
|
1017
|
+
return new TextEncoder().encode(text).byteLength;
|
|
1018
|
+
}
|
|
1019
|
+
return text.length;
|
|
1020
|
+
}
|
|
1021
|
+
var HttpTransport = class {
|
|
1022
|
+
sessionId = "";
|
|
1023
|
+
authToken;
|
|
1024
|
+
endpoint;
|
|
1025
|
+
constructor(endpoint, options) {
|
|
1026
|
+
this.endpoint = endpoint.replace(/\/+$/, "");
|
|
1027
|
+
this.authToken = options?.authToken;
|
|
1028
|
+
}
|
|
1029
|
+
withAuthHeaders(headers) {
|
|
1030
|
+
if (!this.authToken) return headers;
|
|
1031
|
+
return { ...headers, "X-Crumbtrail-Auth": this.authToken };
|
|
1032
|
+
}
|
|
1033
|
+
async sendEvents(events) {
|
|
1034
|
+
const body = JSON.stringify({ sessionId: this.sessionId, events });
|
|
1035
|
+
const init = {
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
headers: this.withAuthHeaders({ "Content-Type": "application/json" }),
|
|
1038
|
+
body
|
|
1039
|
+
};
|
|
1040
|
+
if (utf8ByteLength(body) <= KEEPALIVE_MAX_BYTES) init.keepalive = true;
|
|
1041
|
+
try {
|
|
1042
|
+
await fetch(`${this.endpoint}/api/events`, init);
|
|
1043
|
+
} catch {
|
|
1044
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1045
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
1046
|
+
navigator.sendBeacon(`${this.endpoint}/api/events`, blob);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async sendBlob(name, blob, metadata) {
|
|
1051
|
+
const headers = this.withAuthHeaders({
|
|
1052
|
+
"Content-Type": "application/octet-stream",
|
|
1053
|
+
"X-Session-Id": this.sessionId
|
|
1054
|
+
});
|
|
1055
|
+
if (metadata) {
|
|
1056
|
+
headers["X-Metadata"] = JSON.stringify(metadata);
|
|
1057
|
+
}
|
|
1058
|
+
await fetch(`${this.endpoint}/api/blob/${name}`, {
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
headers,
|
|
1061
|
+
body: blob
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async startSession(sessionId, metadata) {
|
|
1065
|
+
this.sessionId = sessionId;
|
|
1066
|
+
await fetch(`${this.endpoint}/api/session/start`, {
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
headers: this.withAuthHeaders({ "Content-Type": "application/json" }),
|
|
1069
|
+
body: JSON.stringify({ sessionId, metadata })
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
async sendBugReport(report, events, voiceBlob) {
|
|
1073
|
+
await fetch(`${this.endpoint}/api/bug/flag`, {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
headers: this.withAuthHeaders({ "Content-Type": "application/json" }),
|
|
1076
|
+
body: JSON.stringify({ report, events })
|
|
1077
|
+
});
|
|
1078
|
+
if (voiceBlob) {
|
|
1079
|
+
await fetch(`${this.endpoint}/api/bug/${report.bugId}/voice`, {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
headers: this.withAuthHeaders({
|
|
1082
|
+
"Content-Type": "application/octet-stream"
|
|
1083
|
+
}),
|
|
1084
|
+
body: voiceBlob
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async endSession(sessionId) {
|
|
1089
|
+
const body = JSON.stringify({ sessionId });
|
|
1090
|
+
try {
|
|
1091
|
+
await fetch(`${this.endpoint}/api/session/end`, {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
headers: this.withAuthHeaders({ "Content-Type": "application/json" }),
|
|
1094
|
+
body
|
|
1095
|
+
});
|
|
1096
|
+
} catch {
|
|
1097
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1098
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
1099
|
+
navigator.sendBeacon(`${this.endpoint}/api/session/end`, blob);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// src/session-store.ts
|
|
1106
|
+
var DEFAULT_SESSION_STORAGE_KEY = "__crumbtrail_session";
|
|
1107
|
+
function createWebSessionStore(storage, key = DEFAULT_SESSION_STORAGE_KEY) {
|
|
1108
|
+
const resolvedStorage = arguments.length === 0 ? getBrowserSessionStorage() : storage;
|
|
1109
|
+
if (!resolvedStorage) return void 0;
|
|
1110
|
+
return {
|
|
1111
|
+
read() {
|
|
1112
|
+
try {
|
|
1113
|
+
const raw = resolvedStorage.getItem(key);
|
|
1114
|
+
if (!raw) return void 0;
|
|
1115
|
+
const parsed = JSON.parse(raw);
|
|
1116
|
+
if (typeof parsed.id !== "string" || parsed.id.length === 0)
|
|
1117
|
+
return void 0;
|
|
1118
|
+
return {
|
|
1119
|
+
id: parsed.id,
|
|
1120
|
+
lastActivity: typeof parsed.lastActivity === "number" ? parsed.lastActivity : 0
|
|
1121
|
+
};
|
|
1122
|
+
} catch {
|
|
1123
|
+
return void 0;
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
write(session) {
|
|
1127
|
+
try {
|
|
1128
|
+
resolvedStorage.setItem(key, JSON.stringify(session));
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function getBrowserSessionStorage() {
|
|
1135
|
+
try {
|
|
1136
|
+
return typeof sessionStorage !== "undefined" && sessionStorage !== null ? sessionStorage : void 0;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return void 0;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/redaction.ts
|
|
1143
|
+
var BROWSER_REDACTION_POLICY = "crumbtrail.browser-redaction.v1";
|
|
1144
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
1145
|
+
var REDACTED_STORAGE_KEY = "[REDACTED_KEY]";
|
|
1146
|
+
var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1147
|
+
"authorization",
|
|
1148
|
+
"cookie",
|
|
1149
|
+
"set-cookie",
|
|
1150
|
+
"proxy-authorization",
|
|
1151
|
+
"x-api-key",
|
|
1152
|
+
"x-auth-token",
|
|
1153
|
+
"x-access-token",
|
|
1154
|
+
"x-csrf-token",
|
|
1155
|
+
"x-xsrf-token",
|
|
1156
|
+
"x-session-id"
|
|
1157
|
+
]);
|
|
1158
|
+
var URL_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1159
|
+
"content-location",
|
|
1160
|
+
"location",
|
|
1161
|
+
"referer",
|
|
1162
|
+
"referrer"
|
|
1163
|
+
]);
|
|
1164
|
+
var MAX_HEADER_COUNT = 80;
|
|
1165
|
+
var MAX_HEADER_NAME_LENGTH = 160;
|
|
1166
|
+
var MAX_HEADER_VALUE_LENGTH = 2e3;
|
|
1167
|
+
var SENSITIVE_NAME_RE = /(^|[^a-z0-9])(access[-_]?token|api[-_]?key|auth|authorization|bearer|card[-_]?number|client[-_]?secret|cookie|credential(s)?|creds|csrf|cvv|cvc|id[-_]?token|jsessionid|jwt|mfa|otp|pass[-_]?phrase|pass(code|word)?|passwd|password[-_]?confirmation|pin|private[-_]?key|pwd|refresh[-_]?token|secret|security[-_]?code|session|session[-_]?id|sid|ssn|token|verification[-_]?code|xsrf)([^a-z0-9]|$)/i;
|
|
1168
|
+
var PII_NAME_RE = /(^|[^a-z0-9])(email|phone|address|dob|birthdate|postal|zip)([^a-z0-9]|$)/i;
|
|
1169
|
+
var SENSITIVE_URL_SCHEMES = /* @__PURE__ */ new Set([
|
|
1170
|
+
"blob:",
|
|
1171
|
+
"data:",
|
|
1172
|
+
"file:",
|
|
1173
|
+
"javascript:"
|
|
1174
|
+
]);
|
|
1175
|
+
var SENSITIVE_COMPACT_NAMES = /* @__PURE__ */ new Set([
|
|
1176
|
+
"accesskey",
|
|
1177
|
+
"accesstoken",
|
|
1178
|
+
"accesstokens",
|
|
1179
|
+
"apikey",
|
|
1180
|
+
"apikeys",
|
|
1181
|
+
"apisecret",
|
|
1182
|
+
"apisecrets",
|
|
1183
|
+
"auth",
|
|
1184
|
+
"authentication",
|
|
1185
|
+
"authenticationinfo",
|
|
1186
|
+
"authkey",
|
|
1187
|
+
"authtoken",
|
|
1188
|
+
"authorization",
|
|
1189
|
+
"authorizationinfo",
|
|
1190
|
+
"bearer",
|
|
1191
|
+
"cardnumber",
|
|
1192
|
+
"clientsecret",
|
|
1193
|
+
"clientsecrets",
|
|
1194
|
+
"cookie",
|
|
1195
|
+
"credentials",
|
|
1196
|
+
"creds",
|
|
1197
|
+
"csrf",
|
|
1198
|
+
"csrfkey",
|
|
1199
|
+
"csrftoken",
|
|
1200
|
+
"cvc",
|
|
1201
|
+
"cvv",
|
|
1202
|
+
"idtoken",
|
|
1203
|
+
"idtokens",
|
|
1204
|
+
"jsessionid",
|
|
1205
|
+
"jwt",
|
|
1206
|
+
"mfa",
|
|
1207
|
+
"otp",
|
|
1208
|
+
"passcode",
|
|
1209
|
+
"passphrase",
|
|
1210
|
+
"passwd",
|
|
1211
|
+
"password",
|
|
1212
|
+
"passwordconfirmation",
|
|
1213
|
+
"passwords",
|
|
1214
|
+
"pin",
|
|
1215
|
+
"privatekey",
|
|
1216
|
+
"proxyauthentication",
|
|
1217
|
+
"proxyauthenticationinfo",
|
|
1218
|
+
"pwd",
|
|
1219
|
+
"refreshtoken",
|
|
1220
|
+
"refreshtokens",
|
|
1221
|
+
"secret",
|
|
1222
|
+
"secrets",
|
|
1223
|
+
"securitycode",
|
|
1224
|
+
"session",
|
|
1225
|
+
"sessionid",
|
|
1226
|
+
"sessiontoken",
|
|
1227
|
+
"sessiontokens",
|
|
1228
|
+
"sid",
|
|
1229
|
+
"ssn",
|
|
1230
|
+
"token",
|
|
1231
|
+
"tokenkey",
|
|
1232
|
+
"tokens",
|
|
1233
|
+
"verificationcode",
|
|
1234
|
+
"xapikey",
|
|
1235
|
+
"xauthkey",
|
|
1236
|
+
"xauthtoken",
|
|
1237
|
+
"xcsrf",
|
|
1238
|
+
"xcsrfkey",
|
|
1239
|
+
"xcsrftoken",
|
|
1240
|
+
"xsrf",
|
|
1241
|
+
"xsrfkey",
|
|
1242
|
+
"xsrftoken",
|
|
1243
|
+
"xxsrf",
|
|
1244
|
+
"xxsrfkey",
|
|
1245
|
+
"xxsrftoken"
|
|
1246
|
+
]);
|
|
1247
|
+
var SENSITIVE_COMPACT_SUFFIXES = [
|
|
1248
|
+
"accesstoken",
|
|
1249
|
+
"accesstokens",
|
|
1250
|
+
"apikey",
|
|
1251
|
+
"apikeys",
|
|
1252
|
+
"apisecret",
|
|
1253
|
+
"apisecrets",
|
|
1254
|
+
"authtoken",
|
|
1255
|
+
"clientsecret",
|
|
1256
|
+
"clientsecrets",
|
|
1257
|
+
"csrftoken",
|
|
1258
|
+
"idtoken",
|
|
1259
|
+
"idtokens",
|
|
1260
|
+
"privatekey",
|
|
1261
|
+
"refreshtoken",
|
|
1262
|
+
"refreshtokens",
|
|
1263
|
+
"sessiontoken",
|
|
1264
|
+
"sessiontokens",
|
|
1265
|
+
"xsrftoken"
|
|
1266
|
+
];
|
|
1267
|
+
var TOKEN_PATTERNS = [
|
|
1268
|
+
{
|
|
1269
|
+
pattern: /\b(?:Bearer|Token|Basic)\s+[A-Za-z0-9._~+/=-]{8,}\b/gi,
|
|
1270
|
+
reason: "auth_scheme_token"
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
1274
|
+
reason: "jwt_token"
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
pattern: /(?:sk|pk|rk|ghp|gho|ghu|ghs|glpat|xox[baprs])[-_][A-Za-z0-9_.=-]{12,}/gi,
|
|
1278
|
+
reason: "prefixed_token"
|
|
1279
|
+
},
|
|
1280
|
+
{ pattern: /\b[A-Fa-f0-9]{32,}\b/g, reason: "long_hex_token" },
|
|
1281
|
+
{ pattern: /\b[A-Za-z0-9_-]{40,}\b/g, reason: "long_token_like_string" }
|
|
1282
|
+
];
|
|
1283
|
+
var REDACTED_KEY = "[REDACTED_KEY]";
|
|
1284
|
+
var SENSITIVE_PATH_PRECEDERS = /* @__PURE__ */ new Set([
|
|
1285
|
+
"code",
|
|
1286
|
+
"invite",
|
|
1287
|
+
"magic",
|
|
1288
|
+
"mfa",
|
|
1289
|
+
"otp",
|
|
1290
|
+
"passcode",
|
|
1291
|
+
"reset",
|
|
1292
|
+
"session",
|
|
1293
|
+
"token",
|
|
1294
|
+
"verify"
|
|
1295
|
+
]);
|
|
1296
|
+
function metadataFromField(field, summary) {
|
|
1297
|
+
return {
|
|
1298
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
1299
|
+
fields: [field],
|
|
1300
|
+
...summary ? { summaries: [summary] } : {}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function metadataFromFields(fields, summaries = []) {
|
|
1304
|
+
if (fields.length === 0 && summaries.length === 0) return void 0;
|
|
1305
|
+
return {
|
|
1306
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
1307
|
+
fields,
|
|
1308
|
+
...summaries.length > 0 ? { summaries } : {}
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function mergeRedactionMetadata(...items) {
|
|
1312
|
+
const fields = [];
|
|
1313
|
+
const summaries = [];
|
|
1314
|
+
for (const item of items) {
|
|
1315
|
+
if (!item) continue;
|
|
1316
|
+
fields.push(...item.fields);
|
|
1317
|
+
if (item.summaries) summaries.push(...item.summaries);
|
|
1318
|
+
}
|
|
1319
|
+
if (fields.length === 0 && summaries.length === 0) return void 0;
|
|
1320
|
+
return {
|
|
1321
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
1322
|
+
fields,
|
|
1323
|
+
...summaries.length > 0 ? { summaries } : {}
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function attachRedactionMetadata(target, ...items) {
|
|
1327
|
+
const metadata = mergeRedactionMetadata(...items);
|
|
1328
|
+
if (metadata) target.redaction = metadata;
|
|
1329
|
+
}
|
|
1330
|
+
function withMetadata(value, field, summary) {
|
|
1331
|
+
return {
|
|
1332
|
+
value,
|
|
1333
|
+
...field ? { metadata: metadataFromField(field, summary) } : {},
|
|
1334
|
+
...summary ? { summary } : {}
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function isSensitiveName(name) {
|
|
1338
|
+
if (!name) return false;
|
|
1339
|
+
const normalized = name.replace(/([a-z])([A-Z])/g, "$1_$2");
|
|
1340
|
+
const compact = normalized.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1341
|
+
return SENSITIVE_NAME_RE.test(name) || PII_NAME_RE.test(name) || SENSITIVE_NAME_RE.test(normalized) || PII_NAME_RE.test(normalized) || isSensitiveCompactName(compact);
|
|
1342
|
+
}
|
|
1343
|
+
function isSensitiveCompactName(compact) {
|
|
1344
|
+
return SENSITIVE_COMPACT_NAMES.has(compact) || SENSITIVE_COMPACT_SUFFIXES.some(
|
|
1345
|
+
(suffix) => compact.length > suffix.length && compact.endsWith(suffix)
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
function buildSummary(kind, action, reason, originalLength, limit, redactedFields, contentLength) {
|
|
1349
|
+
return {
|
|
1350
|
+
kind,
|
|
1351
|
+
action,
|
|
1352
|
+
reason,
|
|
1353
|
+
...originalLength !== void 0 ? { originalLength } : {},
|
|
1354
|
+
...limit !== void 0 ? { limit } : {},
|
|
1355
|
+
...redactedFields !== void 0 ? { redactedFields } : {},
|
|
1356
|
+
...contentLength !== void 0 ? { contentLength } : {}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function redactTokenLikeString(value, path = "value") {
|
|
1360
|
+
let output = value;
|
|
1361
|
+
const fields = [];
|
|
1362
|
+
for (const { pattern, reason } of TOKEN_PATTERNS) {
|
|
1363
|
+
pattern.lastIndex = 0;
|
|
1364
|
+
let matched = false;
|
|
1365
|
+
output = output.replace(pattern, () => {
|
|
1366
|
+
matched = true;
|
|
1367
|
+
return REDACTED_VALUE;
|
|
1368
|
+
});
|
|
1369
|
+
if (matched) fields.push({ path, reason, action: "redacted" });
|
|
1370
|
+
}
|
|
1371
|
+
const metadata = metadataFromFields(fields);
|
|
1372
|
+
return { value: output, ...metadata ? { metadata } : {} };
|
|
1373
|
+
}
|
|
1374
|
+
function redactQueryString(query, path) {
|
|
1375
|
+
if (!query) return { value: "" };
|
|
1376
|
+
const search = query.startsWith("?") ? query.slice(1) : query;
|
|
1377
|
+
const params = new URLSearchParams(search);
|
|
1378
|
+
const fields = [];
|
|
1379
|
+
for (const key of Array.from(params.keys())) {
|
|
1380
|
+
const values = params.getAll(key);
|
|
1381
|
+
const safeKey = sanitizeKeyName(key);
|
|
1382
|
+
params.delete(key);
|
|
1383
|
+
for (const value of values) {
|
|
1384
|
+
if (value === "") {
|
|
1385
|
+
params.append(safeKey, "");
|
|
1386
|
+
} else {
|
|
1387
|
+
params.append(safeKey, REDACTED_VALUE);
|
|
1388
|
+
fields.push({
|
|
1389
|
+
path: `${path}.query.${safeKey}`,
|
|
1390
|
+
reason: "url_query_value",
|
|
1391
|
+
action: "redacted"
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const serialized = params.toString();
|
|
1397
|
+
const metadata = metadataFromFields(fields);
|
|
1398
|
+
return {
|
|
1399
|
+
value: serialized ? `?${serialized}` : "",
|
|
1400
|
+
...metadata ? { metadata } : {}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function sanitizeKeyName(key) {
|
|
1404
|
+
return redactTokenLikeString(key).value === key ? key : REDACTED_KEY;
|
|
1405
|
+
}
|
|
1406
|
+
function uniqueOutputKey(key, output) {
|
|
1407
|
+
if (!Object.prototype.hasOwnProperty.call(output, key)) return key;
|
|
1408
|
+
let suffix = 2;
|
|
1409
|
+
while (Object.prototype.hasOwnProperty.call(output, `${key}_${suffix}`))
|
|
1410
|
+
suffix += 1;
|
|
1411
|
+
return `${key}_${suffix}`;
|
|
1412
|
+
}
|
|
1413
|
+
function redactUrlPath(pathname, path) {
|
|
1414
|
+
if (!pathname || pathname === "/") return { value: pathname };
|
|
1415
|
+
const fields = [];
|
|
1416
|
+
let previousDecoded = "";
|
|
1417
|
+
const parts = pathname.split("/");
|
|
1418
|
+
const output = parts.map((part, index) => {
|
|
1419
|
+
if (part === "") return part;
|
|
1420
|
+
const decoded = decodeURIComponentDeep(part);
|
|
1421
|
+
const subResult = redactUrlPathComponent(
|
|
1422
|
+
decoded,
|
|
1423
|
+
previousDecoded,
|
|
1424
|
+
`${path}.path`
|
|
1425
|
+
);
|
|
1426
|
+
previousDecoded = subResult.lastToken || decoded.toLowerCase();
|
|
1427
|
+
if (subResult.metadata) fields.push(...subResult.metadata.fields);
|
|
1428
|
+
return subResult.value === decoded ? part : encodeURIComponent(subResult.value);
|
|
1429
|
+
});
|
|
1430
|
+
const metadata = metadataFromFields(fields);
|
|
1431
|
+
return { value: output.join("/"), ...metadata ? { metadata } : {} };
|
|
1432
|
+
}
|
|
1433
|
+
function redactUrlPathComponent(component, previous, path) {
|
|
1434
|
+
const fields = [];
|
|
1435
|
+
let lastToken = previous;
|
|
1436
|
+
if ((SENSITIVE_PATH_PRECEDERS.has(previous) || isSensitiveName(previous)) && component.length > 0) {
|
|
1437
|
+
fields.push({
|
|
1438
|
+
path,
|
|
1439
|
+
reason: "url_path_secret_segment",
|
|
1440
|
+
action: "redacted"
|
|
1441
|
+
});
|
|
1442
|
+
return {
|
|
1443
|
+
value: REDACTED_VALUE,
|
|
1444
|
+
lastToken: REDACTED_VALUE.toLowerCase(),
|
|
1445
|
+
metadata: { policy: BROWSER_REDACTION_POLICY, fields }
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
const parts = component.split(/([/\\;])/);
|
|
1449
|
+
const output = parts.map((part) => {
|
|
1450
|
+
if (part === "/" || part === "\\" || part === ";") return part;
|
|
1451
|
+
if (part === "") return part;
|
|
1452
|
+
const keyValueIndex = part.indexOf("=");
|
|
1453
|
+
if (keyValueIndex > 0) {
|
|
1454
|
+
const key = part.slice(0, keyValueIndex);
|
|
1455
|
+
const value = part.slice(keyValueIndex + 1);
|
|
1456
|
+
const tokenResult2 = redactTokenLikeString(value, path);
|
|
1457
|
+
if (isSensitiveName(key) || tokenResult2.value !== value || isSecretLikePathSegment(value, key.toLowerCase())) {
|
|
1458
|
+
fields.push({
|
|
1459
|
+
path,
|
|
1460
|
+
reason: isSensitiveName(key) ? "url_path_sensitive_key" : "url_path_token",
|
|
1461
|
+
action: "redacted"
|
|
1462
|
+
});
|
|
1463
|
+
lastToken = key.toLowerCase();
|
|
1464
|
+
return `${key}=${REDACTED_VALUE}`;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const tokenResult = redactTokenLikeString(part, path);
|
|
1468
|
+
if (tokenResult.value !== part || isSecretLikePathSegment(part, lastToken)) {
|
|
1469
|
+
fields.push({
|
|
1470
|
+
path,
|
|
1471
|
+
reason: tokenResult.value !== part ? "url_path_token" : "url_path_secret_segment",
|
|
1472
|
+
action: "redacted"
|
|
1473
|
+
});
|
|
1474
|
+
lastToken = REDACTED_VALUE.toLowerCase();
|
|
1475
|
+
return REDACTED_VALUE;
|
|
1476
|
+
}
|
|
1477
|
+
if (part.includes("?") || part.includes("&")) {
|
|
1478
|
+
const decodedQueryResult = redactDecodedQueryLikePathComponent(
|
|
1479
|
+
part,
|
|
1480
|
+
path,
|
|
1481
|
+
lastToken
|
|
1482
|
+
);
|
|
1483
|
+
if (decodedQueryResult.metadata)
|
|
1484
|
+
fields.push(...decodedQueryResult.metadata.fields);
|
|
1485
|
+
lastToken = decodedQueryResult.lastToken;
|
|
1486
|
+
return decodedQueryResult.value;
|
|
1487
|
+
}
|
|
1488
|
+
lastToken = part.toLowerCase();
|
|
1489
|
+
return part;
|
|
1490
|
+
}).join("");
|
|
1491
|
+
const metadata = metadataFromFields(fields);
|
|
1492
|
+
return { value: output, lastToken, ...metadata ? { metadata } : {} };
|
|
1493
|
+
}
|
|
1494
|
+
function redactDecodedQueryLikePathComponent(component, path, previous) {
|
|
1495
|
+
const fields = [];
|
|
1496
|
+
let lastToken = previous;
|
|
1497
|
+
let inDecodedQuery = false;
|
|
1498
|
+
const output = component.split(/([?&])/).map((part) => {
|
|
1499
|
+
if (part === "?" || part === "&") {
|
|
1500
|
+
inDecodedQuery = true;
|
|
1501
|
+
return part;
|
|
1502
|
+
}
|
|
1503
|
+
if (!inDecodedQuery || part === "") return part;
|
|
1504
|
+
const keyValueIndex = part.indexOf("=");
|
|
1505
|
+
if (keyValueIndex > 0) {
|
|
1506
|
+
const rawKey = part.slice(0, keyValueIndex);
|
|
1507
|
+
const rawValue = part.slice(keyValueIndex + 1);
|
|
1508
|
+
const safeKey = sanitizeKeyName(rawKey);
|
|
1509
|
+
lastToken = safeKey.toLowerCase();
|
|
1510
|
+
if (rawValue === "") return `${safeKey}=`;
|
|
1511
|
+
fields.push({
|
|
1512
|
+
path: `${path}.decoded_query.${safeKey}`,
|
|
1513
|
+
reason: "url_path_decoded_query_value",
|
|
1514
|
+
action: "redacted"
|
|
1515
|
+
});
|
|
1516
|
+
return `${safeKey}=${REDACTED_VALUE}`;
|
|
1517
|
+
}
|
|
1518
|
+
fields.push({
|
|
1519
|
+
path: `${path}.decoded_query`,
|
|
1520
|
+
reason: "url_path_decoded_query_value",
|
|
1521
|
+
action: "redacted"
|
|
1522
|
+
});
|
|
1523
|
+
lastToken = REDACTED_VALUE.toLowerCase();
|
|
1524
|
+
return REDACTED_VALUE;
|
|
1525
|
+
}).join("");
|
|
1526
|
+
const metadata = metadataFromFields(fields);
|
|
1527
|
+
return { value: output, lastToken, ...metadata ? { metadata } : {} };
|
|
1528
|
+
}
|
|
1529
|
+
function decodeURIComponentSafe(value) {
|
|
1530
|
+
try {
|
|
1531
|
+
return decodeURIComponent(value);
|
|
1532
|
+
} catch {
|
|
1533
|
+
return value;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function decodeURIComponentDeep(value) {
|
|
1537
|
+
let output = value;
|
|
1538
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1539
|
+
const decoded = decodeURIComponentSafe(output);
|
|
1540
|
+
if (decoded === output) return output;
|
|
1541
|
+
output = decoded;
|
|
1542
|
+
}
|
|
1543
|
+
return output;
|
|
1544
|
+
}
|
|
1545
|
+
function isSecretLikePathSegment(segment2, previous) {
|
|
1546
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
1547
|
+
segment2
|
|
1548
|
+
))
|
|
1549
|
+
return true;
|
|
1550
|
+
if ((SENSITIVE_PATH_PRECEDERS.has(previous) || isSensitiveName(previous)) && segment2.length > 0 && segment2.length <= 256)
|
|
1551
|
+
return true;
|
|
1552
|
+
return /^[A-Za-z0-9_-]{16,39}$/.test(segment2) && /[A-Z0-9_-]/.test(segment2);
|
|
1553
|
+
}
|
|
1554
|
+
function redactRelativeUrl(url, path) {
|
|
1555
|
+
const hashIndex = url.indexOf("#");
|
|
1556
|
+
const beforeHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
1557
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
1558
|
+
const queryIndex = beforeHash.indexOf("?");
|
|
1559
|
+
const base = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
|
|
1560
|
+
const query = queryIndex >= 0 ? beforeHash.slice(queryIndex) : "";
|
|
1561
|
+
const queryResult = redactQueryString(query, path);
|
|
1562
|
+
const pathResult = redactUrlPath(base, path);
|
|
1563
|
+
const tokenResult = redactTokenLikeString(
|
|
1564
|
+
`${pathResult.value}${queryResult.value}`,
|
|
1565
|
+
path
|
|
1566
|
+
);
|
|
1567
|
+
const fields = [];
|
|
1568
|
+
if (queryResult.metadata) fields.push(...queryResult.metadata.fields);
|
|
1569
|
+
if (pathResult.metadata) fields.push(...pathResult.metadata.fields);
|
|
1570
|
+
if (tokenResult.metadata)
|
|
1571
|
+
fields.push(
|
|
1572
|
+
...tokenResult.metadata.fields.map((field) => ({
|
|
1573
|
+
...field,
|
|
1574
|
+
reason: `url_${field.reason}`
|
|
1575
|
+
}))
|
|
1576
|
+
);
|
|
1577
|
+
if (hash)
|
|
1578
|
+
fields.push({
|
|
1579
|
+
path: `${path}.hash`,
|
|
1580
|
+
reason: "url_hash",
|
|
1581
|
+
action: "dropped"
|
|
1582
|
+
});
|
|
1583
|
+
const metadata = metadataFromFields(fields);
|
|
1584
|
+
return { value: tokenResult.value, ...metadata ? { metadata } : {} };
|
|
1585
|
+
}
|
|
1586
|
+
function redactMalformedAbsoluteUrl(path) {
|
|
1587
|
+
return withMetadata(REDACTED_VALUE, {
|
|
1588
|
+
path,
|
|
1589
|
+
reason: "malformed_absolute_url",
|
|
1590
|
+
action: "redacted"
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function redactUrl(url, path = "url") {
|
|
1594
|
+
if (url.trim().startsWith("//")) {
|
|
1595
|
+
const leadingWhitespace2 = url.match(/^\s*/)?.[0] ?? "";
|
|
1596
|
+
const trimmed = url.trim();
|
|
1597
|
+
try {
|
|
1598
|
+
const parsed = new URL(`https:${trimmed}`);
|
|
1599
|
+
const fields = [];
|
|
1600
|
+
if (parsed.username || parsed.password) {
|
|
1601
|
+
parsed.username = "";
|
|
1602
|
+
parsed.password = "";
|
|
1603
|
+
fields.push({
|
|
1604
|
+
path: `${path}.credentials`,
|
|
1605
|
+
reason: "url_credentials",
|
|
1606
|
+
action: "dropped"
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
if (parsed.search) {
|
|
1610
|
+
const queryResult = redactQueryString(parsed.search, path);
|
|
1611
|
+
parsed.search = queryResult.value;
|
|
1612
|
+
if (queryResult.metadata) fields.push(...queryResult.metadata.fields);
|
|
1613
|
+
}
|
|
1614
|
+
const pathResult = redactUrlPath(parsed.pathname, path);
|
|
1615
|
+
parsed.pathname = pathResult.value;
|
|
1616
|
+
if (pathResult.metadata) fields.push(...pathResult.metadata.fields);
|
|
1617
|
+
if (parsed.hash) {
|
|
1618
|
+
parsed.hash = "";
|
|
1619
|
+
fields.push({
|
|
1620
|
+
path: `${path}.hash`,
|
|
1621
|
+
reason: "url_hash",
|
|
1622
|
+
action: "dropped"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const withoutScheme = `//${parsed.host}${parsed.pathname}${parsed.search}`;
|
|
1626
|
+
const tokenResult = redactTokenLikeString(
|
|
1627
|
+
`${leadingWhitespace2}${withoutScheme}`,
|
|
1628
|
+
path
|
|
1629
|
+
);
|
|
1630
|
+
if (tokenResult.metadata)
|
|
1631
|
+
fields.push(
|
|
1632
|
+
...tokenResult.metadata.fields.map((field) => ({
|
|
1633
|
+
...field,
|
|
1634
|
+
reason: `url_${field.reason}`
|
|
1635
|
+
}))
|
|
1636
|
+
);
|
|
1637
|
+
const metadata = metadataFromFields(fields);
|
|
1638
|
+
return { value: tokenResult.value, ...metadata ? { metadata } : {} };
|
|
1639
|
+
} catch {
|
|
1640
|
+
return redactMalformedAbsoluteUrl(path);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const leadingWhitespace = url.match(/^\s*/)?.[0] ?? "";
|
|
1644
|
+
const trimmedUrl = url.slice(leadingWhitespace.length);
|
|
1645
|
+
const hasScheme = /^[a-z][a-z\d+.-]*:/i.test(trimmedUrl);
|
|
1646
|
+
if (!hasScheme) return redactRelativeUrl(url, path);
|
|
1647
|
+
try {
|
|
1648
|
+
const parsed = new URL(trimmedUrl);
|
|
1649
|
+
const fields = [];
|
|
1650
|
+
if (SENSITIVE_URL_SCHEMES.has(parsed.protocol.toLowerCase())) {
|
|
1651
|
+
const summary = `${parsed.protocol}${REDACTED_VALUE}`;
|
|
1652
|
+
return withMetadata(`${leadingWhitespace}${summary}`, {
|
|
1653
|
+
path,
|
|
1654
|
+
reason: "sensitive_url_scheme",
|
|
1655
|
+
action: "redacted"
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
if (parsed.username || parsed.password) {
|
|
1659
|
+
parsed.username = "";
|
|
1660
|
+
parsed.password = "";
|
|
1661
|
+
fields.push({
|
|
1662
|
+
path: `${path}.credentials`,
|
|
1663
|
+
reason: "url_credentials",
|
|
1664
|
+
action: "dropped"
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
if (parsed.search) {
|
|
1668
|
+
const queryResult = redactQueryString(parsed.search, path);
|
|
1669
|
+
parsed.search = queryResult.value;
|
|
1670
|
+
if (queryResult.metadata) fields.push(...queryResult.metadata.fields);
|
|
1671
|
+
}
|
|
1672
|
+
const pathResult = redactUrlPath(parsed.pathname, path);
|
|
1673
|
+
parsed.pathname = pathResult.value;
|
|
1674
|
+
if (pathResult.metadata) fields.push(...pathResult.metadata.fields);
|
|
1675
|
+
if (parsed.hash) {
|
|
1676
|
+
parsed.hash = "";
|
|
1677
|
+
fields.push({
|
|
1678
|
+
path: `${path}.hash`,
|
|
1679
|
+
reason: "url_hash",
|
|
1680
|
+
action: "dropped"
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
const tokenResult = redactTokenLikeString(
|
|
1684
|
+
`${leadingWhitespace}${parsed.toString()}`,
|
|
1685
|
+
path
|
|
1686
|
+
);
|
|
1687
|
+
if (tokenResult.metadata) {
|
|
1688
|
+
fields.push(
|
|
1689
|
+
...tokenResult.metadata.fields.map((field) => ({
|
|
1690
|
+
...field,
|
|
1691
|
+
reason: `url_${field.reason}`
|
|
1692
|
+
}))
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
const metadata = metadataFromFields(fields);
|
|
1696
|
+
return { value: tokenResult.value, ...metadata ? { metadata } : {} };
|
|
1697
|
+
} catch {
|
|
1698
|
+
return redactMalformedAbsoluteUrl(path);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
var URL_IN_TEXT_RE = /https?:\/\/[^\s"'`<>\\{}()[\]|^]+/gi;
|
|
1702
|
+
var URL_TRAILING_PUNCT_RE = /[.,;:!?]+$/;
|
|
1703
|
+
function redactUrlsInText(value, path = "value") {
|
|
1704
|
+
if (value.indexOf("://") === -1) return { value };
|
|
1705
|
+
const fields = [];
|
|
1706
|
+
const output = value.replace(URL_IN_TEXT_RE, (match) => {
|
|
1707
|
+
const trailing = match.match(URL_TRAILING_PUNCT_RE)?.[0] ?? "";
|
|
1708
|
+
const core = trailing ? match.slice(0, match.length - trailing.length) : match;
|
|
1709
|
+
const result = redactUrl(core, path);
|
|
1710
|
+
if (result.metadata) fields.push(...result.metadata.fields);
|
|
1711
|
+
return `${result.value}${trailing}`;
|
|
1712
|
+
});
|
|
1713
|
+
const metadata = metadataFromFields(fields);
|
|
1714
|
+
return { value: output, ...metadata ? { metadata } : {} };
|
|
1715
|
+
}
|
|
1716
|
+
function redactHeaders(headers, path = "headers") {
|
|
1717
|
+
const output = /* @__PURE__ */ Object.create(null);
|
|
1718
|
+
const fields = [];
|
|
1719
|
+
let processed = 0;
|
|
1720
|
+
for (const originalName in headers) {
|
|
1721
|
+
if (!Object.prototype.hasOwnProperty.call(headers, originalName)) continue;
|
|
1722
|
+
if (processed >= MAX_HEADER_COUNT) {
|
|
1723
|
+
fields.push({
|
|
1724
|
+
path: `${path}.__truncatedHeaders`,
|
|
1725
|
+
reason: "header_count_limit",
|
|
1726
|
+
action: "dropped"
|
|
1727
|
+
});
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
processed += 1;
|
|
1731
|
+
const value = headers[originalName];
|
|
1732
|
+
const name = originalName.slice(0, MAX_HEADER_NAME_LENGTH);
|
|
1733
|
+
const rawValue = typeof value === "string" ? value.slice(0, MAX_HEADER_VALUE_LENGTH) : String(value).slice(0, MAX_HEADER_VALUE_LENGTH);
|
|
1734
|
+
if (name !== originalName) {
|
|
1735
|
+
fields.push({
|
|
1736
|
+
path: `${path}.${sanitizeKeyName(name)}`,
|
|
1737
|
+
reason: "header_name_truncated",
|
|
1738
|
+
action: "summarized"
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
if (rawValue !== value) {
|
|
1742
|
+
fields.push({
|
|
1743
|
+
path: `${path}.${sanitizeKeyName(name)}`,
|
|
1744
|
+
reason: "header_value_truncated",
|
|
1745
|
+
action: "summarized"
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
const normalized = name.toLowerCase();
|
|
1749
|
+
const sanitizedName = sanitizeKeyName(name);
|
|
1750
|
+
const outputName = uniqueOutputKey(sanitizedName, output);
|
|
1751
|
+
if (sanitizedName !== name || SENSITIVE_HEADER_NAMES.has(normalized) || isSensitiveName(normalized)) {
|
|
1752
|
+
output[outputName] = REDACTED_VALUE;
|
|
1753
|
+
fields.push({
|
|
1754
|
+
path: `${path}.${outputName}`,
|
|
1755
|
+
reason: "sensitive_header_name",
|
|
1756
|
+
action: "redacted"
|
|
1757
|
+
});
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
const valueResult = URL_HEADER_NAMES.has(normalized) ? redactUrl(rawValue, `${path}.${outputName}`) : normalized === "link" || headerValueLooksUrlLike(rawValue) ? redactUrlLikeHeaderValue(rawValue, `${path}.${outputName}`) : redactTokenLikeString(rawValue, `${path}.${outputName}`);
|
|
1761
|
+
output[outputName] = valueResult.value;
|
|
1762
|
+
if (valueResult.metadata) fields.push(...valueResult.metadata.fields);
|
|
1763
|
+
}
|
|
1764
|
+
const metadata = metadataFromFields(fields);
|
|
1765
|
+
return { value: output, ...metadata ? { metadata } : {} };
|
|
1766
|
+
}
|
|
1767
|
+
function headerValueLooksUrlLike(value) {
|
|
1768
|
+
return /^\s*(?:https?:\/\/|\/\/|[./]*[^ \t\r\n;,]+[/?#][^ \t\r\n]*)/i.test(
|
|
1769
|
+
value
|
|
1770
|
+
) || /\bhttps?:\/\/[^\s,;]+/i.test(value) || /\burl\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/i.test(value);
|
|
1771
|
+
}
|
|
1772
|
+
function redactUrlLikeHeaderValue(value, path) {
|
|
1773
|
+
const fields = [];
|
|
1774
|
+
let output = value.replace(
|
|
1775
|
+
/<([^>]+)>|https?:\/\/[^\s,;]+/gi,
|
|
1776
|
+
(match, bracketed) => {
|
|
1777
|
+
const rawUrl = bracketed ?? match;
|
|
1778
|
+
const result = redactUrl(rawUrl, path);
|
|
1779
|
+
if (result.metadata) fields.push(...result.metadata.fields);
|
|
1780
|
+
return bracketed === void 0 ? result.value : `<${result.value}>`;
|
|
1781
|
+
}
|
|
1782
|
+
);
|
|
1783
|
+
output = output.replace(
|
|
1784
|
+
/\burl(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s,;]+))/gi,
|
|
1785
|
+
(_match, separator, doubleQuoted, singleQuoted, unquoted) => {
|
|
1786
|
+
const rawUrl = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
|
|
1787
|
+
const quote = doubleQuoted !== void 0 ? '"' : singleQuoted !== void 0 ? "'" : "";
|
|
1788
|
+
const result = redactUrl(rawUrl, path);
|
|
1789
|
+
if (result.metadata) fields.push(...result.metadata.fields);
|
|
1790
|
+
return `url${separator}${quote}${result.value}${quote}`;
|
|
1791
|
+
}
|
|
1792
|
+
);
|
|
1793
|
+
output = output.replace(
|
|
1794
|
+
/^(\s*)((?:\/|\.\.?\/)[^\s,;]+)/,
|
|
1795
|
+
(_match, prefix, rawUrl) => {
|
|
1796
|
+
const result = redactUrl(rawUrl, path);
|
|
1797
|
+
if (result.metadata) fields.push(...result.metadata.fields);
|
|
1798
|
+
return `${prefix}${result.value}`;
|
|
1799
|
+
}
|
|
1800
|
+
);
|
|
1801
|
+
const tokenResult = redactTokenLikeString(output, path);
|
|
1802
|
+
if (tokenResult.metadata) fields.push(...tokenResult.metadata.fields);
|
|
1803
|
+
const metadata = metadataFromFields(fields);
|
|
1804
|
+
return { value: tokenResult.value, ...metadata ? { metadata } : {} };
|
|
1805
|
+
}
|
|
1806
|
+
function redactCookieValue(name, value, path = `cookies.${name}`, configuredMaskNames = []) {
|
|
1807
|
+
if (value === "") return { value: "" };
|
|
1808
|
+
const configured = configuredMaskNames.includes(name);
|
|
1809
|
+
const safeName = sanitizeKeyName(name);
|
|
1810
|
+
const safePath = path.replace(name, safeName);
|
|
1811
|
+
const summary = buildSummary(
|
|
1812
|
+
"cookie",
|
|
1813
|
+
"redacted",
|
|
1814
|
+
configured ? "configured_cookie_mask" : "cookie_value",
|
|
1815
|
+
value.length
|
|
1816
|
+
);
|
|
1817
|
+
return withMetadata(
|
|
1818
|
+
REDACTED_VALUE,
|
|
1819
|
+
{
|
|
1820
|
+
path: safePath,
|
|
1821
|
+
reason: configured ? "configured_cookie_mask" : "cookie_value",
|
|
1822
|
+
action: "redacted"
|
|
1823
|
+
},
|
|
1824
|
+
summary
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
function redactCookieName(name) {
|
|
1828
|
+
return sanitizeKeyName(name);
|
|
1829
|
+
}
|
|
1830
|
+
function redactCookieMap(cookies, path = "cookies", configuredMaskNames = []) {
|
|
1831
|
+
const output = {};
|
|
1832
|
+
const metadataItems = [];
|
|
1833
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
1834
|
+
const safeName = sanitizeKeyName(name);
|
|
1835
|
+
const result = redactCookieValue(
|
|
1836
|
+
name,
|
|
1837
|
+
value,
|
|
1838
|
+
`${path}.${safeName}`,
|
|
1839
|
+
configuredMaskNames
|
|
1840
|
+
);
|
|
1841
|
+
output[safeName] = result.value;
|
|
1842
|
+
metadataItems.push(result.metadata);
|
|
1843
|
+
}
|
|
1844
|
+
const metadata = mergeRedactionMetadata(...metadataItems);
|
|
1845
|
+
return { value: output, ...metadata ? { metadata } : {} };
|
|
1846
|
+
}
|
|
1847
|
+
function isJsonContentType(contentType) {
|
|
1848
|
+
const lower = contentType.toLowerCase();
|
|
1849
|
+
return lower.includes("application/json") || lower.includes("+json");
|
|
1850
|
+
}
|
|
1851
|
+
function isFormContentType(contentType) {
|
|
1852
|
+
return contentType.toLowerCase().includes("application/x-www-form-urlencoded");
|
|
1853
|
+
}
|
|
1854
|
+
function isMarkupContentType(contentType) {
|
|
1855
|
+
const lower = contentType.toLowerCase();
|
|
1856
|
+
return lower.includes("application/xml") || lower.includes("text/xml") || lower.includes("+xml") || lower.includes("text/html") || lower.includes("multipart/form-data");
|
|
1857
|
+
}
|
|
1858
|
+
function looksLikeJson(body) {
|
|
1859
|
+
const trimmed = body.trim();
|
|
1860
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
1861
|
+
}
|
|
1862
|
+
function redactJsonValue(value, path, keyName) {
|
|
1863
|
+
if (keyName && isSensitiveName(keyName)) {
|
|
1864
|
+
return {
|
|
1865
|
+
value: REDACTED_VALUE,
|
|
1866
|
+
fields: [{ path, reason: "sensitive_json_field", action: "redacted" }]
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
if (typeof value === "string") {
|
|
1870
|
+
const urlResult = redactUrlsInText(value, path);
|
|
1871
|
+
const result = redactTokenLikeString(urlResult.value, path);
|
|
1872
|
+
return {
|
|
1873
|
+
value: result.value,
|
|
1874
|
+
fields: [
|
|
1875
|
+
...urlResult.metadata?.fields ?? [],
|
|
1876
|
+
...result.metadata?.fields ?? []
|
|
1877
|
+
]
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
if (Array.isArray(value)) {
|
|
1881
|
+
const fields = [];
|
|
1882
|
+
const output = value.map((entry, index) => {
|
|
1883
|
+
const result = redactJsonValue(entry, `${path}[${index}]`);
|
|
1884
|
+
fields.push(...result.fields);
|
|
1885
|
+
return result.value;
|
|
1886
|
+
});
|
|
1887
|
+
return { value: output, fields };
|
|
1888
|
+
}
|
|
1889
|
+
if (value !== null && typeof value === "object") {
|
|
1890
|
+
const fields = [];
|
|
1891
|
+
const output = {};
|
|
1892
|
+
for (const [key, entry] of Object.entries(
|
|
1893
|
+
value
|
|
1894
|
+
)) {
|
|
1895
|
+
const safeKey = sanitizeKeyName(key);
|
|
1896
|
+
if (safeKey !== key) {
|
|
1897
|
+
fields.push({
|
|
1898
|
+
path: `${path}.${safeKey}`,
|
|
1899
|
+
reason: "json_key_token_like",
|
|
1900
|
+
action: "redacted"
|
|
1901
|
+
});
|
|
1902
|
+
output[safeKey] = REDACTED_VALUE;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
const result = redactJsonValue(entry, `${path}.${safeKey}`, key);
|
|
1906
|
+
fields.push(...result.fields);
|
|
1907
|
+
output[safeKey] = result.value;
|
|
1908
|
+
}
|
|
1909
|
+
return { value: output, fields };
|
|
1910
|
+
}
|
|
1911
|
+
return { value, fields: [] };
|
|
1912
|
+
}
|
|
1913
|
+
function redactValue(value, path = "value") {
|
|
1914
|
+
const result = redactJsonValue(value, path);
|
|
1915
|
+
const metadata = metadataFromFields(result.fields);
|
|
1916
|
+
return { value: result.value, ...metadata ? { metadata } : {} };
|
|
1917
|
+
}
|
|
1918
|
+
function redactFormBody(body, path) {
|
|
1919
|
+
const params = new URLSearchParams(body);
|
|
1920
|
+
const fields = [];
|
|
1921
|
+
for (const key of Array.from(params.keys())) {
|
|
1922
|
+
const values = params.getAll(key);
|
|
1923
|
+
const safeKey = sanitizeKeyName(key);
|
|
1924
|
+
params.delete(key);
|
|
1925
|
+
for (const value of values) {
|
|
1926
|
+
if (value === "") {
|
|
1927
|
+
params.append(safeKey, "");
|
|
1928
|
+
} else {
|
|
1929
|
+
params.append(safeKey, REDACTED_VALUE);
|
|
1930
|
+
fields.push({
|
|
1931
|
+
path: `${path}.${safeKey}`,
|
|
1932
|
+
reason: "form_value",
|
|
1933
|
+
action: "redacted"
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (fields.length === 0) return { body };
|
|
1939
|
+
const summary = buildSummary(
|
|
1940
|
+
"form",
|
|
1941
|
+
"redacted",
|
|
1942
|
+
"form_value",
|
|
1943
|
+
body.length,
|
|
1944
|
+
void 0,
|
|
1945
|
+
fields.length
|
|
1946
|
+
);
|
|
1947
|
+
return {
|
|
1948
|
+
body: params.toString(),
|
|
1949
|
+
bodySummary: summary,
|
|
1950
|
+
metadata: {
|
|
1951
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
1952
|
+
fields,
|
|
1953
|
+
summaries: [summary]
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
function redactTextKeyValueBody(body, path) {
|
|
1958
|
+
const parts = body.split(/([&;\n\r])/);
|
|
1959
|
+
const fields = [];
|
|
1960
|
+
let changed = false;
|
|
1961
|
+
let sawKeyValue = false;
|
|
1962
|
+
const output = parts.map((part) => {
|
|
1963
|
+
if (part === "&" || part === ";" || part === "\n" || part === "\r")
|
|
1964
|
+
return part;
|
|
1965
|
+
const match = part.match(/^([^:=\n\r]{1,120})([:=])(.*)$/s);
|
|
1966
|
+
if (!match) {
|
|
1967
|
+
return part;
|
|
1968
|
+
}
|
|
1969
|
+
sawKeyValue = true;
|
|
1970
|
+
const [, rawKey, delimiter, rawValue] = match;
|
|
1971
|
+
const safeKey = sanitizeKeyName(rawKey.trim());
|
|
1972
|
+
const sensitive = isSensitiveName(rawKey);
|
|
1973
|
+
const valueResult = sensitive ? { value: REDACTED_VALUE } : redactTokenLikeString(rawValue, `${path}.${safeKey}`);
|
|
1974
|
+
if (safeKey !== rawKey.trim() || valueResult.value !== rawValue)
|
|
1975
|
+
changed = true;
|
|
1976
|
+
if (sensitive || valueResult.value !== rawValue)
|
|
1977
|
+
fields.push({
|
|
1978
|
+
path: `${path}.${safeKey}`,
|
|
1979
|
+
reason: sensitive ? "text_sensitive_field" : "text_token_like_value",
|
|
1980
|
+
action: "redacted"
|
|
1981
|
+
});
|
|
1982
|
+
return `${safeKey}${delimiter}${valueResult.value}`;
|
|
1983
|
+
});
|
|
1984
|
+
if (!sawKeyValue || !changed) return void 0;
|
|
1985
|
+
const bodySummary = buildSummary(
|
|
1986
|
+
"text",
|
|
1987
|
+
"redacted",
|
|
1988
|
+
"text_key_value_fields",
|
|
1989
|
+
body.length,
|
|
1990
|
+
void 0,
|
|
1991
|
+
fields.length
|
|
1992
|
+
);
|
|
1993
|
+
const metadata = metadataFromFields(fields, [bodySummary]);
|
|
1994
|
+
return { body: output.join(""), bodySummary, metadata };
|
|
1995
|
+
}
|
|
1996
|
+
function redactMarkupTextBody(body, path) {
|
|
1997
|
+
const fields = [];
|
|
1998
|
+
let output = body.replace(
|
|
1999
|
+
/<((?:[\w.-]+:)?(?:access[-_]?token|api[-_]?key|auth|authorization|card[-_]?number|client[-_]?secret|credential|credentials|csrf|cvc|cvv|id[-_]?token|jsessionid|jwt|otp|pass[-_]?phrase|passcode|passwd|password|pin|private[-_]?key|pwd|refresh[-_]?token|secret|security[-_]?code|session[-_]?id|sid|token|verification[-_]?code|xsrf))(\s[^>]*)?>[\s\S]*?<\/\1>/gi,
|
|
2000
|
+
(match, tag, attrs) => {
|
|
2001
|
+
if (!isSensitiveName(tag)) return match;
|
|
2002
|
+
fields.push({
|
|
2003
|
+
path: `${path}.${sanitizeKeyName(tag)}`,
|
|
2004
|
+
reason: "markup_sensitive_tag",
|
|
2005
|
+
action: "redacted"
|
|
2006
|
+
});
|
|
2007
|
+
return `<${tag}${attrs ?? ""}>${REDACTED_VALUE}</${tag}>`;
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
2010
|
+
output = output.replace(
|
|
2011
|
+
/<([A-Za-z][\w:.-]{0,119})([^>]*)>/gi,
|
|
2012
|
+
(match, tag, attrs) => {
|
|
2013
|
+
const marker = readSensitiveMarkupMarker(attrs);
|
|
2014
|
+
if (!marker) return match;
|
|
2015
|
+
const redactedAttrs = replaceSensitiveMarkupPayloadAttributes(attrs);
|
|
2016
|
+
if (redactedAttrs === attrs) return match;
|
|
2017
|
+
fields.push({
|
|
2018
|
+
path: `${path}.${sanitizeKeyName(marker.value)}`,
|
|
2019
|
+
reason: "markup_sensitive_payload_attribute",
|
|
2020
|
+
action: "redacted"
|
|
2021
|
+
});
|
|
2022
|
+
return `<${tag}${redactedAttrs}>`;
|
|
2023
|
+
}
|
|
2024
|
+
);
|
|
2025
|
+
output = output.replace(
|
|
2026
|
+
/<((?:textarea|select|option)\b)([^>]*)>([\s\S]*?)<\/\1>/gi,
|
|
2027
|
+
(match, tag, attrs) => {
|
|
2028
|
+
const marker = readSensitiveMarkupMarker(attrs);
|
|
2029
|
+
if (!marker) return match;
|
|
2030
|
+
fields.push({
|
|
2031
|
+
path: `${path}.${sanitizeKeyName(marker.value)}`,
|
|
2032
|
+
reason: "markup_sensitive_control_text",
|
|
2033
|
+
action: "redacted"
|
|
2034
|
+
});
|
|
2035
|
+
return `<${tag}${attrs}>${REDACTED_VALUE}</${tag}>`;
|
|
2036
|
+
}
|
|
2037
|
+
);
|
|
2038
|
+
output = output.replace(
|
|
2039
|
+
/<([A-Za-z][\w:.-]{0,119})(\s[^>]*)?>([^<]*)<\/\1>/gi,
|
|
2040
|
+
(match, tag, attrs) => {
|
|
2041
|
+
if (!isSensitiveName(tag)) return match;
|
|
2042
|
+
fields.push({
|
|
2043
|
+
path: `${path}.${sanitizeKeyName(tag)}`,
|
|
2044
|
+
reason: "markup_sensitive_tag",
|
|
2045
|
+
action: "redacted"
|
|
2046
|
+
});
|
|
2047
|
+
return `<${tag}${attrs ?? ""}>${REDACTED_VALUE}</${tag}>`;
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2050
|
+
output = output.replace(
|
|
2051
|
+
/([A-Za-z_:][\w:.-]{0,119})(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi,
|
|
2052
|
+
(match, name, eq, doubleQuoted, singleQuoted, _unquoted) => {
|
|
2053
|
+
if (!isSensitiveName(name)) return match;
|
|
2054
|
+
const quote = doubleQuoted !== void 0 ? '"' : singleQuoted !== void 0 ? "'" : "";
|
|
2055
|
+
fields.push({
|
|
2056
|
+
path: `${path}.${sanitizeKeyName(name)}`,
|
|
2057
|
+
reason: "markup_sensitive_attribute",
|
|
2058
|
+
action: "redacted"
|
|
2059
|
+
});
|
|
2060
|
+
return `${name}${eq}${quote}${REDACTED_VALUE}${quote}`;
|
|
2061
|
+
}
|
|
2062
|
+
);
|
|
2063
|
+
output = output.replace(
|
|
2064
|
+
/(name\s*=\s*)(?:"([^"]+)"|'([^']+)'|([^;\s\r\n]+))([\s\S]{0,256}?)(\r?\n\r?\n)([\s\S]*?)(?=\r?\n--|$)/gi,
|
|
2065
|
+
(match, prefix, doubleQuoted, singleQuoted, unquoted, between, separator) => {
|
|
2066
|
+
const name = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
|
|
2067
|
+
if (!isSensitiveName(name)) return match;
|
|
2068
|
+
const quote = doubleQuoted !== void 0 ? '"' : singleQuoted !== void 0 ? "'" : "";
|
|
2069
|
+
fields.push({
|
|
2070
|
+
path: `${path}.${sanitizeKeyName(name)}`,
|
|
2071
|
+
reason: "multipart_sensitive_field",
|
|
2072
|
+
action: "redacted"
|
|
2073
|
+
});
|
|
2074
|
+
return `${prefix}${quote}${name}${quote}${between}${separator}${REDACTED_VALUE}`;
|
|
2075
|
+
}
|
|
2076
|
+
);
|
|
2077
|
+
if (fields.length === 0) return void 0;
|
|
2078
|
+
const bodySummary = buildSummary(
|
|
2079
|
+
"text",
|
|
2080
|
+
"redacted",
|
|
2081
|
+
"markup_sensitive_fields",
|
|
2082
|
+
body.length,
|
|
2083
|
+
void 0,
|
|
2084
|
+
fields.length
|
|
2085
|
+
);
|
|
2086
|
+
const metadata = metadataFromFields(fields, [bodySummary]);
|
|
2087
|
+
return { body: output, bodySummary, metadata };
|
|
2088
|
+
}
|
|
2089
|
+
function readMarkupAttributes(attrs, names) {
|
|
2090
|
+
const wanted = new Set(names.map((name) => name.toLowerCase()));
|
|
2091
|
+
const attributes = [];
|
|
2092
|
+
const attrPattern = /([A-Za-z_:][\w:.-]{0,119})\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
|
|
2093
|
+
let match;
|
|
2094
|
+
while ((match = attrPattern.exec(attrs)) !== null) {
|
|
2095
|
+
const name = match[1];
|
|
2096
|
+
if (!wanted.has(name.toLowerCase())) continue;
|
|
2097
|
+
attributes.push({ name, value: match[2] ?? match[3] ?? match[4] ?? "" });
|
|
2098
|
+
}
|
|
2099
|
+
return attributes;
|
|
2100
|
+
}
|
|
2101
|
+
function readSensitiveMarkupMarker(attrs) {
|
|
2102
|
+
for (const marker of readMarkupAttributes(attrs, [
|
|
2103
|
+
"name",
|
|
2104
|
+
"id",
|
|
2105
|
+
"autocomplete",
|
|
2106
|
+
"type"
|
|
2107
|
+
])) {
|
|
2108
|
+
const value = marker.value.toLowerCase();
|
|
2109
|
+
if (isSensitiveName(marker.value) || value === "hidden" || value === "password")
|
|
2110
|
+
return marker;
|
|
2111
|
+
}
|
|
2112
|
+
return void 0;
|
|
2113
|
+
}
|
|
2114
|
+
function replaceSensitiveMarkupPayloadAttributes(attrs) {
|
|
2115
|
+
const attrPattern = /([A-Za-z_:][\w:.-]{0,119})(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
|
|
2116
|
+
return attrs.replace(
|
|
2117
|
+
attrPattern,
|
|
2118
|
+
(match, name, eq, doubleQuoted, singleQuoted) => {
|
|
2119
|
+
const normalized = name.toLowerCase();
|
|
2120
|
+
if (normalized !== "value" && normalized !== "content" && normalized !== "href" && normalized !== "src" && !normalized.startsWith("data-"))
|
|
2121
|
+
return match;
|
|
2122
|
+
const quote = doubleQuoted !== void 0 ? '"' : singleQuoted !== void 0 ? "'" : "";
|
|
2123
|
+
return `${name}${eq}${quote}${REDACTED_VALUE}${quote}`;
|
|
2124
|
+
}
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
function redactNetworkTextBody(body, options = {}) {
|
|
2128
|
+
const path = options.path ?? "body";
|
|
2129
|
+
const contentType = options.contentType ?? "";
|
|
2130
|
+
const maxLength = options.maxLength;
|
|
2131
|
+
const kind = isFormContentType(contentType) ? "form" : isJsonContentType(contentType) || looksLikeJson(body) ? "json" : "text";
|
|
2132
|
+
if (maxLength !== void 0 && maxLength >= 0 && body.length > maxLength) {
|
|
2133
|
+
const summary2 = buildSummary(
|
|
2134
|
+
kind,
|
|
2135
|
+
"summarized",
|
|
2136
|
+
"payload_too_large",
|
|
2137
|
+
body.length,
|
|
2138
|
+
maxLength
|
|
2139
|
+
);
|
|
2140
|
+
const metadata = metadataFromField(
|
|
2141
|
+
{ path, reason: "payload_too_large", action: "summarized" },
|
|
2142
|
+
summary2
|
|
2143
|
+
);
|
|
2144
|
+
return { bodySummary: summary2, metadata };
|
|
2145
|
+
}
|
|
2146
|
+
if (kind === "form") return redactFormBody(body, path);
|
|
2147
|
+
if (kind === "json") {
|
|
2148
|
+
try {
|
|
2149
|
+
const parsed = JSON.parse(body);
|
|
2150
|
+
const result = redactJsonValue(parsed, path);
|
|
2151
|
+
if (result.fields.length === 0) return { body };
|
|
2152
|
+
const summary2 = buildSummary(
|
|
2153
|
+
"json",
|
|
2154
|
+
"redacted",
|
|
2155
|
+
"sensitive_json_field",
|
|
2156
|
+
body.length,
|
|
2157
|
+
void 0,
|
|
2158
|
+
result.fields.length
|
|
2159
|
+
);
|
|
2160
|
+
return {
|
|
2161
|
+
body: JSON.stringify(result.value),
|
|
2162
|
+
bodySummary: summary2,
|
|
2163
|
+
metadata: {
|
|
2164
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
2165
|
+
fields: result.fields,
|
|
2166
|
+
summaries: [summary2]
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
} catch {
|
|
2170
|
+
const summary2 = buildSummary(
|
|
2171
|
+
"json",
|
|
2172
|
+
"dropped",
|
|
2173
|
+
"malformed_json_body",
|
|
2174
|
+
body.length
|
|
2175
|
+
);
|
|
2176
|
+
const metadata = metadataFromField(
|
|
2177
|
+
{ path, reason: "malformed_json_body", action: "dropped" },
|
|
2178
|
+
summary2
|
|
2179
|
+
);
|
|
2180
|
+
return { bodySummary: summary2, metadata };
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (isMarkupContentType(contentType)) {
|
|
2184
|
+
const markupResult = redactMarkupTextBody(body, path);
|
|
2185
|
+
if (markupResult) return markupResult;
|
|
2186
|
+
}
|
|
2187
|
+
const urlInTextResult = redactUrlsInText(body, path);
|
|
2188
|
+
const urlFields = urlInTextResult.metadata?.fields ?? [];
|
|
2189
|
+
const workingBody = urlInTextResult.value;
|
|
2190
|
+
const keyValueResult = redactTextKeyValueBody(workingBody, path);
|
|
2191
|
+
if (keyValueResult) {
|
|
2192
|
+
if (urlFields.length === 0) return keyValueResult;
|
|
2193
|
+
const mergedFields = [
|
|
2194
|
+
...urlFields,
|
|
2195
|
+
...keyValueResult.metadata?.fields ?? []
|
|
2196
|
+
];
|
|
2197
|
+
const summaries = keyValueResult.metadata?.summaries ?? [];
|
|
2198
|
+
return {
|
|
2199
|
+
...keyValueResult,
|
|
2200
|
+
metadata: {
|
|
2201
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
2202
|
+
fields: mergedFields,
|
|
2203
|
+
...summaries.length > 0 ? { summaries } : {}
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
const textResult = redactTokenLikeString(workingBody, path);
|
|
2208
|
+
const tokenFields = textResult.metadata?.fields ?? [];
|
|
2209
|
+
const allFields = [...urlFields, ...tokenFields];
|
|
2210
|
+
if (allFields.length === 0) return { body };
|
|
2211
|
+
const reason = tokenFields.length > 0 ? "token_like_value" : "url_query_value";
|
|
2212
|
+
const summary = buildSummary(
|
|
2213
|
+
"text",
|
|
2214
|
+
"redacted",
|
|
2215
|
+
reason,
|
|
2216
|
+
body.length,
|
|
2217
|
+
void 0,
|
|
2218
|
+
allFields.length
|
|
2219
|
+
);
|
|
2220
|
+
return {
|
|
2221
|
+
body: textResult.value,
|
|
2222
|
+
bodySummary: summary,
|
|
2223
|
+
metadata: {
|
|
2224
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
2225
|
+
fields: allFields,
|
|
2226
|
+
summaries: [summary]
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
function summarizeBinaryPayload(contentType, contentLength, path = "body") {
|
|
2231
|
+
const reason = contentType ? `binary_payload:${contentType}` : "binary_payload";
|
|
2232
|
+
const summary = buildSummary(
|
|
2233
|
+
"binary",
|
|
2234
|
+
"summarized",
|
|
2235
|
+
reason,
|
|
2236
|
+
void 0,
|
|
2237
|
+
void 0,
|
|
2238
|
+
void 0,
|
|
2239
|
+
contentLength ?? void 0
|
|
2240
|
+
);
|
|
2241
|
+
const metadata = metadataFromField(
|
|
2242
|
+
{ path, reason: "binary_payload", action: "summarized" },
|
|
2243
|
+
summary
|
|
2244
|
+
);
|
|
2245
|
+
return {
|
|
2246
|
+
body: contentLength ? `[bin:${contentLength}]` : "[bin]",
|
|
2247
|
+
bodySummary: summary,
|
|
2248
|
+
metadata
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function summarizeOmittedPayload(reason, path = "body") {
|
|
2252
|
+
const kind = reason === "stream_payload" ? "stream" : "unknown";
|
|
2253
|
+
const summary = buildSummary(kind, "dropped", reason);
|
|
2254
|
+
const metadata = metadataFromField(
|
|
2255
|
+
{ path, reason, action: "dropped" },
|
|
2256
|
+
summary
|
|
2257
|
+
);
|
|
2258
|
+
return { bodySummary: summary, metadata };
|
|
2259
|
+
}
|
|
2260
|
+
function redactStorageKey(key, path = "storage.key") {
|
|
2261
|
+
if (isSensitiveName(key)) {
|
|
2262
|
+
return withMetadata(REDACTED_STORAGE_KEY, {
|
|
2263
|
+
path,
|
|
2264
|
+
reason: "sensitive_storage_key",
|
|
2265
|
+
action: "redacted"
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
const tokenResult = redactTokenLikeString(key, path);
|
|
2269
|
+
if (tokenResult.metadata) {
|
|
2270
|
+
return {
|
|
2271
|
+
value: REDACTED_STORAGE_KEY,
|
|
2272
|
+
metadata: {
|
|
2273
|
+
policy: BROWSER_REDACTION_POLICY,
|
|
2274
|
+
fields: tokenResult.metadata.fields.map((field) => ({
|
|
2275
|
+
...field,
|
|
2276
|
+
reason: "storage_key_token_like"
|
|
2277
|
+
}))
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
return { value: key };
|
|
2282
|
+
}
|
|
2283
|
+
function redactStoredValue(value, options = {}) {
|
|
2284
|
+
if (value == null) return { value: void 0 };
|
|
2285
|
+
if (value === "") return { value: "" };
|
|
2286
|
+
const path = options.path ?? "storage.value";
|
|
2287
|
+
const reason = options.maxLength !== void 0 && value.length > options.maxLength ? "storage_value_too_large" : options.key && isSensitiveName(options.key) ? "sensitive_storage_value" : "storage_value";
|
|
2288
|
+
const summary = buildSummary(
|
|
2289
|
+
"storage",
|
|
2290
|
+
"redacted",
|
|
2291
|
+
reason,
|
|
2292
|
+
value.length,
|
|
2293
|
+
options.maxLength
|
|
2294
|
+
);
|
|
2295
|
+
return withMetadata(
|
|
2296
|
+
REDACTED_VALUE,
|
|
2297
|
+
{ path, reason, action: "redacted" },
|
|
2298
|
+
summary
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
function redactInputValue(value, options = {}) {
|
|
2302
|
+
if (value === "") return { value: "" };
|
|
2303
|
+
const path = options.path ?? "input.value";
|
|
2304
|
+
const type = options.type?.toLowerCase();
|
|
2305
|
+
const reason = type === "password" || type === "email" || type === "tel" || type === "search" || isSensitiveName(options.name) ? "sensitive_input_value" : "input_value";
|
|
2306
|
+
const summary = buildSummary("input", "redacted", reason, value.length);
|
|
2307
|
+
return withMetadata(
|
|
2308
|
+
REDACTED_VALUE,
|
|
2309
|
+
{ path, reason, action: "redacted" },
|
|
2310
|
+
summary
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/collectors/console.ts
|
|
2315
|
+
var LEVEL_MAP = {
|
|
2316
|
+
log: "log",
|
|
2317
|
+
warn: "warn",
|
|
2318
|
+
error: "err",
|
|
2319
|
+
debug: "dbg",
|
|
2320
|
+
info: "info"
|
|
2321
|
+
};
|
|
2322
|
+
var METHODS = ["log", "warn", "error", "debug", "info"];
|
|
2323
|
+
function redactConsoleArg(value, path) {
|
|
2324
|
+
if (typeof value === "string") {
|
|
2325
|
+
const result2 = redactNetworkTextBody(value, {
|
|
2326
|
+
contentType: "text/plain",
|
|
2327
|
+
path
|
|
2328
|
+
});
|
|
2329
|
+
return {
|
|
2330
|
+
value: safeStringify(result2.body ?? ""),
|
|
2331
|
+
metadata: result2.metadata
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
const serialized = safeStringify(value);
|
|
2335
|
+
const result = redactNetworkTextBody(serialized, {
|
|
2336
|
+
contentType: "application/json",
|
|
2337
|
+
path
|
|
2338
|
+
});
|
|
2339
|
+
return {
|
|
2340
|
+
value: result.body ?? serialized,
|
|
2341
|
+
metadata: result.metadata
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
function redactConsoleStack(stack) {
|
|
2345
|
+
if (!stack) return {};
|
|
2346
|
+
const result = redactNetworkTextBody(stack, {
|
|
2347
|
+
contentType: "text/plain",
|
|
2348
|
+
path: "stk"
|
|
2349
|
+
});
|
|
2350
|
+
return { value: result.body ?? stack, metadata: result.metadata };
|
|
2351
|
+
}
|
|
2352
|
+
function consoleCollector(bus, config) {
|
|
2353
|
+
const originals = /* @__PURE__ */ new Map();
|
|
2354
|
+
for (const method of METHODS) {
|
|
2355
|
+
originals.set(method, console[method]);
|
|
2356
|
+
console[method] = (...args) => {
|
|
2357
|
+
const redactedArgs = config.captureRawConsole ? args.map((a) => safeStringify(a)) : args.map((a, index) => redactConsoleArg(a, `args[${index}]`));
|
|
2358
|
+
const stack = method === "error" ? config.captureRawConsole ? { value: new Error().stack } : redactConsoleStack(new Error().stack) : void 0;
|
|
2359
|
+
const d = {
|
|
2360
|
+
lv: LEVEL_MAP[method],
|
|
2361
|
+
args: redactedArgs.map(
|
|
2362
|
+
(arg) => typeof arg === "string" ? arg : arg.value
|
|
2363
|
+
)
|
|
2364
|
+
};
|
|
2365
|
+
if (method === "error") d.stk = stack?.value;
|
|
2366
|
+
if (!config.captureRawConsole) {
|
|
2367
|
+
attachRedactionMetadata(
|
|
2368
|
+
d,
|
|
2369
|
+
...redactedArgs.map(
|
|
2370
|
+
(arg) => typeof arg === "string" ? void 0 : arg.metadata
|
|
2371
|
+
),
|
|
2372
|
+
stack?.metadata
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
bus.emit({ t: now(), k: "con", d });
|
|
2376
|
+
originals.get(method).apply(console, args);
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
return () => {
|
|
2380
|
+
for (const method of METHODS) {
|
|
2381
|
+
console[method] = originals.get(method);
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// src/collectors/error.ts
|
|
2387
|
+
function bodyPlaceholder(summary) {
|
|
2388
|
+
return summary ? `[${summary.action}:${summary.reason}]` : "[REDACTED]";
|
|
2389
|
+
}
|
|
2390
|
+
function redactText(value, path) {
|
|
2391
|
+
if (value == null) return {};
|
|
2392
|
+
const result = redactNetworkTextBody(value, {
|
|
2393
|
+
contentType: "text/plain",
|
|
2394
|
+
path
|
|
2395
|
+
});
|
|
2396
|
+
return {
|
|
2397
|
+
value: result.body ?? bodyPlaceholder(result.bodySummary),
|
|
2398
|
+
metadata: result.metadata
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function redactErrorPayload(payload, config) {
|
|
2402
|
+
if (config.captureRawErrors) return payload;
|
|
2403
|
+
const msg = redactText(
|
|
2404
|
+
typeof payload.msg === "string" ? payload.msg : void 0,
|
|
2405
|
+
"msg"
|
|
2406
|
+
);
|
|
2407
|
+
const stk = redactText(
|
|
2408
|
+
typeof payload.stk === "string" ? payload.stk : void 0,
|
|
2409
|
+
"stk"
|
|
2410
|
+
);
|
|
2411
|
+
const file = typeof payload.file === "string" ? redactUrl(payload.file, "file") : void 0;
|
|
2412
|
+
const d = {
|
|
2413
|
+
...payload,
|
|
2414
|
+
...msg.value !== void 0 ? { msg: msg.value } : {},
|
|
2415
|
+
...stk.value !== void 0 ? { stk: stk.value } : {},
|
|
2416
|
+
...file ? { file: file.value } : {}
|
|
2417
|
+
};
|
|
2418
|
+
attachRedactionMetadata(d, msg.metadata, stk.metadata, file?.metadata);
|
|
2419
|
+
return d;
|
|
2420
|
+
}
|
|
2421
|
+
function errorCollector(bus, config) {
|
|
2422
|
+
const onError = (event) => {
|
|
2423
|
+
bus.emit({
|
|
2424
|
+
t: now(),
|
|
2425
|
+
k: "err",
|
|
2426
|
+
d: redactErrorPayload(
|
|
2427
|
+
{
|
|
2428
|
+
msg: event.message,
|
|
2429
|
+
file: event.filename,
|
|
2430
|
+
line: event.lineno,
|
|
2431
|
+
col: event.colno,
|
|
2432
|
+
stk: event.error?.stack
|
|
2433
|
+
},
|
|
2434
|
+
config
|
|
2435
|
+
)
|
|
2436
|
+
});
|
|
2437
|
+
};
|
|
2438
|
+
const onRejection = (event) => {
|
|
2439
|
+
const reason = event.reason;
|
|
2440
|
+
bus.emit({
|
|
2441
|
+
t: now(),
|
|
2442
|
+
k: "rej",
|
|
2443
|
+
d: redactErrorPayload(
|
|
2444
|
+
{
|
|
2445
|
+
msg: reason instanceof Error ? reason.message : String(reason),
|
|
2446
|
+
stk: reason instanceof Error ? reason.stack : void 0
|
|
2447
|
+
},
|
|
2448
|
+
config
|
|
2449
|
+
)
|
|
2450
|
+
});
|
|
2451
|
+
};
|
|
2452
|
+
window.addEventListener("error", onError);
|
|
2453
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
2454
|
+
return () => {
|
|
2455
|
+
window.removeEventListener("error", onError);
|
|
2456
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// src/collectors/interaction.ts
|
|
2461
|
+
function describeInteractionTarget(target, config) {
|
|
2462
|
+
try {
|
|
2463
|
+
const descriptor = config.describeInteractionElement?.(target) ?? describeElement(target);
|
|
2464
|
+
if (isRecord(descriptor)) return removeUndefined(descriptor);
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
return {
|
|
2468
|
+
tag: target.tagName,
|
|
2469
|
+
descriptorError: "interaction_descriptor_failed"
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function readDescriptorMetadata(descriptor) {
|
|
2473
|
+
const redaction = descriptor.redaction;
|
|
2474
|
+
if (!isRecord(redaction)) return void 0;
|
|
2475
|
+
if (redaction.policy !== BROWSER_REDACTION_POLICY) return void 0;
|
|
2476
|
+
if (!Array.isArray(redaction.fields)) return void 0;
|
|
2477
|
+
return redaction;
|
|
2478
|
+
}
|
|
2479
|
+
function isRecord(value) {
|
|
2480
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2481
|
+
}
|
|
2482
|
+
function removeUndefined(value) {
|
|
2483
|
+
return Object.fromEntries(
|
|
2484
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
function readSafeOrigin(url) {
|
|
2488
|
+
if (!url) return void 0;
|
|
2489
|
+
try {
|
|
2490
|
+
const parsed = new URL(url, window.location.href);
|
|
2491
|
+
return parsed.origin === "null" ? void 0 : parsed.origin;
|
|
2492
|
+
} catch {
|
|
2493
|
+
return void 0;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function isTopFrame() {
|
|
2497
|
+
try {
|
|
2498
|
+
return window.self === window.top;
|
|
2499
|
+
} catch {
|
|
2500
|
+
return false;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
function describeFrameContext(url) {
|
|
2504
|
+
return removeUndefined({
|
|
2505
|
+
top: isTopFrame(),
|
|
2506
|
+
origin: readSafeOrigin(url)
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
function interactionCollector(bus, config) {
|
|
2510
|
+
const cleanups = [];
|
|
2511
|
+
const onClick = (e) => {
|
|
2512
|
+
const target = e.target;
|
|
2513
|
+
if (!(target instanceof Element)) return;
|
|
2514
|
+
for (const sel of config.ignoreSelectors) {
|
|
2515
|
+
if (target.matches(sel)) return;
|
|
2516
|
+
}
|
|
2517
|
+
const el = describeInteractionTarget(target, config);
|
|
2518
|
+
const d = {
|
|
2519
|
+
el,
|
|
2520
|
+
pos: [e.clientX, e.clientY]
|
|
2521
|
+
};
|
|
2522
|
+
attachRedactionMetadata(d, readDescriptorMetadata(el));
|
|
2523
|
+
bus.emit({
|
|
2524
|
+
t: now(),
|
|
2525
|
+
k: "clk",
|
|
2526
|
+
d
|
|
2527
|
+
});
|
|
2528
|
+
};
|
|
2529
|
+
document.addEventListener("click", onClick, true);
|
|
2530
|
+
cleanups.push(() => document.removeEventListener("click", onClick, true));
|
|
2531
|
+
const onInput = (e) => {
|
|
2532
|
+
const target = e.target;
|
|
2533
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement))
|
|
2534
|
+
return;
|
|
2535
|
+
const el = describeInteractionTarget(target, config);
|
|
2536
|
+
const type = target instanceof HTMLInputElement ? target.type : void 0;
|
|
2537
|
+
const val = redactInputValue(target.value, {
|
|
2538
|
+
name: target.name || void 0,
|
|
2539
|
+
type,
|
|
2540
|
+
path: "val"
|
|
2541
|
+
});
|
|
2542
|
+
const d = {
|
|
2543
|
+
el,
|
|
2544
|
+
val: val.value,
|
|
2545
|
+
ev: e.type
|
|
2546
|
+
};
|
|
2547
|
+
if (val.summary) d.valSummary = val.summary;
|
|
2548
|
+
attachRedactionMetadata(d, readDescriptorMetadata(el), val.metadata);
|
|
2549
|
+
bus.emit({
|
|
2550
|
+
t: now(),
|
|
2551
|
+
k: "inp",
|
|
2552
|
+
d
|
|
2553
|
+
});
|
|
2554
|
+
};
|
|
2555
|
+
document.addEventListener("input", onInput, true);
|
|
2556
|
+
document.addEventListener("change", onInput, true);
|
|
2557
|
+
cleanups.push(() => {
|
|
2558
|
+
document.removeEventListener("input", onInput, true);
|
|
2559
|
+
document.removeEventListener("change", onInput, true);
|
|
2560
|
+
});
|
|
2561
|
+
const onSubmit = (e) => {
|
|
2562
|
+
const target = e.target;
|
|
2563
|
+
if (!(target instanceof HTMLFormElement)) return;
|
|
2564
|
+
const el = describeInteractionTarget(target, config);
|
|
2565
|
+
const d = {
|
|
2566
|
+
el,
|
|
2567
|
+
val: "",
|
|
2568
|
+
ev: "submit"
|
|
2569
|
+
};
|
|
2570
|
+
attachRedactionMetadata(d, readDescriptorMetadata(el));
|
|
2571
|
+
bus.emit({
|
|
2572
|
+
t: now(),
|
|
2573
|
+
k: "inp",
|
|
2574
|
+
d
|
|
2575
|
+
});
|
|
2576
|
+
};
|
|
2577
|
+
document.addEventListener("submit", onSubmit, true);
|
|
2578
|
+
cleanups.push(() => document.removeEventListener("submit", onSubmit, true));
|
|
2579
|
+
let currentUrl = window.location.href;
|
|
2580
|
+
const emitNav = (to, from, tr) => {
|
|
2581
|
+
const toResult = redactUrl(to, "to");
|
|
2582
|
+
const fromResult = from ? redactUrl(from, "from") : void 0;
|
|
2583
|
+
const d = removeUndefined({
|
|
2584
|
+
from: fromResult?.value ?? "",
|
|
2585
|
+
to: toResult.value,
|
|
2586
|
+
tr,
|
|
2587
|
+
fromOrigin: from ? readSafeOrigin(from) : void 0,
|
|
2588
|
+
toOrigin: readSafeOrigin(to),
|
|
2589
|
+
frame: describeFrameContext(to)
|
|
2590
|
+
});
|
|
2591
|
+
attachRedactionMetadata(d, fromResult?.metadata, toResult.metadata);
|
|
2592
|
+
bus.emit({ t: now(), k: "nav", d });
|
|
2593
|
+
};
|
|
2594
|
+
emitNav(currentUrl, "", "init");
|
|
2595
|
+
const origPushState = history.pushState.bind(history);
|
|
2596
|
+
const origReplaceState = history.replaceState.bind(history);
|
|
2597
|
+
history.pushState = function(...args) {
|
|
2598
|
+
const from = currentUrl;
|
|
2599
|
+
origPushState(...args);
|
|
2600
|
+
currentUrl = window.location.href;
|
|
2601
|
+
emitNav(currentUrl, from, "push");
|
|
2602
|
+
};
|
|
2603
|
+
history.replaceState = function(...args) {
|
|
2604
|
+
const from = currentUrl;
|
|
2605
|
+
origReplaceState(...args);
|
|
2606
|
+
currentUrl = window.location.href;
|
|
2607
|
+
emitNav(currentUrl, from, "replace");
|
|
2608
|
+
};
|
|
2609
|
+
const onPopState = () => {
|
|
2610
|
+
const from = currentUrl;
|
|
2611
|
+
currentUrl = window.location.href;
|
|
2612
|
+
emitNav(currentUrl, from, "pop");
|
|
2613
|
+
};
|
|
2614
|
+
window.addEventListener("popstate", onPopState);
|
|
2615
|
+
const onHashChange = () => {
|
|
2616
|
+
const from = currentUrl;
|
|
2617
|
+
currentUrl = window.location.href;
|
|
2618
|
+
emitNav(currentUrl, from, "hash");
|
|
2619
|
+
};
|
|
2620
|
+
window.addEventListener("hashchange", onHashChange);
|
|
2621
|
+
cleanups.push(() => {
|
|
2622
|
+
history.pushState = origPushState;
|
|
2623
|
+
history.replaceState = origReplaceState;
|
|
2624
|
+
window.removeEventListener("popstate", onPopState);
|
|
2625
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
2626
|
+
});
|
|
2627
|
+
return () => {
|
|
2628
|
+
for (const fn of cleanups) fn();
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/collectors/keystroke.ts
|
|
2633
|
+
var SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
|
|
2634
|
+
"cc-additional-name",
|
|
2635
|
+
"cc-csc",
|
|
2636
|
+
"cc-exp",
|
|
2637
|
+
"cc-exp-month",
|
|
2638
|
+
"cc-exp-year",
|
|
2639
|
+
"cc-family-name",
|
|
2640
|
+
"cc-given-name",
|
|
2641
|
+
"cc-name",
|
|
2642
|
+
"cc-number",
|
|
2643
|
+
"cc-type",
|
|
2644
|
+
"current-password",
|
|
2645
|
+
"email",
|
|
2646
|
+
"new-password",
|
|
2647
|
+
"one-time-code",
|
|
2648
|
+
"tel",
|
|
2649
|
+
"tel-area-code",
|
|
2650
|
+
"tel-country-code",
|
|
2651
|
+
"tel-extension",
|
|
2652
|
+
"tel-local",
|
|
2653
|
+
"tel-local-prefix",
|
|
2654
|
+
"tel-local-suffix",
|
|
2655
|
+
"tel-national",
|
|
2656
|
+
"transaction-amount",
|
|
2657
|
+
"transaction-currency",
|
|
2658
|
+
"username"
|
|
2659
|
+
]);
|
|
2660
|
+
function keystrokeCollector(bus, config) {
|
|
2661
|
+
let lastKeystrokeTs = 0;
|
|
2662
|
+
let lastKeyupTs = 0;
|
|
2663
|
+
const throttleMs = config.keystrokeThrottleMs;
|
|
2664
|
+
function getModifiers(e) {
|
|
2665
|
+
let mod = "";
|
|
2666
|
+
if (e.ctrlKey) mod += "c";
|
|
2667
|
+
if (e.shiftKey) mod += "s";
|
|
2668
|
+
if (e.altKey) mod += "a";
|
|
2669
|
+
if (e.metaKey) mod += "m";
|
|
2670
|
+
return mod || void 0;
|
|
2671
|
+
}
|
|
2672
|
+
function inputName(target) {
|
|
2673
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
|
|
2674
|
+
return target.name || target.id || void 0;
|
|
2675
|
+
}
|
|
2676
|
+
return void 0;
|
|
2677
|
+
}
|
|
2678
|
+
function inputType(target) {
|
|
2679
|
+
return target instanceof HTMLInputElement ? target.type.toLowerCase() : void 0;
|
|
2680
|
+
}
|
|
2681
|
+
function hasSensitiveMarker(target) {
|
|
2682
|
+
if (!(target instanceof Element)) return false;
|
|
2683
|
+
if (target.closest(
|
|
2684
|
+
'[data-sensitive="true"],[data-sensitive=""],[data-crumbtrail-sensitive="true"],[data-crumbtrail-sensitive=""]'
|
|
2685
|
+
)) {
|
|
2686
|
+
return true;
|
|
2687
|
+
}
|
|
2688
|
+
const autocomplete = target.getAttribute("autocomplete");
|
|
2689
|
+
if (!autocomplete) return false;
|
|
2690
|
+
return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
|
|
2691
|
+
}
|
|
2692
|
+
function shouldMaskKey(target) {
|
|
2693
|
+
if (hasSensitiveMarker(target)) return true;
|
|
2694
|
+
const type = inputType(target);
|
|
2695
|
+
if (type && config.maskInputTypes.map((entry) => entry.toLowerCase()).includes(type))
|
|
2696
|
+
return true;
|
|
2697
|
+
const name = inputName(target);
|
|
2698
|
+
if (!name) return false;
|
|
2699
|
+
return Boolean(redactValue({ [name]: "x" }, "key.el").metadata);
|
|
2700
|
+
}
|
|
2701
|
+
function emitKeyEvent(e, dir) {
|
|
2702
|
+
const t = now();
|
|
2703
|
+
if (dir === "up" && throttleMs > 0 && t - lastKeyupTs < throttleMs) {
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const dt = lastKeystrokeTs > 0 ? t - lastKeystrokeTs : void 0;
|
|
2707
|
+
lastKeystrokeTs = t;
|
|
2708
|
+
if (dir === "up") lastKeyupTs = t;
|
|
2709
|
+
const target = e.target;
|
|
2710
|
+
const el = {
|
|
2711
|
+
tag: target instanceof Element ? target.tagName : "UNKNOWN"
|
|
2712
|
+
};
|
|
2713
|
+
if (target instanceof Element && target.id) el.id = target.id;
|
|
2714
|
+
if (target instanceof HTMLInputElement && target.name)
|
|
2715
|
+
el.name = target.name;
|
|
2716
|
+
const masked = shouldMaskKey(target);
|
|
2717
|
+
const keyRedaction = masked ? redactInputValue(e.key, {
|
|
2718
|
+
name: inputName(target),
|
|
2719
|
+
type: inputType(target),
|
|
2720
|
+
path: "key"
|
|
2721
|
+
}) : void 0;
|
|
2722
|
+
const d = {
|
|
2723
|
+
key: masked ? "*" : e.key,
|
|
2724
|
+
code: e.code,
|
|
2725
|
+
dir,
|
|
2726
|
+
el,
|
|
2727
|
+
mod: getModifiers(e),
|
|
2728
|
+
dt
|
|
2729
|
+
};
|
|
2730
|
+
if (masked && e.key) {
|
|
2731
|
+
attachRedactionMetadata(
|
|
2732
|
+
d,
|
|
2733
|
+
keyRedaction?.metadata ?? redactValue(REDACTED_VALUE, "key").metadata
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
bus.emit({
|
|
2737
|
+
t,
|
|
2738
|
+
k: "key",
|
|
2739
|
+
d
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
const onKeydown = (e) => emitKeyEvent(e, "dn");
|
|
2743
|
+
const onKeyup = (e) => emitKeyEvent(e, "up");
|
|
2744
|
+
document.addEventListener("keydown", onKeydown, true);
|
|
2745
|
+
document.addEventListener("keyup", onKeyup, true);
|
|
2746
|
+
return () => {
|
|
2747
|
+
document.removeEventListener("keydown", onKeydown, true);
|
|
2748
|
+
document.removeEventListener("keyup", onKeyup, true);
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// src/collectors/scroll.ts
|
|
2753
|
+
function scrollCollector(bus, config) {
|
|
2754
|
+
const throttleMs = config.scrollThrottleMs;
|
|
2755
|
+
const lastEmit = /* @__PURE__ */ new Map();
|
|
2756
|
+
const lastPos = /* @__PURE__ */ new Map();
|
|
2757
|
+
function getDirection(prev, curr) {
|
|
2758
|
+
const dx = curr[0] - prev[0];
|
|
2759
|
+
const dy = curr[1] - prev[1];
|
|
2760
|
+
if (Math.abs(dy) >= Math.abs(dx)) {
|
|
2761
|
+
return dy > 0 ? "dn" : "up";
|
|
2762
|
+
}
|
|
2763
|
+
return dx > 0 ? "rt" : "lt";
|
|
2764
|
+
}
|
|
2765
|
+
const handler = (event) => {
|
|
2766
|
+
const target = event.target;
|
|
2767
|
+
let el;
|
|
2768
|
+
let pos;
|
|
2769
|
+
if (target === document || target === document.documentElement) {
|
|
2770
|
+
el = "document";
|
|
2771
|
+
pos = [window.scrollX, window.scrollY];
|
|
2772
|
+
} else if (target instanceof Element) {
|
|
2773
|
+
el = target.id ? `#${target.id}` : target.tagName.toLowerCase();
|
|
2774
|
+
pos = [target.scrollLeft, target.scrollTop];
|
|
2775
|
+
} else {
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
const t = now();
|
|
2779
|
+
const lastTime = lastEmit.get(el) ?? 0;
|
|
2780
|
+
if (t - lastTime < throttleMs) return;
|
|
2781
|
+
const prev = lastPos.get(el);
|
|
2782
|
+
const dir = prev ? getDirection(prev, pos) : "dn";
|
|
2783
|
+
lastEmit.set(el, t);
|
|
2784
|
+
lastPos.set(el, pos);
|
|
2785
|
+
bus.emit({ t, k: "scr", d: { el, pos, dir } });
|
|
2786
|
+
};
|
|
2787
|
+
document.addEventListener("scroll", handler, true);
|
|
2788
|
+
return () => {
|
|
2789
|
+
document.removeEventListener("scroll", handler, true);
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// src/collectors/visibility.ts
|
|
2794
|
+
function visibilityCollector(bus, _config) {
|
|
2795
|
+
const handler = () => {
|
|
2796
|
+
bus.emit({
|
|
2797
|
+
t: now(),
|
|
2798
|
+
k: "vis",
|
|
2799
|
+
d: { state: document.visibilityState }
|
|
2800
|
+
});
|
|
2801
|
+
};
|
|
2802
|
+
document.addEventListener("visibilitychange", handler);
|
|
2803
|
+
return () => {
|
|
2804
|
+
document.removeEventListener("visibilitychange", handler);
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/collectors/clipboard.ts
|
|
2809
|
+
function clipboardCollector(bus, config) {
|
|
2810
|
+
const maxLen = config.clipboardMaxLength;
|
|
2811
|
+
const handler = (event) => {
|
|
2812
|
+
const type = event.type;
|
|
2813
|
+
let txt;
|
|
2814
|
+
if (type === "paste") {
|
|
2815
|
+
const ce = event;
|
|
2816
|
+
txt = ce.clipboardData?.getData("text/plain");
|
|
2817
|
+
} else {
|
|
2818
|
+
txt = window.getSelection()?.toString();
|
|
2819
|
+
}
|
|
2820
|
+
const d = { op: type };
|
|
2821
|
+
if (txt) {
|
|
2822
|
+
const truncated = truncate(txt, maxLen);
|
|
2823
|
+
if (config.captureRawClipboard) {
|
|
2824
|
+
d.txt = truncated;
|
|
2825
|
+
} else {
|
|
2826
|
+
const redacted = redactNetworkTextBody(truncated, {
|
|
2827
|
+
contentType: "text/plain",
|
|
2828
|
+
path: "txt"
|
|
2829
|
+
});
|
|
2830
|
+
d.txt = redacted.body ?? "";
|
|
2831
|
+
if (redacted.bodySummary) d.txtSummary = redacted.bodySummary;
|
|
2832
|
+
attachRedactionMetadata(d, redacted.metadata);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
if (event.target instanceof Element) {
|
|
2836
|
+
const el = { tag: event.target.tagName };
|
|
2837
|
+
if (event.target.id) el.id = event.target.id;
|
|
2838
|
+
d.el = el;
|
|
2839
|
+
}
|
|
2840
|
+
bus.emit({ t: now(), k: "clip", d });
|
|
2841
|
+
};
|
|
2842
|
+
document.addEventListener("copy", handler, true);
|
|
2843
|
+
document.addEventListener("cut", handler, true);
|
|
2844
|
+
document.addEventListener("paste", handler, true);
|
|
2845
|
+
return () => {
|
|
2846
|
+
document.removeEventListener("copy", handler, true);
|
|
2847
|
+
document.removeEventListener("cut", handler, true);
|
|
2848
|
+
document.removeEventListener("paste", handler, true);
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/collectors/cookie.ts
|
|
2853
|
+
function parseCookies(cookieStr) {
|
|
2854
|
+
const map = {};
|
|
2855
|
+
if (!cookieStr) return map;
|
|
2856
|
+
const pairs = cookieStr.split(";");
|
|
2857
|
+
for (const pair of pairs) {
|
|
2858
|
+
const eqIdx = pair.indexOf("=");
|
|
2859
|
+
if (eqIdx === -1) continue;
|
|
2860
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
2861
|
+
const value = pair.slice(eqIdx + 1).trim();
|
|
2862
|
+
if (name) map[name] = value;
|
|
2863
|
+
}
|
|
2864
|
+
return map;
|
|
2865
|
+
}
|
|
2866
|
+
function buildFlags(cookie) {
|
|
2867
|
+
let flags = "";
|
|
2868
|
+
if (cookie.secure) flags += "s";
|
|
2869
|
+
if (cookie.httpOnly) flags += "h";
|
|
2870
|
+
if (cookie.sameSite) {
|
|
2871
|
+
const ss = cookie.sameSite.toLowerCase();
|
|
2872
|
+
if (ss === "strict") flags += "S";
|
|
2873
|
+
else if (ss === "lax") flags += "L";
|
|
2874
|
+
else if (ss === "none") flags += "N";
|
|
2875
|
+
}
|
|
2876
|
+
return flags;
|
|
2877
|
+
}
|
|
2878
|
+
function redactCookieForEvent(name, value, config) {
|
|
2879
|
+
return redactCookieValue(
|
|
2880
|
+
name,
|
|
2881
|
+
value,
|
|
2882
|
+
`cookie.${name}.val`,
|
|
2883
|
+
config.cookieMaskNames
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
function safeCookieName(name) {
|
|
2887
|
+
return redactCookieName(name);
|
|
2888
|
+
}
|
|
2889
|
+
function cookieCollector(bus, config) {
|
|
2890
|
+
let lastCookies = {};
|
|
2891
|
+
const initial = parseCookies(document.cookie);
|
|
2892
|
+
for (const [name, value] of Object.entries(initial)) {
|
|
2893
|
+
const val = redactCookieForEvent(name, value, config);
|
|
2894
|
+
const d = {
|
|
2895
|
+
op: "set",
|
|
2896
|
+
name: safeCookieName(name),
|
|
2897
|
+
val: val.value
|
|
2898
|
+
};
|
|
2899
|
+
if (val.summary) d.valSummary = val.summary;
|
|
2900
|
+
attachRedactionMetadata(d, val.metadata);
|
|
2901
|
+
bus.emit({
|
|
2902
|
+
t: now(),
|
|
2903
|
+
k: "cookie",
|
|
2904
|
+
d
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
lastCookies = { ...initial };
|
|
2908
|
+
const hasCookieStore = typeof globalThis.cookieStore !== "undefined";
|
|
2909
|
+
if (hasCookieStore) {
|
|
2910
|
+
const cs = globalThis.cookieStore;
|
|
2911
|
+
const changeHandler = (event) => {
|
|
2912
|
+
const e = event;
|
|
2913
|
+
for (const cookie of e.changed) {
|
|
2914
|
+
const val = redactCookieForEvent(cookie.name, cookie.value, config);
|
|
2915
|
+
const flags = buildFlags(cookie);
|
|
2916
|
+
const d = {
|
|
2917
|
+
op: lastCookies[cookie.name] !== void 0 ? "mod" : "set",
|
|
2918
|
+
name: safeCookieName(cookie.name),
|
|
2919
|
+
val: val.value
|
|
2920
|
+
};
|
|
2921
|
+
if (val.summary) d.valSummary = val.summary;
|
|
2922
|
+
if (flags) d.flags = flags;
|
|
2923
|
+
attachRedactionMetadata(d, val.metadata);
|
|
2924
|
+
bus.emit({ t: now(), k: "cookie", d });
|
|
2925
|
+
lastCookies[cookie.name] = cookie.value;
|
|
2926
|
+
}
|
|
2927
|
+
for (const cookie of e.deleted) {
|
|
2928
|
+
bus.emit({
|
|
2929
|
+
t: now(),
|
|
2930
|
+
k: "cookie",
|
|
2931
|
+
d: { op: "del", name: safeCookieName(cookie.name) }
|
|
2932
|
+
});
|
|
2933
|
+
delete lastCookies[cookie.name];
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
cs.addEventListener("change", changeHandler);
|
|
2937
|
+
return () => {
|
|
2938
|
+
cs.removeEventListener("change", changeHandler);
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
const pollInterval = setInterval(() => {
|
|
2942
|
+
const current = parseCookies(document.cookie);
|
|
2943
|
+
for (const [name, value] of Object.entries(current)) {
|
|
2944
|
+
if (!(name in lastCookies)) {
|
|
2945
|
+
const val = redactCookieForEvent(name, value, config);
|
|
2946
|
+
const d = {
|
|
2947
|
+
op: "set",
|
|
2948
|
+
name: safeCookieName(name),
|
|
2949
|
+
val: val.value
|
|
2950
|
+
};
|
|
2951
|
+
if (val.summary) d.valSummary = val.summary;
|
|
2952
|
+
attachRedactionMetadata(d, val.metadata);
|
|
2953
|
+
bus.emit({
|
|
2954
|
+
t: now(),
|
|
2955
|
+
k: "cookie",
|
|
2956
|
+
d
|
|
2957
|
+
});
|
|
2958
|
+
} else if (lastCookies[name] !== value) {
|
|
2959
|
+
const val = redactCookieForEvent(name, value, config);
|
|
2960
|
+
const d = {
|
|
2961
|
+
op: "mod",
|
|
2962
|
+
name: safeCookieName(name),
|
|
2963
|
+
val: val.value
|
|
2964
|
+
};
|
|
2965
|
+
if (val.summary) d.valSummary = val.summary;
|
|
2966
|
+
attachRedactionMetadata(d, val.metadata);
|
|
2967
|
+
bus.emit({
|
|
2968
|
+
t: now(),
|
|
2969
|
+
k: "cookie",
|
|
2970
|
+
d
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
for (const name of Object.keys(lastCookies)) {
|
|
2975
|
+
if (!(name in current)) {
|
|
2976
|
+
bus.emit({
|
|
2977
|
+
t: now(),
|
|
2978
|
+
k: "cookie",
|
|
2979
|
+
d: { op: "del", name: safeCookieName(name) }
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
lastCookies = { ...current };
|
|
2984
|
+
}, config.cookiePollIntervalMs);
|
|
2985
|
+
return () => {
|
|
2986
|
+
clearInterval(pollInterval);
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/collectors/storage.ts
|
|
2991
|
+
function parseCookies2(cookieStr) {
|
|
2992
|
+
const map = {};
|
|
2993
|
+
if (!cookieStr) return map;
|
|
2994
|
+
const pairs = cookieStr.split(";");
|
|
2995
|
+
for (const pair of pairs) {
|
|
2996
|
+
const eqIdx = pair.indexOf("=");
|
|
2997
|
+
if (eqIdx === -1) continue;
|
|
2998
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
2999
|
+
const value = pair.slice(eqIdx + 1).trim();
|
|
3000
|
+
if (name) map[name] = value;
|
|
3001
|
+
}
|
|
3002
|
+
return map;
|
|
3003
|
+
}
|
|
3004
|
+
function dumpStorage(storage) {
|
|
3005
|
+
const map = {};
|
|
3006
|
+
for (let i = 0; i < storage.length; i++) {
|
|
3007
|
+
const key = storage.key(i);
|
|
3008
|
+
if (key !== null) {
|
|
3009
|
+
map[key] = storage.getItem(key) ?? "";
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return map;
|
|
3013
|
+
}
|
|
3014
|
+
function redactStorageSnapshot(values, type, maxLen) {
|
|
3015
|
+
const out = {};
|
|
3016
|
+
const metadataItems = [];
|
|
3017
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3018
|
+
const keyResult = redactStorageKey(key, `${type}.key`);
|
|
3019
|
+
const valueResult = redactStoredValue(value, {
|
|
3020
|
+
key,
|
|
3021
|
+
maxLength: maxLen,
|
|
3022
|
+
path: `${type}.${keyResult.value}.value`
|
|
3023
|
+
});
|
|
3024
|
+
out[keyResult.value] = valueResult.value;
|
|
3025
|
+
metadataItems.push(keyResult.metadata, valueResult.metadata);
|
|
3026
|
+
}
|
|
3027
|
+
const metadata = mergeRedactionMetadata(...metadataItems);
|
|
3028
|
+
return { value: out, ...metadata ? { metadata } : {} };
|
|
3029
|
+
}
|
|
3030
|
+
function redactStorageNameList(values, path) {
|
|
3031
|
+
const out = [];
|
|
3032
|
+
const metadataItems = [];
|
|
3033
|
+
values.forEach((value, index) => {
|
|
3034
|
+
if (!value) {
|
|
3035
|
+
out.push("");
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const result = redactStorageKey(value, `${path}[${index}]`);
|
|
3039
|
+
out.push(result.value);
|
|
3040
|
+
metadataItems.push(result.metadata);
|
|
3041
|
+
});
|
|
3042
|
+
const metadata = mergeRedactionMetadata(...metadataItems);
|
|
3043
|
+
return { value: out, ...metadata ? { metadata } : {} };
|
|
3044
|
+
}
|
|
3045
|
+
function mergeIntoRedactionMetadata(target, metadata) {
|
|
3046
|
+
if (!metadata) return;
|
|
3047
|
+
const existing = isRedactionMetadata(target.redaction) ? target.redaction : void 0;
|
|
3048
|
+
attachRedactionMetadata(target, existing, metadata);
|
|
3049
|
+
}
|
|
3050
|
+
function isRedactionMetadata(value) {
|
|
3051
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && value.policy === "crumbtrail.browser-redaction.v1" && Array.isArray(value.fields);
|
|
3052
|
+
}
|
|
3053
|
+
function assignRedactedStorageValue(target, field, result) {
|
|
3054
|
+
if (result.value !== void 0) target[field] = result.value;
|
|
3055
|
+
if (result.summary) target[`${field}Summary`] = result.summary;
|
|
3056
|
+
return result.metadata;
|
|
3057
|
+
}
|
|
3058
|
+
function patchStorageMethod(storage, method, fn) {
|
|
3059
|
+
Object.defineProperty(storage, method, {
|
|
3060
|
+
value: fn,
|
|
3061
|
+
writable: true,
|
|
3062
|
+
configurable: true,
|
|
3063
|
+
enumerable: false
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
function restoreStorageMethod(storage, method, origFn) {
|
|
3067
|
+
Object.defineProperty(storage, method, {
|
|
3068
|
+
value: origFn,
|
|
3069
|
+
writable: true,
|
|
3070
|
+
configurable: true,
|
|
3071
|
+
enumerable: false
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
function storageCollector(bus, config) {
|
|
3075
|
+
const maxLen = config.storageValueMaxLength;
|
|
3076
|
+
const excludeKeys = new Set(config.storageExcludeKeys);
|
|
3077
|
+
const cookieSnap = redactCookieMap(
|
|
3078
|
+
parseCookies2(document.cookie),
|
|
3079
|
+
"cookies",
|
|
3080
|
+
config.cookieMaskNames
|
|
3081
|
+
);
|
|
3082
|
+
const localStorageSnap = redactStorageSnapshot(
|
|
3083
|
+
dumpStorage(localStorage),
|
|
3084
|
+
"localStorage",
|
|
3085
|
+
maxLen
|
|
3086
|
+
);
|
|
3087
|
+
const sessionStorageSnap = redactStorageSnapshot(
|
|
3088
|
+
dumpStorage(sessionStorage),
|
|
3089
|
+
"sessionStorage",
|
|
3090
|
+
maxLen
|
|
3091
|
+
);
|
|
3092
|
+
const snapData = {
|
|
3093
|
+
cookies: cookieSnap.value,
|
|
3094
|
+
localStorage: localStorageSnap.value,
|
|
3095
|
+
sessionStorage: sessionStorageSnap.value
|
|
3096
|
+
};
|
|
3097
|
+
attachRedactionMetadata(
|
|
3098
|
+
snapData,
|
|
3099
|
+
cookieSnap.metadata,
|
|
3100
|
+
localStorageSnap.metadata,
|
|
3101
|
+
sessionStorageSnap.metadata
|
|
3102
|
+
);
|
|
3103
|
+
if (config.captureIdb) {
|
|
3104
|
+
try {
|
|
3105
|
+
const idbFactory = typeof indexedDB !== "undefined" ? indexedDB : void 0;
|
|
3106
|
+
if (idbFactory && typeof idbFactory.databases === "function") {
|
|
3107
|
+
idbFactory.databases().then((dbs) => {
|
|
3108
|
+
const names = redactStorageNameList(
|
|
3109
|
+
dbs.map((db) => db.name),
|
|
3110
|
+
"idb.name"
|
|
3111
|
+
);
|
|
3112
|
+
snapData.idb = dbs.map((db, index) => ({
|
|
3113
|
+
name: names.value[index],
|
|
3114
|
+
version: db.version
|
|
3115
|
+
}));
|
|
3116
|
+
mergeIntoRedactionMetadata(snapData, names.metadata);
|
|
3117
|
+
}).catch(() => {
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
} catch {
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (config.captureCacheApi) {
|
|
3124
|
+
try {
|
|
3125
|
+
if (typeof caches !== "undefined" && typeof caches.keys === "function") {
|
|
3126
|
+
caches.keys().then((names) => {
|
|
3127
|
+
const redactedNames = redactStorageNameList(names, "cacheApi.name");
|
|
3128
|
+
snapData.cacheApi = redactedNames.value;
|
|
3129
|
+
mergeIntoRedactionMetadata(snapData, redactedNames.metadata);
|
|
3130
|
+
}).catch(() => {
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
} catch {
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
bus.emit({ t: now(), k: "snap", d: snapData });
|
|
3137
|
+
const origProtoSetItem = Storage.prototype.setItem;
|
|
3138
|
+
const origProtoRemoveItem = Storage.prototype.removeItem;
|
|
3139
|
+
const origProtoClear = Storage.prototype.clear;
|
|
3140
|
+
const origLocalSetItem = localStorage.setItem.bind(localStorage);
|
|
3141
|
+
const origLocalRemoveItem = localStorage.removeItem.bind(localStorage);
|
|
3142
|
+
const origLocalClear = localStorage.clear.bind(localStorage);
|
|
3143
|
+
const origSessionSetItem = sessionStorage.setItem.bind(sessionStorage);
|
|
3144
|
+
const origSessionRemoveItem = sessionStorage.removeItem.bind(sessionStorage);
|
|
3145
|
+
const origSessionClear = sessionStorage.clear.bind(sessionStorage);
|
|
3146
|
+
function makeSetItem(type, storage, origFn) {
|
|
3147
|
+
return function patchedSetItem(key, value) {
|
|
3148
|
+
if (!excludeKeys.has(key)) {
|
|
3149
|
+
const keyResult = redactStorageKey(key, `${type}.key`);
|
|
3150
|
+
const d = {
|
|
3151
|
+
type,
|
|
3152
|
+
op: "set",
|
|
3153
|
+
key: keyResult.value
|
|
3154
|
+
};
|
|
3155
|
+
const oldValMetadata = assignRedactedStorageValue(
|
|
3156
|
+
d,
|
|
3157
|
+
"oldVal",
|
|
3158
|
+
redactStoredValue(storage.getItem(key), {
|
|
3159
|
+
key,
|
|
3160
|
+
maxLength: maxLen,
|
|
3161
|
+
path: `${type}.${keyResult.value}.oldVal`
|
|
3162
|
+
})
|
|
3163
|
+
);
|
|
3164
|
+
const newValMetadata = assignRedactedStorageValue(
|
|
3165
|
+
d,
|
|
3166
|
+
"newVal",
|
|
3167
|
+
redactStoredValue(value, {
|
|
3168
|
+
key,
|
|
3169
|
+
maxLength: maxLen,
|
|
3170
|
+
path: `${type}.${keyResult.value}.newVal`
|
|
3171
|
+
})
|
|
3172
|
+
);
|
|
3173
|
+
attachRedactionMetadata(
|
|
3174
|
+
d,
|
|
3175
|
+
keyResult.metadata,
|
|
3176
|
+
oldValMetadata,
|
|
3177
|
+
newValMetadata
|
|
3178
|
+
);
|
|
3179
|
+
bus.emit({
|
|
3180
|
+
t: now(),
|
|
3181
|
+
k: "stor",
|
|
3182
|
+
d
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
return origFn(key, value);
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
function makeRemoveItem(type, storage, origFn) {
|
|
3189
|
+
return function patchedRemoveItem(key) {
|
|
3190
|
+
if (!excludeKeys.has(key)) {
|
|
3191
|
+
const keyResult = redactStorageKey(key, `${type}.key`);
|
|
3192
|
+
const d = {
|
|
3193
|
+
type,
|
|
3194
|
+
op: "del",
|
|
3195
|
+
key: keyResult.value
|
|
3196
|
+
};
|
|
3197
|
+
const oldValMetadata = assignRedactedStorageValue(
|
|
3198
|
+
d,
|
|
3199
|
+
"oldVal",
|
|
3200
|
+
redactStoredValue(storage.getItem(key), {
|
|
3201
|
+
key,
|
|
3202
|
+
maxLength: maxLen,
|
|
3203
|
+
path: `${type}.${keyResult.value}.oldVal`
|
|
3204
|
+
})
|
|
3205
|
+
);
|
|
3206
|
+
attachRedactionMetadata(d, keyResult.metadata, oldValMetadata);
|
|
3207
|
+
bus.emit({
|
|
3208
|
+
t: now(),
|
|
3209
|
+
k: "stor",
|
|
3210
|
+
d
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
return origFn(key);
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
function makeClear(type, origFn) {
|
|
3217
|
+
return function patchedClear() {
|
|
3218
|
+
bus.emit({
|
|
3219
|
+
t: now(),
|
|
3220
|
+
k: "stor",
|
|
3221
|
+
d: { type, op: "clear" }
|
|
3222
|
+
});
|
|
3223
|
+
return origFn();
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
Storage.prototype.setItem = makeSetItem(
|
|
3227
|
+
"local",
|
|
3228
|
+
localStorage,
|
|
3229
|
+
origProtoSetItem
|
|
3230
|
+
);
|
|
3231
|
+
Storage.prototype.removeItem = makeRemoveItem(
|
|
3232
|
+
"local",
|
|
3233
|
+
localStorage,
|
|
3234
|
+
origProtoRemoveItem
|
|
3235
|
+
);
|
|
3236
|
+
Storage.prototype.clear = makeClear("local", origProtoClear);
|
|
3237
|
+
patchStorageMethod(
|
|
3238
|
+
localStorage,
|
|
3239
|
+
"setItem",
|
|
3240
|
+
makeSetItem("local", localStorage, origLocalSetItem)
|
|
3241
|
+
);
|
|
3242
|
+
patchStorageMethod(
|
|
3243
|
+
localStorage,
|
|
3244
|
+
"removeItem",
|
|
3245
|
+
makeRemoveItem("local", localStorage, origLocalRemoveItem)
|
|
3246
|
+
);
|
|
3247
|
+
patchStorageMethod(localStorage, "clear", makeClear("local", origLocalClear));
|
|
3248
|
+
patchStorageMethod(
|
|
3249
|
+
sessionStorage,
|
|
3250
|
+
"setItem",
|
|
3251
|
+
makeSetItem("session", sessionStorage, origSessionSetItem)
|
|
3252
|
+
);
|
|
3253
|
+
patchStorageMethod(
|
|
3254
|
+
sessionStorage,
|
|
3255
|
+
"removeItem",
|
|
3256
|
+
makeRemoveItem("session", sessionStorage, origSessionRemoveItem)
|
|
3257
|
+
);
|
|
3258
|
+
patchStorageMethod(
|
|
3259
|
+
sessionStorage,
|
|
3260
|
+
"clear",
|
|
3261
|
+
makeClear("session", origSessionClear)
|
|
3262
|
+
);
|
|
3263
|
+
const storageHandler = (event) => {
|
|
3264
|
+
if (event.key && excludeKeys.has(event.key)) return;
|
|
3265
|
+
const type = event.storageArea === localStorage ? "local" : "session";
|
|
3266
|
+
if (event.key === null) {
|
|
3267
|
+
bus.emit({
|
|
3268
|
+
t: now(),
|
|
3269
|
+
k: "stor",
|
|
3270
|
+
d: { type, op: "clear" }
|
|
3271
|
+
});
|
|
3272
|
+
} else if (event.newValue === null) {
|
|
3273
|
+
const keyResult = redactStorageKey(event.key, `${type}.key`);
|
|
3274
|
+
const d = {
|
|
3275
|
+
type,
|
|
3276
|
+
op: "del",
|
|
3277
|
+
key: keyResult.value
|
|
3278
|
+
};
|
|
3279
|
+
const oldValMetadata = assignRedactedStorageValue(
|
|
3280
|
+
d,
|
|
3281
|
+
"oldVal",
|
|
3282
|
+
redactStoredValue(event.oldValue, {
|
|
3283
|
+
key: event.key,
|
|
3284
|
+
maxLength: maxLen,
|
|
3285
|
+
path: `${type}.${keyResult.value}.oldVal`
|
|
3286
|
+
})
|
|
3287
|
+
);
|
|
3288
|
+
attachRedactionMetadata(d, keyResult.metadata, oldValMetadata);
|
|
3289
|
+
bus.emit({
|
|
3290
|
+
t: now(),
|
|
3291
|
+
k: "stor",
|
|
3292
|
+
d
|
|
3293
|
+
});
|
|
3294
|
+
} else {
|
|
3295
|
+
const keyResult = redactStorageKey(event.key, `${type}.key`);
|
|
3296
|
+
const d = {
|
|
3297
|
+
type,
|
|
3298
|
+
op: "set",
|
|
3299
|
+
key: keyResult.value
|
|
3300
|
+
};
|
|
3301
|
+
const oldValMetadata = assignRedactedStorageValue(
|
|
3302
|
+
d,
|
|
3303
|
+
"oldVal",
|
|
3304
|
+
redactStoredValue(event.oldValue, {
|
|
3305
|
+
key: event.key,
|
|
3306
|
+
maxLength: maxLen,
|
|
3307
|
+
path: `${type}.${keyResult.value}.oldVal`
|
|
3308
|
+
})
|
|
3309
|
+
);
|
|
3310
|
+
const newValMetadata = assignRedactedStorageValue(
|
|
3311
|
+
d,
|
|
3312
|
+
"newVal",
|
|
3313
|
+
redactStoredValue(event.newValue, {
|
|
3314
|
+
key: event.key,
|
|
3315
|
+
maxLength: maxLen,
|
|
3316
|
+
path: `${type}.${keyResult.value}.newVal`
|
|
3317
|
+
})
|
|
3318
|
+
);
|
|
3319
|
+
attachRedactionMetadata(
|
|
3320
|
+
d,
|
|
3321
|
+
keyResult.metadata,
|
|
3322
|
+
oldValMetadata,
|
|
3323
|
+
newValMetadata
|
|
3324
|
+
);
|
|
3325
|
+
bus.emit({
|
|
3326
|
+
t: now(),
|
|
3327
|
+
k: "stor",
|
|
3328
|
+
d
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
window.addEventListener("storage", storageHandler);
|
|
3333
|
+
return () => {
|
|
3334
|
+
Storage.prototype.setItem = origProtoSetItem;
|
|
3335
|
+
Storage.prototype.removeItem = origProtoRemoveItem;
|
|
3336
|
+
Storage.prototype.clear = origProtoClear;
|
|
3337
|
+
restoreStorageMethod(localStorage, "setItem", origLocalSetItem);
|
|
3338
|
+
restoreStorageMethod(localStorage, "removeItem", origLocalRemoveItem);
|
|
3339
|
+
restoreStorageMethod(localStorage, "clear", origLocalClear);
|
|
3340
|
+
restoreStorageMethod(sessionStorage, "setItem", origSessionSetItem);
|
|
3341
|
+
restoreStorageMethod(sessionStorage, "removeItem", origSessionRemoveItem);
|
|
3342
|
+
restoreStorageMethod(sessionStorage, "clear", origSessionClear);
|
|
3343
|
+
window.removeEventListener("storage", storageHandler);
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// src/collectors/network.ts
|
|
3348
|
+
var nextId = 1;
|
|
3349
|
+
var BINARY_CONTENT_TYPES = ["octet-stream", "image/", "audio/", "video/"];
|
|
3350
|
+
var DEDUP_MAP_MAX = 1e3;
|
|
3351
|
+
var dedupMap = /* @__PURE__ */ new Map();
|
|
3352
|
+
function djb2(s) {
|
|
3353
|
+
let h = 5381;
|
|
3354
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i);
|
|
3355
|
+
return h >>> 0;
|
|
3356
|
+
}
|
|
3357
|
+
function clearDedupMap() {
|
|
3358
|
+
dedupMap.clear();
|
|
3359
|
+
}
|
|
3360
|
+
function checkDedup(url, body, timestamp) {
|
|
3361
|
+
if (body == null || typeof body !== "string") return void 0;
|
|
3362
|
+
const key = url + ":" + djb2(url + body);
|
|
3363
|
+
if (dedupMap.has(key)) {
|
|
3364
|
+
return { ref: dedupMap.get(key), dedup: true };
|
|
3365
|
+
}
|
|
3366
|
+
if (dedupMap.size >= DEDUP_MAP_MAX) {
|
|
3367
|
+
const oldest = dedupMap.keys().next().value;
|
|
3368
|
+
if (oldest !== void 0) dedupMap.delete(oldest);
|
|
3369
|
+
}
|
|
3370
|
+
dedupMap.set(key, timestamp);
|
|
3371
|
+
return void 0;
|
|
3372
|
+
}
|
|
3373
|
+
function isBinaryContentType(ct) {
|
|
3374
|
+
const lower = ct.toLowerCase();
|
|
3375
|
+
return BINARY_CONTENT_TYPES.some((t) => lower.includes(t));
|
|
3376
|
+
}
|
|
3377
|
+
function isSSE(ct) {
|
|
3378
|
+
return ct.toLowerCase().includes("text/event-stream");
|
|
3379
|
+
}
|
|
3380
|
+
function shouldExclude(url, config) {
|
|
3381
|
+
if (config.httpEndpoint && url.includes(config.httpEndpoint)) return true;
|
|
3382
|
+
return config.networkExcludeUrls.some((pattern) => url.includes(pattern));
|
|
3383
|
+
}
|
|
3384
|
+
function headersToRecord(headers) {
|
|
3385
|
+
if (!headers) return void 0;
|
|
3386
|
+
const result = {};
|
|
3387
|
+
if (headers instanceof Headers) {
|
|
3388
|
+
headers.forEach((value, key) => {
|
|
3389
|
+
result[key] = value;
|
|
3390
|
+
});
|
|
3391
|
+
} else if (Array.isArray(headers)) {
|
|
3392
|
+
for (const [key, value] of headers) {
|
|
3393
|
+
result[key] = value;
|
|
3394
|
+
}
|
|
3395
|
+
} else {
|
|
3396
|
+
return headers;
|
|
3397
|
+
}
|
|
3398
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
3399
|
+
}
|
|
3400
|
+
function responseHeadersToRecord(response) {
|
|
3401
|
+
const result = {};
|
|
3402
|
+
response.headers.forEach((value, key) => {
|
|
3403
|
+
result[key] = value;
|
|
3404
|
+
});
|
|
3405
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
3406
|
+
}
|
|
3407
|
+
function extractUrlString(input) {
|
|
3408
|
+
if (typeof input === "string") return input;
|
|
3409
|
+
if (input instanceof URL) return input.toString();
|
|
3410
|
+
if (input instanceof Request) return input.url;
|
|
3411
|
+
return String(input);
|
|
3412
|
+
}
|
|
3413
|
+
function extractMethod(input, init) {
|
|
3414
|
+
if (init?.method) return init.method.toUpperCase();
|
|
3415
|
+
if (input instanceof Request) return input.method.toUpperCase();
|
|
3416
|
+
return "GET";
|
|
3417
|
+
}
|
|
3418
|
+
function extractRequestBody(input, init) {
|
|
3419
|
+
const body = init?.body ?? (input instanceof Request ? input.body : void 0);
|
|
3420
|
+
if (body == null) return { nonText: false };
|
|
3421
|
+
if (typeof body === "string") return { body, nonText: false };
|
|
3422
|
+
return { nonText: true };
|
|
3423
|
+
}
|
|
3424
|
+
function getHeaderValue(headers, name) {
|
|
3425
|
+
if (!headers) return void 0;
|
|
3426
|
+
const target = name.toLowerCase();
|
|
3427
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
3428
|
+
if (key.toLowerCase() === target) return value;
|
|
3429
|
+
}
|
|
3430
|
+
return void 0;
|
|
3431
|
+
}
|
|
3432
|
+
function headersToInit(input, init) {
|
|
3433
|
+
try {
|
|
3434
|
+
if (init?.headers !== void 0) return new Headers(init.headers);
|
|
3435
|
+
if (input instanceof Request) return new Headers(input.headers);
|
|
3436
|
+
return void 0;
|
|
3437
|
+
} catch {
|
|
3438
|
+
return void 0;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
function headersToWritableInit(input, init) {
|
|
3442
|
+
return headersToInit(input, init) ?? new Headers();
|
|
3443
|
+
}
|
|
3444
|
+
function buildFetchArgsWithHeaders(input, init, headers) {
|
|
3445
|
+
return [input, { ...init, headers }];
|
|
3446
|
+
}
|
|
3447
|
+
function applyFetchCorrelationHeaders(input, init, config, context) {
|
|
3448
|
+
const existingHeaders = headersToInit(input, init);
|
|
3449
|
+
const url = extractUrlString(input);
|
|
3450
|
+
if (!config.networkCorrelationHeaders || !canInjectCorrelationHeaders(url, config.networkCorrelationAllowedOrigins)) {
|
|
3451
|
+
return { input, init, requestHeaders: headersToRecord(existingHeaders) };
|
|
3452
|
+
}
|
|
3453
|
+
const sessionId = existingHeaders?.get(CRUMBTRAIL_SESSION_HEADER) ?? context?.sessionId;
|
|
3454
|
+
if (!sessionId) {
|
|
3455
|
+
return { input, init, requestHeaders: headersToRecord(existingHeaders) };
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
const headers = headersToWritableInit(input, init);
|
|
3459
|
+
const existingSessionId = headers.get(CRUMBTRAIL_SESSION_HEADER);
|
|
3460
|
+
const existingRequestId = headers.get(CRUMBTRAIL_REQUEST_HEADER);
|
|
3461
|
+
const existingTraceparent = headers.get(W3C_TRACEPARENT_HEADER);
|
|
3462
|
+
const correlation = resolveOutboundCorrelation({
|
|
3463
|
+
sessionId: existingSessionId ?? sessionId,
|
|
3464
|
+
existingRequestId: existingRequestId ?? void 0,
|
|
3465
|
+
existingTraceparent: existingTraceparent ?? void 0
|
|
3466
|
+
});
|
|
3467
|
+
if (!existingSessionId)
|
|
3468
|
+
headers.set(CRUMBTRAIL_SESSION_HEADER, correlation.sessionId);
|
|
3469
|
+
if (!existingRequestId)
|
|
3470
|
+
headers.set(CRUMBTRAIL_REQUEST_HEADER, correlation.requestId);
|
|
3471
|
+
if (!existingTraceparent)
|
|
3472
|
+
headers.set(W3C_TRACEPARENT_HEADER, correlation.traceparent);
|
|
3473
|
+
const [nextInput, nextInit] = buildFetchArgsWithHeaders(
|
|
3474
|
+
input,
|
|
3475
|
+
init,
|
|
3476
|
+
headers
|
|
3477
|
+
);
|
|
3478
|
+
return {
|
|
3479
|
+
input: nextInput,
|
|
3480
|
+
init: nextInit,
|
|
3481
|
+
requestHeaders: headersToRecord(headers),
|
|
3482
|
+
sessionId: correlation.sessionId,
|
|
3483
|
+
requestId: correlation.requestId,
|
|
3484
|
+
traceId: correlation.traceId,
|
|
3485
|
+
spanId: correlation.spanId
|
|
3486
|
+
};
|
|
3487
|
+
} catch {
|
|
3488
|
+
return { input, init, requestHeaders: headersToRecord(existingHeaders) };
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
function applyBodyResult(target, result) {
|
|
3492
|
+
if (result.body !== void 0) target.body = result.body;
|
|
3493
|
+
if (result.bodySummary) target.bodySummary = result.bodySummary;
|
|
3494
|
+
}
|
|
3495
|
+
function wrapFetch(bus, config, originalFetch, context, pending) {
|
|
3496
|
+
return async function instrumentedFetch(input, init) {
|
|
3497
|
+
const url = extractUrlString(input);
|
|
3498
|
+
if (shouldExclude(url, config)) {
|
|
3499
|
+
return originalFetch(input, init);
|
|
3500
|
+
}
|
|
3501
|
+
const id = nextId++;
|
|
3502
|
+
const method = extractMethod(input, init);
|
|
3503
|
+
const startTime = now();
|
|
3504
|
+
const fetchArgs = applyFetchCorrelationHeaders(
|
|
3505
|
+
input,
|
|
3506
|
+
init,
|
|
3507
|
+
config,
|
|
3508
|
+
context
|
|
3509
|
+
);
|
|
3510
|
+
const urlResult = redactUrl(url, "url");
|
|
3511
|
+
const reqMetadata = [
|
|
3512
|
+
urlResult.metadata
|
|
3513
|
+
];
|
|
3514
|
+
const reqData = {
|
|
3515
|
+
id,
|
|
3516
|
+
method,
|
|
3517
|
+
url: urlResult.value
|
|
3518
|
+
};
|
|
3519
|
+
if (fetchArgs.sessionId) reqData.sessionId = fetchArgs.sessionId;
|
|
3520
|
+
if (fetchArgs.requestId) reqData.requestId = fetchArgs.requestId;
|
|
3521
|
+
if (fetchArgs.traceId) reqData.traceId = fetchArgs.traceId;
|
|
3522
|
+
if (fetchArgs.spanId) reqData.spanId = fetchArgs.spanId;
|
|
3523
|
+
const requestHeaders = config.networkCaptureHeaders ? fetchArgs.requestHeaders : void 0;
|
|
3524
|
+
if (requestHeaders) {
|
|
3525
|
+
const headersResult = redactHeaders(requestHeaders, "hdrs");
|
|
3526
|
+
reqData.hdrs = headersResult.value;
|
|
3527
|
+
reqMetadata.push(headersResult.metadata);
|
|
3528
|
+
}
|
|
3529
|
+
const requestBody = extractRequestBody(input, init);
|
|
3530
|
+
if (requestBody.body !== void 0) {
|
|
3531
|
+
const bodyResult = redactNetworkTextBody(requestBody.body, {
|
|
3532
|
+
contentType: getHeaderValue(requestHeaders, "content-type"),
|
|
3533
|
+
maxLength: config.networkMaxBodySize,
|
|
3534
|
+
path: "body"
|
|
3535
|
+
});
|
|
3536
|
+
applyBodyResult(reqData, bodyResult);
|
|
3537
|
+
reqMetadata.push(bodyResult.metadata);
|
|
3538
|
+
} else if (requestBody.nonText) {
|
|
3539
|
+
const bodyResult = summarizeOmittedPayload(
|
|
3540
|
+
"non_text_request_body",
|
|
3541
|
+
"body"
|
|
3542
|
+
);
|
|
3543
|
+
applyBodyResult(reqData, bodyResult);
|
|
3544
|
+
reqMetadata.push(bodyResult.metadata);
|
|
3545
|
+
}
|
|
3546
|
+
attachRedactionMetadata(reqData, ...reqMetadata);
|
|
3547
|
+
bus.emit({ t: startTime, k: "net.req", d: reqData });
|
|
3548
|
+
pending.set(id, { method, url: urlResult.value, startTime });
|
|
3549
|
+
let response;
|
|
3550
|
+
try {
|
|
3551
|
+
response = await originalFetch(fetchArgs.input, fetchArgs.init);
|
|
3552
|
+
} finally {
|
|
3553
|
+
pending.delete(id);
|
|
3554
|
+
}
|
|
3555
|
+
const dur = now() - startTime;
|
|
3556
|
+
const resMetadata = [];
|
|
3557
|
+
const resData = { id, st: response.status, dur };
|
|
3558
|
+
if (fetchArgs.sessionId) resData.sessionId = fetchArgs.sessionId;
|
|
3559
|
+
if (fetchArgs.requestId) resData.requestId = fetchArgs.requestId;
|
|
3560
|
+
if (fetchArgs.traceId) resData.traceId = fetchArgs.traceId;
|
|
3561
|
+
if (fetchArgs.spanId) resData.spanId = fetchArgs.spanId;
|
|
3562
|
+
if (config.networkCaptureHeaders) {
|
|
3563
|
+
const hdrs = responseHeadersToRecord(response);
|
|
3564
|
+
if (hdrs) {
|
|
3565
|
+
const headersResult = redactHeaders(hdrs, "hdrs");
|
|
3566
|
+
resData.hdrs = headersResult.value;
|
|
3567
|
+
resMetadata.push(headersResult.metadata);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3571
|
+
if (isSSE(contentType)) {
|
|
3572
|
+
const bodyResult = summarizeOmittedPayload("stream_payload", "body");
|
|
3573
|
+
applyBodyResult(resData, bodyResult);
|
|
3574
|
+
resMetadata.push(bodyResult.metadata);
|
|
3575
|
+
} else if (isBinaryContentType(contentType)) {
|
|
3576
|
+
const bodyResult = summarizeBinaryPayload(
|
|
3577
|
+
contentType,
|
|
3578
|
+
response.headers.get("content-length"),
|
|
3579
|
+
"body"
|
|
3580
|
+
);
|
|
3581
|
+
applyBodyResult(resData, bodyResult);
|
|
3582
|
+
resMetadata.push(bodyResult.metadata);
|
|
3583
|
+
} else {
|
|
3584
|
+
try {
|
|
3585
|
+
const cloned = response.clone();
|
|
3586
|
+
const text = await cloned.text();
|
|
3587
|
+
if (text) {
|
|
3588
|
+
const bodyResult = redactNetworkTextBody(text, {
|
|
3589
|
+
contentType,
|
|
3590
|
+
maxLength: config.networkMaxBodySize,
|
|
3591
|
+
path: "body"
|
|
3592
|
+
});
|
|
3593
|
+
if (bodyResult.body !== void 0) {
|
|
3594
|
+
const ts = String(now());
|
|
3595
|
+
const dedupResult = checkDedup(
|
|
3596
|
+
urlResult.value,
|
|
3597
|
+
bodyResult.body,
|
|
3598
|
+
ts
|
|
3599
|
+
);
|
|
3600
|
+
if (dedupResult) {
|
|
3601
|
+
resData.body = dedupResult;
|
|
3602
|
+
resData.dedup = true;
|
|
3603
|
+
} else {
|
|
3604
|
+
resData.body = bodyResult.body;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
if (bodyResult.bodySummary)
|
|
3608
|
+
resData.bodySummary = bodyResult.bodySummary;
|
|
3609
|
+
resMetadata.push(bodyResult.metadata);
|
|
3610
|
+
}
|
|
3611
|
+
} catch {
|
|
3612
|
+
const bodyResult = summarizeOmittedPayload("body_read_failed", "body");
|
|
3613
|
+
applyBodyResult(resData, bodyResult);
|
|
3614
|
+
resMetadata.push(bodyResult.metadata);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
attachRedactionMetadata(resData, ...resMetadata);
|
|
3618
|
+
bus.emit({ t: now(), k: "net.res", d: resData });
|
|
3619
|
+
return response;
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
function wrapXHR(bus, config, xhrPrototype, context, pending) {
|
|
3623
|
+
const origOpen = xhrPrototype.open;
|
|
3624
|
+
const origSend = xhrPrototype.send;
|
|
3625
|
+
const origSetRequestHeader = xhrPrototype.setRequestHeader;
|
|
3626
|
+
const xhrMeta = /* @__PURE__ */ new WeakMap();
|
|
3627
|
+
xhrPrototype.open = function(method, url, ...rest) {
|
|
3628
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
3629
|
+
const excluded = shouldExclude(urlStr, config);
|
|
3630
|
+
xhrMeta.set(this, {
|
|
3631
|
+
id: nextId++,
|
|
3632
|
+
method: method.toUpperCase(),
|
|
3633
|
+
url: urlStr,
|
|
3634
|
+
startTime: 0,
|
|
3635
|
+
excluded,
|
|
3636
|
+
requestHeaders: {}
|
|
3637
|
+
});
|
|
3638
|
+
return origOpen.call(this, method, url, ...rest);
|
|
3639
|
+
};
|
|
3640
|
+
xhrPrototype.setRequestHeader = function(name, value) {
|
|
3641
|
+
const meta = xhrMeta.get(this);
|
|
3642
|
+
if (meta) {
|
|
3643
|
+
meta.requestHeaders[name] = value;
|
|
3644
|
+
}
|
|
3645
|
+
return origSetRequestHeader.call(this, name, value);
|
|
3646
|
+
};
|
|
3647
|
+
xhrPrototype.send = function(body) {
|
|
3648
|
+
const meta = xhrMeta.get(this);
|
|
3649
|
+
if (!meta || meta.excluded) {
|
|
3650
|
+
return origSend.call(this, body);
|
|
3651
|
+
}
|
|
3652
|
+
if (config.networkCorrelationHeaders && canInjectCorrelationHeaders(
|
|
3653
|
+
meta.url,
|
|
3654
|
+
config.networkCorrelationAllowedOrigins
|
|
3655
|
+
)) {
|
|
3656
|
+
const existingSessionId = getHeaderValue(
|
|
3657
|
+
meta.requestHeaders,
|
|
3658
|
+
CRUMBTRAIL_SESSION_HEADER
|
|
3659
|
+
);
|
|
3660
|
+
const existingRequestId = getHeaderValue(
|
|
3661
|
+
meta.requestHeaders,
|
|
3662
|
+
CRUMBTRAIL_REQUEST_HEADER
|
|
3663
|
+
);
|
|
3664
|
+
const existingTraceparent = getHeaderValue(
|
|
3665
|
+
meta.requestHeaders,
|
|
3666
|
+
W3C_TRACEPARENT_HEADER
|
|
3667
|
+
);
|
|
3668
|
+
const sessionId = existingSessionId ?? context?.sessionId;
|
|
3669
|
+
if (sessionId) {
|
|
3670
|
+
const correlation = resolveOutboundCorrelation({
|
|
3671
|
+
sessionId,
|
|
3672
|
+
existingRequestId,
|
|
3673
|
+
existingTraceparent
|
|
3674
|
+
});
|
|
3675
|
+
meta.sessionId = correlation.sessionId;
|
|
3676
|
+
meta.requestId = correlation.requestId;
|
|
3677
|
+
meta.traceId = correlation.traceId;
|
|
3678
|
+
meta.spanId = correlation.spanId;
|
|
3679
|
+
if (!existingSessionId) {
|
|
3680
|
+
try {
|
|
3681
|
+
origSetRequestHeader.call(
|
|
3682
|
+
this,
|
|
3683
|
+
CRUMBTRAIL_SESSION_HEADER,
|
|
3684
|
+
meta.sessionId
|
|
3685
|
+
);
|
|
3686
|
+
meta.requestHeaders[CRUMBTRAIL_SESSION_HEADER] = meta.sessionId;
|
|
3687
|
+
} catch {
|
|
3688
|
+
meta.sessionId = void 0;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
if (!existingRequestId) {
|
|
3692
|
+
try {
|
|
3693
|
+
origSetRequestHeader.call(
|
|
3694
|
+
this,
|
|
3695
|
+
CRUMBTRAIL_REQUEST_HEADER,
|
|
3696
|
+
meta.requestId
|
|
3697
|
+
);
|
|
3698
|
+
meta.requestHeaders[CRUMBTRAIL_REQUEST_HEADER] = meta.requestId;
|
|
3699
|
+
} catch {
|
|
3700
|
+
meta.requestId = void 0;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
if (!existingTraceparent) {
|
|
3704
|
+
try {
|
|
3705
|
+
origSetRequestHeader.call(
|
|
3706
|
+
this,
|
|
3707
|
+
W3C_TRACEPARENT_HEADER,
|
|
3708
|
+
correlation.traceparent
|
|
3709
|
+
);
|
|
3710
|
+
meta.requestHeaders[W3C_TRACEPARENT_HEADER] = correlation.traceparent;
|
|
3711
|
+
} catch {
|
|
3712
|
+
meta.traceId = void 0;
|
|
3713
|
+
meta.spanId = void 0;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
meta.startTime = now();
|
|
3719
|
+
const urlResult = redactUrl(meta.url, "url");
|
|
3720
|
+
const reqMetadata = [
|
|
3721
|
+
urlResult.metadata
|
|
3722
|
+
];
|
|
3723
|
+
const reqData = {
|
|
3724
|
+
id: meta.id,
|
|
3725
|
+
method: meta.method,
|
|
3726
|
+
url: urlResult.value
|
|
3727
|
+
};
|
|
3728
|
+
if (meta.sessionId) reqData.sessionId = meta.sessionId;
|
|
3729
|
+
if (meta.requestId) reqData.requestId = meta.requestId;
|
|
3730
|
+
if (meta.traceId) reqData.traceId = meta.traceId;
|
|
3731
|
+
if (meta.spanId) reqData.spanId = meta.spanId;
|
|
3732
|
+
if (config.networkCaptureHeaders && Object.keys(meta.requestHeaders).length > 0) {
|
|
3733
|
+
const headersResult = redactHeaders({ ...meta.requestHeaders }, "hdrs");
|
|
3734
|
+
reqData.hdrs = headersResult.value;
|
|
3735
|
+
reqMetadata.push(headersResult.metadata);
|
|
3736
|
+
}
|
|
3737
|
+
if (body != null && typeof body === "string") {
|
|
3738
|
+
const bodyResult = redactNetworkTextBody(body, {
|
|
3739
|
+
contentType: getHeaderValue(meta.requestHeaders, "content-type"),
|
|
3740
|
+
maxLength: config.networkMaxBodySize,
|
|
3741
|
+
path: "body"
|
|
3742
|
+
});
|
|
3743
|
+
applyBodyResult(reqData, bodyResult);
|
|
3744
|
+
reqMetadata.push(bodyResult.metadata);
|
|
3745
|
+
} else if (body != null) {
|
|
3746
|
+
const bodyResult = summarizeOmittedPayload(
|
|
3747
|
+
"non_text_request_body",
|
|
3748
|
+
"body"
|
|
3749
|
+
);
|
|
3750
|
+
applyBodyResult(reqData, bodyResult);
|
|
3751
|
+
reqMetadata.push(bodyResult.metadata);
|
|
3752
|
+
}
|
|
3753
|
+
attachRedactionMetadata(reqData, ...reqMetadata);
|
|
3754
|
+
bus.emit({ t: meta.startTime, k: "net.req", d: reqData });
|
|
3755
|
+
const emitResponse = () => {
|
|
3756
|
+
const dur = now() - meta.startTime;
|
|
3757
|
+
const resMetadata = [];
|
|
3758
|
+
const resData = {
|
|
3759
|
+
id: meta.id,
|
|
3760
|
+
st: this.status,
|
|
3761
|
+
dur
|
|
3762
|
+
};
|
|
3763
|
+
if (meta.sessionId) resData.sessionId = meta.sessionId;
|
|
3764
|
+
if (meta.requestId) resData.requestId = meta.requestId;
|
|
3765
|
+
if (meta.traceId) resData.traceId = meta.traceId;
|
|
3766
|
+
if (meta.spanId) resData.spanId = meta.spanId;
|
|
3767
|
+
if (config.networkCaptureHeaders) {
|
|
3768
|
+
const rawHeaders = this.getAllResponseHeaders();
|
|
3769
|
+
if (rawHeaders) {
|
|
3770
|
+
const hdrs = {};
|
|
3771
|
+
rawHeaders.split("\r\n").filter(Boolean).forEach((line) => {
|
|
3772
|
+
const idx = line.indexOf(": ");
|
|
3773
|
+
if (idx > -1) {
|
|
3774
|
+
hdrs[line.slice(0, idx)] = line.slice(idx + 2);
|
|
3775
|
+
}
|
|
3776
|
+
});
|
|
3777
|
+
if (Object.keys(hdrs).length > 0) {
|
|
3778
|
+
const headersResult = redactHeaders(hdrs, "hdrs");
|
|
3779
|
+
resData.hdrs = headersResult.value;
|
|
3780
|
+
resMetadata.push(headersResult.metadata);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
const contentType = this.getResponseHeader("content-type") ?? "";
|
|
3785
|
+
if (isSSE(contentType)) {
|
|
3786
|
+
const bodyResult = summarizeOmittedPayload("stream_payload", "body");
|
|
3787
|
+
applyBodyResult(resData, bodyResult);
|
|
3788
|
+
resMetadata.push(bodyResult.metadata);
|
|
3789
|
+
} else if (isBinaryContentType(contentType)) {
|
|
3790
|
+
const bodyResult = summarizeBinaryPayload(
|
|
3791
|
+
contentType,
|
|
3792
|
+
this.getResponseHeader("content-length"),
|
|
3793
|
+
"body"
|
|
3794
|
+
);
|
|
3795
|
+
applyBodyResult(resData, bodyResult);
|
|
3796
|
+
resMetadata.push(bodyResult.metadata);
|
|
3797
|
+
} else {
|
|
3798
|
+
const text = this.responseText;
|
|
3799
|
+
if (text) {
|
|
3800
|
+
const bodyResult = redactNetworkTextBody(text, {
|
|
3801
|
+
contentType,
|
|
3802
|
+
maxLength: config.networkMaxBodySize,
|
|
3803
|
+
path: "body"
|
|
3804
|
+
});
|
|
3805
|
+
if (bodyResult.body !== void 0) {
|
|
3806
|
+
const ts = String(now());
|
|
3807
|
+
const dedupResult = checkDedup(
|
|
3808
|
+
urlResult.value,
|
|
3809
|
+
bodyResult.body,
|
|
3810
|
+
ts
|
|
3811
|
+
);
|
|
3812
|
+
if (dedupResult) {
|
|
3813
|
+
resData.body = dedupResult;
|
|
3814
|
+
resData.dedup = true;
|
|
3815
|
+
} else {
|
|
3816
|
+
resData.body = bodyResult.body;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (bodyResult.bodySummary)
|
|
3820
|
+
resData.bodySummary = bodyResult.bodySummary;
|
|
3821
|
+
resMetadata.push(bodyResult.metadata);
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
attachRedactionMetadata(resData, ...resMetadata);
|
|
3825
|
+
bus.emit({ t: now(), k: "net.res", d: resData });
|
|
3826
|
+
};
|
|
3827
|
+
this.addEventListener("load", emitResponse);
|
|
3828
|
+
this.addEventListener("error", emitResponse);
|
|
3829
|
+
this.addEventListener("timeout", emitResponse);
|
|
3830
|
+
pending.set(meta.id, {
|
|
3831
|
+
method: meta.method,
|
|
3832
|
+
url: urlResult.value,
|
|
3833
|
+
startTime: meta.startTime
|
|
3834
|
+
});
|
|
3835
|
+
this.addEventListener("loadend", () => pending.delete(meta.id));
|
|
3836
|
+
return origSend.call(this, body);
|
|
3837
|
+
};
|
|
3838
|
+
return { origOpen, origSend, origSetRequestHeader };
|
|
3839
|
+
}
|
|
3840
|
+
function networkCollector(bus, config, context) {
|
|
3841
|
+
const originalFetch = globalThis.fetch;
|
|
3842
|
+
const shouldPatchFetch = typeof originalFetch === "function";
|
|
3843
|
+
const xhrPrototype = globalThis.XMLHttpRequest?.prototype;
|
|
3844
|
+
const shouldPatchXHR = Boolean(
|
|
3845
|
+
xhrPrototype && typeof xhrPrototype.open === "function" && typeof xhrPrototype.send === "function" && typeof xhrPrototype.setRequestHeader === "function"
|
|
3846
|
+
);
|
|
3847
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3848
|
+
const originalXHRMethods = shouldPatchXHR ? wrapXHR(bus, config, xhrPrototype, context, pending) : void 0;
|
|
3849
|
+
if (shouldPatchFetch) {
|
|
3850
|
+
globalThis.fetch = wrapFetch(bus, config, originalFetch, context, pending);
|
|
3851
|
+
}
|
|
3852
|
+
const unregisterPendingProvider = context?.registerStateProvider?.(
|
|
3853
|
+
"network.pending",
|
|
3854
|
+
() => Array.from(pending.values()).map((request) => ({
|
|
3855
|
+
method: request.method,
|
|
3856
|
+
url: request.url,
|
|
3857
|
+
ageMs: now() - request.startTime
|
|
3858
|
+
}))
|
|
3859
|
+
);
|
|
3860
|
+
return () => {
|
|
3861
|
+
if (shouldPatchFetch) {
|
|
3862
|
+
globalThis.fetch = originalFetch;
|
|
3863
|
+
}
|
|
3864
|
+
if (shouldPatchXHR && originalXHRMethods) {
|
|
3865
|
+
xhrPrototype.open = originalXHRMethods.origOpen;
|
|
3866
|
+
xhrPrototype.send = originalXHRMethods.origSend;
|
|
3867
|
+
xhrPrototype.setRequestHeader = originalXHRMethods.origSetRequestHeader;
|
|
3868
|
+
}
|
|
3869
|
+
unregisterPendingProvider?.();
|
|
3870
|
+
pending.clear();
|
|
3871
|
+
clearDedupMap();
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// src/collectors/performance.ts
|
|
3876
|
+
var ENTRY_TYPES = [
|
|
3877
|
+
{
|
|
3878
|
+
type: "resource",
|
|
3879
|
+
metric: "res",
|
|
3880
|
+
extract: (entry) => {
|
|
3881
|
+
const name = redactUrl(String(entry.name ?? ""), "name");
|
|
3882
|
+
const data = {
|
|
3883
|
+
name: name.value,
|
|
3884
|
+
duration: entry.duration,
|
|
3885
|
+
transferSize: entry.transferSize,
|
|
3886
|
+
initiatorType: entry.initiatorType
|
|
3887
|
+
};
|
|
3888
|
+
attachRedactionMetadata(data, name.metadata);
|
|
3889
|
+
return data;
|
|
3890
|
+
}
|
|
3891
|
+
},
|
|
3892
|
+
{
|
|
3893
|
+
type: "longtask",
|
|
3894
|
+
metric: "longtask",
|
|
3895
|
+
extract: (entry) => ({
|
|
3896
|
+
duration: entry.duration,
|
|
3897
|
+
name: entry.name
|
|
3898
|
+
})
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
type: "layout-shift",
|
|
3902
|
+
metric: "cls",
|
|
3903
|
+
extract: (entry) => ({
|
|
3904
|
+
value: entry.value,
|
|
3905
|
+
hadRecentInput: entry.hadRecentInput
|
|
3906
|
+
})
|
|
3907
|
+
},
|
|
3908
|
+
{
|
|
3909
|
+
type: "largest-contentful-paint",
|
|
3910
|
+
metric: "lcp",
|
|
3911
|
+
extract: (entry) => {
|
|
3912
|
+
const data = {
|
|
3913
|
+
startTime: entry.startTime,
|
|
3914
|
+
size: entry.size
|
|
3915
|
+
};
|
|
3916
|
+
if (entry.element?.tagName) {
|
|
3917
|
+
data.element = entry.element.tagName;
|
|
3918
|
+
}
|
|
3919
|
+
return data;
|
|
3920
|
+
}
|
|
3921
|
+
},
|
|
3922
|
+
{
|
|
3923
|
+
type: "first-input",
|
|
3924
|
+
metric: "fid",
|
|
3925
|
+
extract: (entry) => ({
|
|
3926
|
+
delay: entry.processingStart - entry.startTime,
|
|
3927
|
+
name: entry.name
|
|
3928
|
+
})
|
|
3929
|
+
}
|
|
3930
|
+
];
|
|
3931
|
+
function performanceCollector(bus, _config) {
|
|
3932
|
+
if (typeof globalThis.PerformanceObserver === "undefined") {
|
|
3933
|
+
return () => {
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
const observers = [];
|
|
3937
|
+
for (const cfg of ENTRY_TYPES) {
|
|
3938
|
+
try {
|
|
3939
|
+
const observer = new PerformanceObserver((list) => {
|
|
3940
|
+
for (const entry of list.getEntries()) {
|
|
3941
|
+
bus.emit({
|
|
3942
|
+
t: now(),
|
|
3943
|
+
k: "perf",
|
|
3944
|
+
d: { metric: cfg.metric, ...cfg.extract(entry) }
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
});
|
|
3948
|
+
observer.observe({ type: cfg.type, buffered: true });
|
|
3949
|
+
observers.push(observer);
|
|
3950
|
+
} catch {
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return () => {
|
|
3954
|
+
for (const observer of observers) {
|
|
3955
|
+
observer.disconnect();
|
|
3956
|
+
}
|
|
3957
|
+
};
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
// src/collectors/heartbeat.ts
|
|
3961
|
+
var INTERVAL_MS = 3e4;
|
|
3962
|
+
function heartbeatCollector(bus, _config) {
|
|
3963
|
+
const id = setInterval(() => {
|
|
3964
|
+
const d = {};
|
|
3965
|
+
const heap = performance.memory?.usedJSHeapSize;
|
|
3966
|
+
if (heap !== void 0) {
|
|
3967
|
+
d.heap = heap;
|
|
3968
|
+
}
|
|
3969
|
+
if (typeof document !== "undefined") {
|
|
3970
|
+
d.dom = document.querySelectorAll("*").length;
|
|
3971
|
+
}
|
|
3972
|
+
bus.emit({ t: now(), k: "hb", d });
|
|
3973
|
+
}, INTERVAL_MS);
|
|
3974
|
+
return () => {
|
|
3975
|
+
clearInterval(id);
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/collectors/environment.ts
|
|
3980
|
+
function environmentCollector(bus, _config, context) {
|
|
3981
|
+
const declared = context.getDeclaredEnv?.() ?? {};
|
|
3982
|
+
const snapshot = buildEnvSnapshot(declared.flags, declared.config);
|
|
3983
|
+
bus.emit({
|
|
3984
|
+
t: now(),
|
|
3985
|
+
k: "env",
|
|
3986
|
+
d: snapshot
|
|
3987
|
+
});
|
|
3988
|
+
context.onEnvEmitted?.();
|
|
3989
|
+
return () => {
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
function buildEnvSnapshot(flags, config) {
|
|
3993
|
+
const snapshot = { kind: "snapshot" };
|
|
3994
|
+
const ua = safeUserAgent();
|
|
3995
|
+
if (ua) {
|
|
3996
|
+
snapshot.userAgent = ua;
|
|
3997
|
+
const browser = detectBrowser(ua);
|
|
3998
|
+
if (browser) snapshot.browser = browser;
|
|
3999
|
+
const os = detectOs(ua);
|
|
4000
|
+
if (os) snapshot.os = os;
|
|
4001
|
+
}
|
|
4002
|
+
const viewport = safeViewport();
|
|
4003
|
+
if (viewport) snapshot.viewport = viewport;
|
|
4004
|
+
const locale = safeLocale();
|
|
4005
|
+
if (locale) snapshot.locale = locale;
|
|
4006
|
+
const timezone = safeTimezone();
|
|
4007
|
+
if (timezone) snapshot.timezone = timezone;
|
|
4008
|
+
applyDeclaredEnv(snapshot, flags, config);
|
|
4009
|
+
return snapshot;
|
|
4010
|
+
}
|
|
4011
|
+
function buildEnvDelta(flags, config) {
|
|
4012
|
+
const delta = { kind: "delta" };
|
|
4013
|
+
applyDeclaredEnv(delta, flags, config);
|
|
4014
|
+
return delta;
|
|
4015
|
+
}
|
|
4016
|
+
function applyDeclaredEnv(target, flags, config) {
|
|
4017
|
+
const metadataItems = [];
|
|
4018
|
+
if (flags && Object.keys(flags).length > 0) {
|
|
4019
|
+
const result = redactValue(flags, "env.flags");
|
|
4020
|
+
target.flags = result.value;
|
|
4021
|
+
if (result.metadata) metadataItems.push(result.metadata);
|
|
4022
|
+
}
|
|
4023
|
+
if (config && Object.keys(config).length > 0) {
|
|
4024
|
+
const result = redactValue(config, "env.config");
|
|
4025
|
+
target.config = result.value;
|
|
4026
|
+
if (result.metadata) metadataItems.push(result.metadata);
|
|
4027
|
+
}
|
|
4028
|
+
if (metadataItems.length > 0) {
|
|
4029
|
+
target.redaction = mergeMetadata(metadataItems);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
function mergeMetadata(items) {
|
|
4033
|
+
return {
|
|
4034
|
+
policy: items[0].policy,
|
|
4035
|
+
fields: items.flatMap((item) => item.fields)
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
function safeUserAgent() {
|
|
4039
|
+
try {
|
|
4040
|
+
if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
|
|
4041
|
+
return navigator.userAgent;
|
|
4042
|
+
}
|
|
4043
|
+
} catch {
|
|
4044
|
+
}
|
|
4045
|
+
return void 0;
|
|
4046
|
+
}
|
|
4047
|
+
function safeViewport() {
|
|
4048
|
+
try {
|
|
4049
|
+
if (typeof window !== "undefined" && typeof window.innerWidth === "number" && typeof window.innerHeight === "number") {
|
|
4050
|
+
return { w: window.innerWidth, h: window.innerHeight };
|
|
4051
|
+
}
|
|
4052
|
+
} catch {
|
|
4053
|
+
}
|
|
4054
|
+
return void 0;
|
|
4055
|
+
}
|
|
4056
|
+
function safeLocale() {
|
|
4057
|
+
try {
|
|
4058
|
+
const resolved = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
4059
|
+
if (resolved) return resolved;
|
|
4060
|
+
} catch {
|
|
4061
|
+
}
|
|
4062
|
+
try {
|
|
4063
|
+
if (typeof navigator !== "undefined" && typeof navigator.language === "string") {
|
|
4064
|
+
return navigator.language;
|
|
4065
|
+
}
|
|
4066
|
+
} catch {
|
|
4067
|
+
}
|
|
4068
|
+
return void 0;
|
|
4069
|
+
}
|
|
4070
|
+
function safeTimezone() {
|
|
4071
|
+
try {
|
|
4072
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
4073
|
+
if (tz) return tz;
|
|
4074
|
+
} catch {
|
|
4075
|
+
}
|
|
4076
|
+
return void 0;
|
|
4077
|
+
}
|
|
4078
|
+
function detectBrowser(ua) {
|
|
4079
|
+
const tests = [
|
|
4080
|
+
{ name: "Edge", re: /Edg(?:e|A|iOS)?\/([\d.]+)/ },
|
|
4081
|
+
{ name: "Opera", re: /OPR\/([\d.]+)/ },
|
|
4082
|
+
{ name: "Chrome", re: /Chrome\/([\d.]+)/ },
|
|
4083
|
+
{ name: "Firefox", re: /Firefox\/([\d.]+)/ },
|
|
4084
|
+
{ name: "Safari", re: /Version\/([\d.]+).*Safari/ }
|
|
4085
|
+
];
|
|
4086
|
+
for (const { name, re } of tests) {
|
|
4087
|
+
const match = ua.match(re);
|
|
4088
|
+
if (match) return match[1] ? { name, version: match[1] } : { name };
|
|
4089
|
+
}
|
|
4090
|
+
return void 0;
|
|
4091
|
+
}
|
|
4092
|
+
function detectOs(ua) {
|
|
4093
|
+
if (/Windows NT/.test(ua)) return "Windows";
|
|
4094
|
+
if (/Mac OS X/.test(ua)) return "macOS";
|
|
4095
|
+
if (/Android/.test(ua)) return "Android";
|
|
4096
|
+
if (/(iPhone|iPad|iPod)/.test(ua)) return "iOS";
|
|
4097
|
+
if (/Linux/.test(ua)) return "Linux";
|
|
4098
|
+
return void 0;
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
// src/bug-logger.ts
|
|
4102
|
+
var PRESETS = {
|
|
4103
|
+
full: PRESET_FULL,
|
|
4104
|
+
light: PRESET_LIGHT,
|
|
4105
|
+
passive: PRESET_PASSIVE
|
|
4106
|
+
};
|
|
4107
|
+
var COLLECTOR_MAP = {
|
|
4108
|
+
environment: environmentCollector,
|
|
4109
|
+
console: consoleCollector,
|
|
4110
|
+
errors: errorCollector,
|
|
4111
|
+
interactions: interactionCollector,
|
|
4112
|
+
keystrokes: keystrokeCollector,
|
|
4113
|
+
scroll: scrollCollector,
|
|
4114
|
+
visibility: visibilityCollector,
|
|
4115
|
+
clipboard: clipboardCollector,
|
|
4116
|
+
cookies: cookieCollector,
|
|
4117
|
+
storage: storageCollector,
|
|
4118
|
+
network: networkCollector,
|
|
4119
|
+
performance: performanceCollector,
|
|
4120
|
+
heartbeat: heartbeatCollector
|
|
4121
|
+
};
|
|
4122
|
+
var SEVERITY_FLUSH_MIN_INTERVAL_MS = 1e3;
|
|
4123
|
+
var INERT_TRANSPORT = {
|
|
4124
|
+
async sendEvents() {
|
|
4125
|
+
},
|
|
4126
|
+
async sendBlob() {
|
|
4127
|
+
},
|
|
4128
|
+
async startSession() {
|
|
4129
|
+
},
|
|
4130
|
+
async endSession() {
|
|
4131
|
+
},
|
|
4132
|
+
async sendBugReport() {
|
|
4133
|
+
}
|
|
4134
|
+
};
|
|
4135
|
+
function bodyPlaceholder2(summary) {
|
|
4136
|
+
return summary ? `[${summary.action}:${summary.reason}]` : "[REDACTED]";
|
|
4137
|
+
}
|
|
4138
|
+
function readPersistedSessionId(store, idleMs) {
|
|
4139
|
+
const persisted = store.read();
|
|
4140
|
+
if (!persisted) return void 0;
|
|
4141
|
+
if (now() - persisted.lastActivity > idleMs) return void 0;
|
|
4142
|
+
return persisted.id;
|
|
4143
|
+
}
|
|
4144
|
+
function writePersistedSession(store, id) {
|
|
4145
|
+
store.write({ id, lastActivity: now() });
|
|
4146
|
+
}
|
|
4147
|
+
var Crumbtrail = class _Crumbtrail {
|
|
4148
|
+
bus;
|
|
4149
|
+
transport;
|
|
4150
|
+
ringBuffer;
|
|
4151
|
+
cleanups = [];
|
|
4152
|
+
config;
|
|
4153
|
+
sessionId;
|
|
4154
|
+
widgetCleanup;
|
|
4155
|
+
stateProviders = /* @__PURE__ */ new Map();
|
|
4156
|
+
declaredFlags = {};
|
|
4157
|
+
declaredConfig = {};
|
|
4158
|
+
envEmitted = false;
|
|
4159
|
+
constructor(config, bus, transport, ringBuffer, sessionId) {
|
|
4160
|
+
this.config = config;
|
|
4161
|
+
this.bus = bus;
|
|
4162
|
+
this.transport = transport;
|
|
4163
|
+
this.ringBuffer = ringBuffer;
|
|
4164
|
+
this.sessionId = sessionId;
|
|
4165
|
+
}
|
|
4166
|
+
static init(presetOrConfig) {
|
|
4167
|
+
const overrides = typeof presetOrConfig === "string" ? PRESETS[presetOrConfig] : presetOrConfig;
|
|
4168
|
+
const config = { ...DEFAULT_CONFIG, ...overrides };
|
|
4169
|
+
if (typeof window === "undefined" && !config.transportInstance) {
|
|
4170
|
+
return new _Crumbtrail(
|
|
4171
|
+
config,
|
|
4172
|
+
new EventBus(),
|
|
4173
|
+
INERT_TRANSPORT,
|
|
4174
|
+
new RingBuffer(config.ringBufferMs, config.ringBufferMaxEvents),
|
|
4175
|
+
config.sessionId ?? generateSessionId()
|
|
4176
|
+
);
|
|
4177
|
+
}
|
|
4178
|
+
const sessionStore = config.sessionPersistence === "session" ? config.sessionStore ?? createWebSessionStore() : void 0;
|
|
4179
|
+
const useSessionStore = Boolean(sessionStore);
|
|
4180
|
+
const sessionId = config.sessionId ?? (sessionStore ? readPersistedSessionId(sessionStore, config.sessionIdleMs) : void 0) ?? generateSessionId();
|
|
4181
|
+
if (sessionStore) writePersistedSession(sessionStore, sessionId);
|
|
4182
|
+
const bus = new EventBus();
|
|
4183
|
+
const ringBuffer = new RingBuffer(
|
|
4184
|
+
config.ringBufferMs,
|
|
4185
|
+
config.ringBufferMaxEvents
|
|
4186
|
+
);
|
|
4187
|
+
const transport = config.transportInstance ?? new HttpTransport(config.httpEndpoint, {
|
|
4188
|
+
authToken: config.httpAuthToken
|
|
4189
|
+
});
|
|
4190
|
+
bus.subscribe((events) => {
|
|
4191
|
+
transport.sendEvents(events).catch(() => {
|
|
4192
|
+
});
|
|
4193
|
+
});
|
|
4194
|
+
bus.subscribe((events) => {
|
|
4195
|
+
ringBuffer.pushBatch(events);
|
|
4196
|
+
});
|
|
4197
|
+
if (useSessionStore && sessionStore) {
|
|
4198
|
+
bus.subscribe(() => {
|
|
4199
|
+
writePersistedSession(sessionStore, sessionId);
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
bus.start(config.flushIntervalMs, config.flushBufferSize);
|
|
4203
|
+
const instance = new _Crumbtrail(
|
|
4204
|
+
config,
|
|
4205
|
+
bus,
|
|
4206
|
+
transport,
|
|
4207
|
+
ringBuffer,
|
|
4208
|
+
sessionId
|
|
4209
|
+
);
|
|
4210
|
+
let lastSeverityFlushAt = Number.NEGATIVE_INFINITY;
|
|
4211
|
+
let severityFlushPending = false;
|
|
4212
|
+
instance.cleanups.push(
|
|
4213
|
+
bus.tap((event) => {
|
|
4214
|
+
if (severityFlushPending) return;
|
|
4215
|
+
if (!isSevereEvent(event)) return;
|
|
4216
|
+
if (now() - lastSeverityFlushAt < SEVERITY_FLUSH_MIN_INTERVAL_MS)
|
|
4217
|
+
return;
|
|
4218
|
+
lastSeverityFlushAt = now();
|
|
4219
|
+
severityFlushPending = true;
|
|
4220
|
+
queueMicrotask(() => {
|
|
4221
|
+
severityFlushPending = false;
|
|
4222
|
+
bus.flush();
|
|
4223
|
+
});
|
|
4224
|
+
})
|
|
4225
|
+
);
|
|
4226
|
+
if (typeof window !== "undefined") {
|
|
4227
|
+
const flushOnPageHide = () => bus.flush();
|
|
4228
|
+
window.addEventListener("pagehide", flushOnPageHide);
|
|
4229
|
+
instance.cleanups.push(
|
|
4230
|
+
() => window.removeEventListener("pagehide", flushOnPageHide)
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
const collectorContext = {
|
|
4234
|
+
sessionId,
|
|
4235
|
+
getDeclaredEnv: () => ({
|
|
4236
|
+
flags: instance.declaredFlags,
|
|
4237
|
+
config: instance.declaredConfig
|
|
4238
|
+
}),
|
|
4239
|
+
onEnvEmitted: () => {
|
|
4240
|
+
instance.envEmitted = true;
|
|
4241
|
+
},
|
|
4242
|
+
registerStateProvider: (name, provider) => instance.registerStateProvider(name, provider)
|
|
4243
|
+
};
|
|
4244
|
+
const autoFlagDetectors = [];
|
|
4245
|
+
if (config.autoFlagOnError) autoFlagDetectors.push(errorDetector());
|
|
4246
|
+
if (config.autoFlagOnSignals) {
|
|
4247
|
+
autoFlagDetectors.push(
|
|
4248
|
+
rageClickDetector({
|
|
4249
|
+
threshold: config.rageClickThreshold,
|
|
4250
|
+
windowMs: config.rageClickWindowMs
|
|
4251
|
+
}),
|
|
4252
|
+
retryStormDetector({
|
|
4253
|
+
threshold: config.retryStormThreshold,
|
|
4254
|
+
windowMs: config.retryStormWindowMs,
|
|
4255
|
+
failThreshold: config.retryStormFailThreshold
|
|
4256
|
+
}),
|
|
4257
|
+
slowResponseDetector({
|
|
4258
|
+
thresholdMs: config.slowRequestMs,
|
|
4259
|
+
count: config.slowRequestCount,
|
|
4260
|
+
windowMs: config.slowRequestWindowMs
|
|
4261
|
+
}),
|
|
4262
|
+
abandonedFlowDetector({
|
|
4263
|
+
windowMs: config.abandonedFlowWindowMs,
|
|
4264
|
+
minInputs: config.abandonedFlowMinInputs
|
|
4265
|
+
})
|
|
4266
|
+
);
|
|
4267
|
+
}
|
|
4268
|
+
if (autoFlagDetectors.length > 0) {
|
|
4269
|
+
const autoFlag = createAutoFlagController({
|
|
4270
|
+
debounceMs: config.autoFlagDebounceMs,
|
|
4271
|
+
maxPerSession: config.autoFlagMaxPerSession,
|
|
4272
|
+
flag: (options) => instance.flagBug(options),
|
|
4273
|
+
detectors: autoFlagDetectors
|
|
4274
|
+
});
|
|
4275
|
+
instance.cleanups.push(bus.tap((event) => autoFlag.handleEvent(event)));
|
|
4276
|
+
instance.cleanups.push(() => autoFlag.dispose());
|
|
4277
|
+
}
|
|
4278
|
+
transport.startSession(sessionId, {
|
|
4279
|
+
url: typeof location !== "undefined" ? location.href : "",
|
|
4280
|
+
ua: typeof navigator !== "undefined" ? navigator.userAgent : ""
|
|
4281
|
+
}).catch(() => {
|
|
4282
|
+
});
|
|
4283
|
+
for (const [key, collector] of Object.entries(COLLECTOR_MAP)) {
|
|
4284
|
+
if (config[key]) {
|
|
4285
|
+
instance.cleanups.push(collector(bus, config, collectorContext));
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
if (config.widget && typeof document !== "undefined") {
|
|
4289
|
+
Promise.resolve().then(() => (init_bug_widget(), bug_widget_exports)).then(({ mountWidget: mountWidget2 }) => {
|
|
4290
|
+
instance.widgetCleanup = mountWidget2(instance);
|
|
4291
|
+
}).catch(() => {
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
return instance;
|
|
4295
|
+
}
|
|
4296
|
+
async flagBug(options) {
|
|
4297
|
+
const bugId = `bug_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
4298
|
+
const windowMs = options?.windowMs ?? this.config.ringBufferMs;
|
|
4299
|
+
const flaggedAt = now();
|
|
4300
|
+
const stateProviderNames = Array.from(this.stateProviders.keys());
|
|
4301
|
+
for (const [name, provider] of this.stateProviders) {
|
|
4302
|
+
try {
|
|
4303
|
+
const rawValue = provider();
|
|
4304
|
+
const state = this.config.captureRawState ? { value: rawValue, metadata: void 0 } : redactValue(rawValue, `state.${name}`);
|
|
4305
|
+
const value = state.value;
|
|
4306
|
+
const json = JSON.stringify(value);
|
|
4307
|
+
const truncated = json.length > this.config.stateMaxBytes ? `${json.slice(0, this.config.stateMaxBytes)}...` : json;
|
|
4308
|
+
const d = {
|
|
4309
|
+
name,
|
|
4310
|
+
json: truncated,
|
|
4311
|
+
truncated: truncated !== json
|
|
4312
|
+
};
|
|
4313
|
+
if (!this.config.captureRawState)
|
|
4314
|
+
attachRedactionMetadata(d, state.metadata);
|
|
4315
|
+
this.bus.emit({
|
|
4316
|
+
t: flaggedAt,
|
|
4317
|
+
k: "state.snap",
|
|
4318
|
+
d
|
|
4319
|
+
});
|
|
4320
|
+
} catch (err) {
|
|
4321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4322
|
+
const redactedMsg = this.config.captureRawState ? { body: msg, metadata: void 0 } : redactNetworkTextBody(msg, {
|
|
4323
|
+
contentType: "text/plain",
|
|
4324
|
+
path: "msg"
|
|
4325
|
+
});
|
|
4326
|
+
const d = {
|
|
4327
|
+
name,
|
|
4328
|
+
msg: redactedMsg.body ?? bodyPlaceholder2(redactedMsg.bodySummary),
|
|
4329
|
+
...redactedMsg.bodySummary ? { msgSummary: redactedMsg.bodySummary } : {}
|
|
4330
|
+
};
|
|
4331
|
+
if (!this.config.captureRawState)
|
|
4332
|
+
attachRedactionMetadata(d, redactedMsg.metadata);
|
|
4333
|
+
this.bus.emit({
|
|
4334
|
+
t: flaggedAt,
|
|
4335
|
+
k: "state.err",
|
|
4336
|
+
d
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
if (this.config.domSnapshot && typeof document !== "undefined") {
|
|
4341
|
+
try {
|
|
4342
|
+
const fullHtml = document.documentElement.outerHTML;
|
|
4343
|
+
const clipped = fullHtml.slice(0, this.config.domSnapshotMaxBytes);
|
|
4344
|
+
const redacted = this.config.captureRawState ? { body: clipped, metadata: void 0 } : redactNetworkTextBody(clipped, {
|
|
4345
|
+
contentType: "text/html",
|
|
4346
|
+
path: "dom"
|
|
4347
|
+
});
|
|
4348
|
+
const d = {
|
|
4349
|
+
html: redacted.body ?? clipped,
|
|
4350
|
+
truncated: clipped.length !== fullHtml.length,
|
|
4351
|
+
bytes: fullHtml.length
|
|
4352
|
+
};
|
|
4353
|
+
if (!this.config.captureRawState)
|
|
4354
|
+
attachRedactionMetadata(d, redacted.metadata);
|
|
4355
|
+
this.bus.emit({ t: flaggedAt, k: "dom.snap", d });
|
|
4356
|
+
} catch {
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
this.bus.emit({
|
|
4360
|
+
t: flaggedAt,
|
|
4361
|
+
k: "bug.flag",
|
|
4362
|
+
d: { bugId, note: options?.note }
|
|
4363
|
+
});
|
|
4364
|
+
this.bus.flush();
|
|
4365
|
+
const events = this.ringBuffer.snapshot(windowMs);
|
|
4366
|
+
const errorCount = events.filter(
|
|
4367
|
+
(e) => e.k === "err" || e.k === "rej"
|
|
4368
|
+
).length;
|
|
4369
|
+
const failedRequestCount = events.filter(
|
|
4370
|
+
(e) => isFailedNetworkResponse(e)
|
|
4371
|
+
).length;
|
|
4372
|
+
const eventKinds = {};
|
|
4373
|
+
for (const e of events) {
|
|
4374
|
+
eventKinds[e.k] = (eventKinds[e.k] || 0) + 1;
|
|
4375
|
+
}
|
|
4376
|
+
const durationMs = events.length >= 2 ? events[events.length - 1].t - events[0].t : 0;
|
|
4377
|
+
const report = {
|
|
4378
|
+
bugId,
|
|
4379
|
+
sessionId: this.sessionId,
|
|
4380
|
+
flaggedAt,
|
|
4381
|
+
windowMs,
|
|
4382
|
+
note: options?.note,
|
|
4383
|
+
voiceNote: options?.voiceBlob ? "voice.webm" : void 0,
|
|
4384
|
+
url: typeof location !== "undefined" ? location.href : "",
|
|
4385
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
4386
|
+
tags: options?.tags,
|
|
4387
|
+
summary: {
|
|
4388
|
+
errorCount,
|
|
4389
|
+
failedRequestCount,
|
|
4390
|
+
eventCount: events.length,
|
|
4391
|
+
eventKinds,
|
|
4392
|
+
durationMs,
|
|
4393
|
+
stateProviderCount: stateProviderNames.length
|
|
4394
|
+
}
|
|
4395
|
+
};
|
|
4396
|
+
await this.transport.sendBugReport(report, events, options?.voiceBlob);
|
|
4397
|
+
return { bugId };
|
|
4398
|
+
}
|
|
4399
|
+
mark(label) {
|
|
4400
|
+
this.bus.emit({ t: now(), k: "mark", d: { label } });
|
|
4401
|
+
}
|
|
4402
|
+
addEvent(partial) {
|
|
4403
|
+
const { type, data, ...envelope } = partial;
|
|
4404
|
+
this.bus.emit({ t: now(), k: type, d: data, ...envelope });
|
|
4405
|
+
}
|
|
4406
|
+
getSessionId() {
|
|
4407
|
+
return this.sessionId;
|
|
4408
|
+
}
|
|
4409
|
+
createRequestHeaders(requestId) {
|
|
4410
|
+
return createCrumbtrailRequestHeaders(this.sessionId, requestId);
|
|
4411
|
+
}
|
|
4412
|
+
pause() {
|
|
4413
|
+
this.bus.pause();
|
|
4414
|
+
}
|
|
4415
|
+
resume() {
|
|
4416
|
+
this.bus.resume();
|
|
4417
|
+
}
|
|
4418
|
+
registerStateProvider(name, provider) {
|
|
4419
|
+
this.stateProviders.set(name, provider);
|
|
4420
|
+
return () => {
|
|
4421
|
+
this.stateProviders.delete(name);
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
/**
|
|
4425
|
+
* Declaratively attach vendor-agnostic feature flags / config to the session environment.
|
|
4426
|
+
* Values are redacted before they rest. Merges into the declared env; if the initial
|
|
4427
|
+
* `k:'env'` snapshot has already been emitted (the normal case, since `setEnv` is called
|
|
4428
|
+
* after `init`), it emits a `k:'env'` delta event ({ kind:'delta' }). If called before the
|
|
4429
|
+
* snapshot is emitted (e.g. environment collector disabled or not yet run), the values are
|
|
4430
|
+
* folded into the snapshot instead.
|
|
4431
|
+
*/
|
|
4432
|
+
setEnv(declaration) {
|
|
4433
|
+
if (declaration.flags) Object.assign(this.declaredFlags, declaration.flags);
|
|
4434
|
+
if (declaration.config)
|
|
4435
|
+
Object.assign(this.declaredConfig, declaration.config);
|
|
4436
|
+
if (!this.envEmitted) return;
|
|
4437
|
+
const delta = buildEnvDelta(declaration.flags, declaration.config);
|
|
4438
|
+
this.bus.emit({
|
|
4439
|
+
t: now(),
|
|
4440
|
+
k: "env",
|
|
4441
|
+
d: delta
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
async stop() {
|
|
4445
|
+
if (this.widgetCleanup) this.widgetCleanup();
|
|
4446
|
+
for (const cleanup of this.cleanups) {
|
|
4447
|
+
cleanup();
|
|
4448
|
+
}
|
|
4449
|
+
this.stateProviders.clear();
|
|
4450
|
+
this.bus.stop();
|
|
4451
|
+
this.ringBuffer.clear();
|
|
4452
|
+
await this.transport.endSession(this.sessionId);
|
|
4453
|
+
return { sessionId: this.sessionId };
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
function isSevereEvent(event) {
|
|
4457
|
+
return event.k === "err" || event.k === "rej" || isFailedNetworkResponse(event);
|
|
4458
|
+
}
|
|
4459
|
+
function isFailedNetworkResponse(event) {
|
|
4460
|
+
return event.k === "net.res" && (typeof event.d.st === "number" && event.d.st >= 400 || hasApplicationFailure(event.d.body));
|
|
4461
|
+
}
|
|
4462
|
+
function hasApplicationFailure(value) {
|
|
4463
|
+
if (typeof value === "string") return hasApplicationFailureInText(value);
|
|
4464
|
+
if (Array.isArray(value))
|
|
4465
|
+
return value.some((item) => hasApplicationFailure(item));
|
|
4466
|
+
if (!isRecord2(value) || value.dedup === true) return false;
|
|
4467
|
+
if (value.ok === false || value.status === "failed") return true;
|
|
4468
|
+
return Object.values(value).some((nested) => hasApplicationFailure(nested));
|
|
4469
|
+
}
|
|
4470
|
+
function hasApplicationFailureInText(text) {
|
|
4471
|
+
for (const candidate of extractJsonCandidates(text)) {
|
|
4472
|
+
try {
|
|
4473
|
+
if (hasApplicationFailure(JSON.parse(candidate))) return true;
|
|
4474
|
+
} catch {
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
return false;
|
|
4478
|
+
}
|
|
4479
|
+
function extractJsonCandidates(text) {
|
|
4480
|
+
const trimmed = text.trim();
|
|
4481
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
4482
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("["))
|
|
4483
|
+
candidates.add(trimmed);
|
|
4484
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
4485
|
+
const chunk = line.trim();
|
|
4486
|
+
if (!chunk) continue;
|
|
4487
|
+
const framed = chunk.match(/^\d+:(.*)$/);
|
|
4488
|
+
const unframed = (framed?.[1] ?? chunk).trim();
|
|
4489
|
+
if (unframed.startsWith("{") || unframed.startsWith("["))
|
|
4490
|
+
candidates.add(unframed);
|
|
4491
|
+
const objectStart = unframed.indexOf("{");
|
|
4492
|
+
if (objectStart >= 0) candidates.add(unframed.slice(objectStart));
|
|
4493
|
+
}
|
|
4494
|
+
return [...candidates];
|
|
4495
|
+
}
|
|
4496
|
+
function isRecord2(value) {
|
|
4497
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
// src/target-resolver.ts
|
|
4501
|
+
var WebTargetDescriptorResolver = class {
|
|
4502
|
+
resolve(element) {
|
|
4503
|
+
try {
|
|
4504
|
+
const signature = computeElementSignature(element);
|
|
4505
|
+
return {
|
|
4506
|
+
componentName: element.tagName.toLowerCase(),
|
|
4507
|
+
ancestryHash: signature.sig,
|
|
4508
|
+
selector: signature.path
|
|
4509
|
+
};
|
|
4510
|
+
} catch {
|
|
4511
|
+
return void 0;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
};
|
|
4515
|
+
var webTargetDescriptorResolver = new WebTargetDescriptorResolver();
|
|
4516
|
+
|
|
4517
|
+
// src/evidence.ts
|
|
4518
|
+
var EVIDENCE_SCHEMA_VERSION = "evidence.v1";
|
|
4519
|
+
|
|
4520
|
+
// src/tokenize.ts
|
|
4521
|
+
function tokenize(s) {
|
|
4522
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 3);
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
// src/intent.ts
|
|
4526
|
+
var MIN_SHARED = 2;
|
|
4527
|
+
var STRUCTURAL_TOKENS = /* @__PURE__ */ new Set([
|
|
4528
|
+
"src",
|
|
4529
|
+
"dist",
|
|
4530
|
+
"index",
|
|
4531
|
+
"node",
|
|
4532
|
+
"lib",
|
|
4533
|
+
"libs",
|
|
4534
|
+
"test",
|
|
4535
|
+
"tests",
|
|
4536
|
+
"app",
|
|
4537
|
+
"apps",
|
|
4538
|
+
"routes",
|
|
4539
|
+
"route",
|
|
4540
|
+
"api",
|
|
4541
|
+
"www",
|
|
4542
|
+
"http",
|
|
4543
|
+
"https",
|
|
4544
|
+
"com",
|
|
4545
|
+
"main",
|
|
4546
|
+
"packages",
|
|
4547
|
+
"pkg",
|
|
4548
|
+
"build"
|
|
4549
|
+
]);
|
|
4550
|
+
var ROUTE_SEGMENT_PATTERN = /\/[a-z0-9_\-/]+/gi;
|
|
4551
|
+
function tokensForEvidence(item) {
|
|
4552
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4553
|
+
const { ref, brief } = item;
|
|
4554
|
+
if (ref.sig) for (const t of tokenize(ref.sig)) tokens.add(t);
|
|
4555
|
+
if (ref.table) for (const t of tokenize(ref.table)) tokens.add(t);
|
|
4556
|
+
if (brief) for (const t of tokenize(brief)) tokens.add(t);
|
|
4557
|
+
return tokens;
|
|
4558
|
+
}
|
|
4559
|
+
function tokensForCommit(commit) {
|
|
4560
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4561
|
+
for (const file of commit.files) {
|
|
4562
|
+
for (const t of tokenize(file)) tokens.add(t);
|
|
4563
|
+
}
|
|
4564
|
+
for (const t of tokenize(commit.message)) tokens.add(t);
|
|
4565
|
+
return tokens;
|
|
4566
|
+
}
|
|
4567
|
+
function sharedCount(a, b) {
|
|
4568
|
+
let count = 0;
|
|
4569
|
+
for (const token of a) if (b.has(token)) count++;
|
|
4570
|
+
return count;
|
|
4571
|
+
}
|
|
4572
|
+
function withoutStructural(tokens) {
|
|
4573
|
+
const result = /* @__PURE__ */ new Set();
|
|
4574
|
+
for (const t of tokens) if (!STRUCTURAL_TOKENS.has(t)) result.add(t);
|
|
4575
|
+
return result;
|
|
4576
|
+
}
|
|
4577
|
+
function evidenceIdentityTokens(item) {
|
|
4578
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4579
|
+
const { ref, brief } = item;
|
|
4580
|
+
if (ref.table) for (const t of tokenize(ref.table)) tokens.add(t);
|
|
4581
|
+
if (brief) {
|
|
4582
|
+
const matches = brief.match(ROUTE_SEGMENT_PATTERN) ?? [];
|
|
4583
|
+
for (const segment2 of matches) {
|
|
4584
|
+
for (const t of tokenize(segment2)) tokens.add(t);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
return withoutStructural(tokens);
|
|
4588
|
+
}
|
|
4589
|
+
function commitPathTokens(commit) {
|
|
4590
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4591
|
+
for (const file of commit.files) {
|
|
4592
|
+
for (const t of tokenize(file)) tokens.add(t);
|
|
4593
|
+
}
|
|
4594
|
+
return withoutStructural(tokens);
|
|
4595
|
+
}
|
|
4596
|
+
function inferIntent(evidence, commits) {
|
|
4597
|
+
const signals = [];
|
|
4598
|
+
const commitTokens = commits.map((commit) => ({
|
|
4599
|
+
commit,
|
|
4600
|
+
tokens: tokensForCommit(commit),
|
|
4601
|
+
pathTokens: commitPathTokens(commit)
|
|
4602
|
+
}));
|
|
4603
|
+
for (const item of evidence) {
|
|
4604
|
+
const evidenceTokens = tokensForEvidence(item);
|
|
4605
|
+
const identityTokens = evidenceIdentityTokens(item);
|
|
4606
|
+
let best;
|
|
4607
|
+
for (const { commit, tokens, pathTokens } of commitTokens) {
|
|
4608
|
+
const strong = sharedCount(identityTokens, pathTokens);
|
|
4609
|
+
const weak = sharedCount(evidenceTokens, tokens);
|
|
4610
|
+
const qualifies = strong >= 1 || weak >= MIN_SHARED;
|
|
4611
|
+
if (!qualifies) continue;
|
|
4612
|
+
const score = strong * 100 + weak;
|
|
4613
|
+
if (!best || score > best.score || score === best.score && commit.sha < best.commit.sha) {
|
|
4614
|
+
best = { commit, score };
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
if (best) {
|
|
4618
|
+
signals.push({
|
|
4619
|
+
evidenceId: item.id,
|
|
4620
|
+
explainedByCommit: {
|
|
4621
|
+
sha: best.commit.sha,
|
|
4622
|
+
pr: best.commit.pr,
|
|
4623
|
+
message: best.commit.message
|
|
4624
|
+
},
|
|
4625
|
+
prIntent: best.commit.message.split("\n")[0]
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return signals;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
// src/fusion.ts
|
|
4633
|
+
var FUSION_SCHEMA_VERSION = "fusion.v1";
|
|
4634
|
+
function assembleBundle(input) {
|
|
4635
|
+
const evidence = rankEvidence(input.symptom, input.evidence);
|
|
4636
|
+
const hypotheses = classifyHypotheses(
|
|
4637
|
+
input.symptom,
|
|
4638
|
+
input.evidence,
|
|
4639
|
+
input.intent
|
|
4640
|
+
);
|
|
4641
|
+
const gaps = input.gaps ?? [];
|
|
4642
|
+
return {
|
|
4643
|
+
schemaVersion: FUSION_SCHEMA_VERSION,
|
|
4644
|
+
symptom: input.symptom,
|
|
4645
|
+
evidence,
|
|
4646
|
+
opinion: { stance: "advisory", hypotheses },
|
|
4647
|
+
gaps,
|
|
4648
|
+
directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
var LANE_PRIOR = {
|
|
4652
|
+
db: 0.2,
|
|
4653
|
+
network: 0.2,
|
|
4654
|
+
flow: 0.15,
|
|
4655
|
+
env: 0.1,
|
|
4656
|
+
browser: 0.1,
|
|
4657
|
+
logs: 0.05,
|
|
4658
|
+
memory: 0.05,
|
|
4659
|
+
code: 0.05
|
|
4660
|
+
};
|
|
4661
|
+
function jaccard(a, b) {
|
|
4662
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
4663
|
+
const setA = new Set(a);
|
|
4664
|
+
const setB = new Set(b);
|
|
4665
|
+
let intersection = 0;
|
|
4666
|
+
for (const value of setA) if (setB.has(value)) intersection += 1;
|
|
4667
|
+
const union = (/* @__PURE__ */ new Set([...a, ...b])).size;
|
|
4668
|
+
return union === 0 ? 0 : intersection / union;
|
|
4669
|
+
}
|
|
4670
|
+
function symptomText(symptom) {
|
|
4671
|
+
return [symptom.title, symptom.description, symptom.errorSig].filter((part) => Boolean(part)).join(" ");
|
|
4672
|
+
}
|
|
4673
|
+
function scoreEvidenceRelevance(symptom, item) {
|
|
4674
|
+
let score = 0;
|
|
4675
|
+
if (symptom.url) {
|
|
4676
|
+
const needle = symptom.url.toLowerCase();
|
|
4677
|
+
const haystack = `${item.ref.sig ?? ""} ${item.brief}`.toLowerCase();
|
|
4678
|
+
if (haystack.includes(needle)) score += 0.5;
|
|
4679
|
+
}
|
|
4680
|
+
const symptomTokens = tokenize(symptomText(symptom));
|
|
4681
|
+
const itemTokens = tokenize(`${item.brief} ${item.kind}`);
|
|
4682
|
+
score += 0.3 * jaccard(symptomTokens, itemTokens);
|
|
4683
|
+
score += LANE_PRIOR[item.lane] ?? 0.05;
|
|
4684
|
+
return Math.min(1, score);
|
|
4685
|
+
}
|
|
4686
|
+
function rankEvidence(symptom, items) {
|
|
4687
|
+
return items.map((item, index) => ({
|
|
4688
|
+
item,
|
|
4689
|
+
index,
|
|
4690
|
+
score: scoreEvidenceRelevance(symptom, item)
|
|
4691
|
+
})).sort((a, b) => {
|
|
4692
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
4693
|
+
return a.index - b.index;
|
|
4694
|
+
}).map((entry) => entry.item);
|
|
4695
|
+
}
|
|
4696
|
+
function classifyHypotheses(symptom, evidence, intent) {
|
|
4697
|
+
const hypotheses = [];
|
|
4698
|
+
const evidenceIds = new Set(evidence.map((item) => item.id));
|
|
4699
|
+
const explainedById = /* @__PURE__ */ new Map();
|
|
4700
|
+
for (const signal of intent) {
|
|
4701
|
+
if (signal.explainedByCommit && evidenceIds.has(signal.evidenceId)) {
|
|
4702
|
+
explainedById.set(signal.evidenceId, signal);
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
for (const [evidenceId, signal] of explainedById) {
|
|
4706
|
+
const commit = signal.explainedByCommit;
|
|
4707
|
+
hypotheses.push({
|
|
4708
|
+
kind: "intentional-change",
|
|
4709
|
+
confidence: 0.7,
|
|
4710
|
+
rationale: `explained by commit ${commit.sha}: ${commit.message}`,
|
|
4711
|
+
evidenceIds: [evidenceId]
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
const unexplained = evidence.filter((item) => !explainedById.has(item.id));
|
|
4715
|
+
const regressionEvidence = unexplained.filter(
|
|
4716
|
+
(item) => item.lane === "network" || item.lane === "db" || item.lane === "flow"
|
|
4717
|
+
);
|
|
4718
|
+
if (regressionEvidence.length > 0) {
|
|
4719
|
+
const confidence = Math.min(0.9, 0.4 + 0.1 * regressionEvidence.length);
|
|
4720
|
+
hypotheses.push({
|
|
4721
|
+
kind: "regression",
|
|
4722
|
+
confidence,
|
|
4723
|
+
rationale: `${regressionEvidence.length} behavior change(s) vs baseline with no matching intentional commit`,
|
|
4724
|
+
evidenceIds: regressionEvidence.map((item) => item.id)
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
const envEvidence = unexplained.filter((item) => item.lane === "env");
|
|
4728
|
+
if (envEvidence.length > 0) {
|
|
4729
|
+
hypotheses.push({
|
|
4730
|
+
kind: "environment",
|
|
4731
|
+
confidence: 0.5,
|
|
4732
|
+
rationale: "environment/config differs",
|
|
4733
|
+
evidenceIds: envEvidence.map((item) => item.id)
|
|
4734
|
+
});
|
|
4735
|
+
}
|
|
4736
|
+
const browserEvidence = unexplained.filter((item) => item.lane === "browser");
|
|
4737
|
+
if (browserEvidence.length > 0) {
|
|
4738
|
+
hypotheses.push({
|
|
4739
|
+
kind: "client-side",
|
|
4740
|
+
confidence: 0.5,
|
|
4741
|
+
rationale: "client-side factor (browser/network/device)",
|
|
4742
|
+
evidenceIds: browserEvidence.map((item) => item.id)
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
if (evidence.length === 0 && symptom.title.trim().length > 0) {
|
|
4746
|
+
hypotheses.push({
|
|
4747
|
+
kind: "latent",
|
|
4748
|
+
confidence: 0.3,
|
|
4749
|
+
rationale: "no behavior change captured; likely a long-standing/latent issue or missing instrumentation",
|
|
4750
|
+
evidenceIds: []
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
if (hypotheses.length === 0) {
|
|
4754
|
+
hypotheses.push({
|
|
4755
|
+
kind: "inconclusive",
|
|
4756
|
+
confidence: 0.2,
|
|
4757
|
+
rationale: "insufficient evidence to distinguish causes",
|
|
4758
|
+
evidenceIds: []
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
return hypotheses.map((hypothesis, index) => ({ hypothesis, index })).sort((a, b) => {
|
|
4762
|
+
if (b.hypothesis.confidence !== a.hypothesis.confidence) {
|
|
4763
|
+
return b.hypothesis.confidence - a.hypothesis.confidence;
|
|
4764
|
+
}
|
|
4765
|
+
return a.index - b.index;
|
|
4766
|
+
}).map((entry) => entry.hypothesis);
|
|
4767
|
+
}
|
|
4768
|
+
var INFORMATIVE_LANES = ["network", "db", "browser", "flow"];
|
|
4769
|
+
function slugify(title) {
|
|
4770
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4771
|
+
}
|
|
4772
|
+
function signatureFor(symptom) {
|
|
4773
|
+
if (symptom.errorSig && symptom.errorSig.length > 0) return symptom.errorSig;
|
|
4774
|
+
const slug = slugify(symptom.title ?? "");
|
|
4775
|
+
return slug.length > 0 ? slug : "unknown";
|
|
4776
|
+
}
|
|
4777
|
+
function suggestCaptureDirectives(symptom, evidence, gaps) {
|
|
4778
|
+
const thin = evidence.length === 0 || gaps.length > 0;
|
|
4779
|
+
if (!thin) return [];
|
|
4780
|
+
const present = new Set(evidence.map((e) => e.lane));
|
|
4781
|
+
const missing = INFORMATIVE_LANES.filter((lane) => !present.has(lane));
|
|
4782
|
+
if (missing.length === 0) return [];
|
|
4783
|
+
return [
|
|
4784
|
+
{
|
|
4785
|
+
signature: signatureFor(symptom),
|
|
4786
|
+
raise: missing,
|
|
4787
|
+
scope: "signature",
|
|
4788
|
+
reason: `thin evidence for this signature; raise capture on: ${missing.join(", ")}`
|
|
4789
|
+
}
|
|
4790
|
+
];
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
// src/evidence-source.ts
|
|
4794
|
+
var EVIDENCE_SOURCE_SCHEMA_VERSION = "evidence-source.v1";
|
|
4795
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4796
|
+
0 && (module.exports = {
|
|
4797
|
+
BROWSER_REDACTION_POLICY,
|
|
4798
|
+
CRUMBTRAIL_EVENT_KINDS,
|
|
4799
|
+
CRUMBTRAIL_REQUEST_HEADER,
|
|
4800
|
+
CRUMBTRAIL_REQUEST_HEADER_LOWER,
|
|
4801
|
+
CRUMBTRAIL_REQUEST_ID_MAX_LENGTH,
|
|
4802
|
+
CRUMBTRAIL_SCHEMA_VERSION,
|
|
4803
|
+
CRUMBTRAIL_SESSION_HEADER,
|
|
4804
|
+
CRUMBTRAIL_SESSION_HEADER_LOWER,
|
|
4805
|
+
Crumbtrail,
|
|
4806
|
+
DB_DIFF_BULK_EVENT_KIND,
|
|
4807
|
+
DB_DIFF_EVENT_KIND,
|
|
4808
|
+
DB_READ_BULK_EVENT_KIND,
|
|
4809
|
+
DB_READ_EVENT_KIND,
|
|
4810
|
+
DEFAULT_CONFIG,
|
|
4811
|
+
DEFAULT_SESSION_STORAGE_KEY,
|
|
4812
|
+
EVIDENCE_SCHEMA_VERSION,
|
|
4813
|
+
EVIDENCE_SOURCE_SCHEMA_VERSION,
|
|
4814
|
+
EventBus,
|
|
4815
|
+
FUSION_SCHEMA_VERSION,
|
|
4816
|
+
HttpTransport,
|
|
4817
|
+
PRESET_FULL,
|
|
4818
|
+
PRESET_LIGHT,
|
|
4819
|
+
PRESET_PASSIVE,
|
|
4820
|
+
REDACTED_STORAGE_KEY,
|
|
4821
|
+
REDACTED_VALUE,
|
|
4822
|
+
RingBuffer,
|
|
4823
|
+
W3C_TRACEPARENT_HEADER,
|
|
4824
|
+
WebTargetDescriptorResolver,
|
|
4825
|
+
abandonedFlowDetector,
|
|
4826
|
+
assembleBundle,
|
|
4827
|
+
attachRedactionMetadata,
|
|
4828
|
+
buildEnvDelta,
|
|
4829
|
+
buildEnvSnapshot,
|
|
4830
|
+
canInjectCorrelationHeaders,
|
|
4831
|
+
computeElementPath,
|
|
4832
|
+
computeElementSignature,
|
|
4833
|
+
createAutoFlagController,
|
|
4834
|
+
createCrumbtrailRequestHeaders,
|
|
4835
|
+
createWebSessionStore,
|
|
4836
|
+
environmentCollector,
|
|
4837
|
+
errorDetector,
|
|
4838
|
+
errorSignature,
|
|
4839
|
+
formatTraceparent,
|
|
4840
|
+
generateRequestId,
|
|
4841
|
+
generateSpanId,
|
|
4842
|
+
generateTraceContext,
|
|
4843
|
+
generateTraceId,
|
|
4844
|
+
hashString,
|
|
4845
|
+
inferIntent,
|
|
4846
|
+
mergeRedactionMetadata,
|
|
4847
|
+
parseTraceparent,
|
|
4848
|
+
rageClickDetector,
|
|
4849
|
+
redactCookieMap,
|
|
4850
|
+
redactCookieName,
|
|
4851
|
+
redactCookieValue,
|
|
4852
|
+
redactHeaders,
|
|
4853
|
+
redactInputValue,
|
|
4854
|
+
redactNetworkTextBody,
|
|
4855
|
+
redactStorageKey,
|
|
4856
|
+
redactStoredValue,
|
|
4857
|
+
redactTokenLikeString,
|
|
4858
|
+
redactUrl,
|
|
4859
|
+
redactUrlsInText,
|
|
4860
|
+
redactValue,
|
|
4861
|
+
resolveOutboundCorrelation,
|
|
4862
|
+
retryStormDetector,
|
|
4863
|
+
slowResponseDetector,
|
|
4864
|
+
summarizeBinaryPayload,
|
|
4865
|
+
summarizeOmittedPayload,
|
|
4866
|
+
webTargetDescriptorResolver
|
|
4867
|
+
});
|