@maple-dev/browser 0.4.0 → 0.9.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 +1 -1
- package/README.md +81 -2
- package/dist/index.d.mts +73 -20
- package/dist/index.mjs +307 -57
- package/dist/{replay-session-B_ACRogX.mjs → replay-session-gSlAYwqV.mjs} +81 -111
- package/dist/{sink-DHTfvFgl.mjs → sink-DHPiMgU0.mjs} +776 -134
- package/package.json +26 -10
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as warnDropped, E as activeTraceId, M as scrubUrl, O as withStartedTraceId, T as safeEmit, b as postSessionBlob, m as nextChunkSeq, n as getObservedTraceIds, o as startSessionLifecycle, p as markActivity, r as publishSessionSink, u as startEventSink, w as BLOCK_SELECTOR, x as postSessionMeta, y as gzip } from "./sink-DHPiMgU0.mjs";
|
|
2
2
|
import { record } from "rrweb";
|
|
3
|
-
//#region ../browser-session/src/replay/capture/shared.ts
|
|
4
|
-
/** Emit best-effort: capture must never throw into the host app's call site. */
|
|
5
|
-
function safeEmit(emit, ev) {
|
|
6
|
-
try {
|
|
7
|
-
emit(ev);
|
|
8
|
-
} catch {}
|
|
9
|
-
}
|
|
10
|
-
//#endregion
|
|
11
3
|
//#region ../browser-session/src/replay/capture/console.ts
|
|
12
4
|
const LEVELS = [
|
|
13
5
|
"log",
|
|
@@ -17,6 +9,8 @@ const LEVELS = [
|
|
|
17
9
|
"debug"
|
|
18
10
|
];
|
|
19
11
|
const MAX_MESSAGE = 2e3;
|
|
12
|
+
/** No array logged into a 2,000-character message needs more elements than this. */
|
|
13
|
+
const MAX_ARRAY_ELEMENTS = 100;
|
|
20
14
|
/**
|
|
21
15
|
* Capture `console.*` calls as session events. Wraps each method, emits a
|
|
22
16
|
* distilled record, then forwards to the original so the host app's console
|
|
@@ -48,90 +42,28 @@ function formatArgs(args) {
|
|
|
48
42
|
if (typeof a === "string") return a;
|
|
49
43
|
if (a instanceof Error) return `${a.name}: ${a.message}`;
|
|
50
44
|
try {
|
|
51
|
-
return
|
|
45
|
+
return boundedStringify(a, MAX_MESSAGE) ?? String(a);
|
|
52
46
|
} catch {
|
|
53
47
|
return String(a);
|
|
54
48
|
}
|
|
55
49
|
}).join(" ");
|
|
56
50
|
return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}…` : text;
|
|
57
51
|
}
|
|
58
|
-
//#endregion
|
|
59
|
-
//#region ../browser-session/src/replay/capture/errors.ts
|
|
60
|
-
const MAX_STACK = 4e3;
|
|
61
|
-
/** Capture uncaught errors + unhandled promise rejections as session events. */
|
|
62
|
-
function installErrorCapture(emit) {
|
|
63
|
-
const onError = (event) => {
|
|
64
|
-
safeEmit(emit, {
|
|
65
|
-
type: "error",
|
|
66
|
-
level: "error",
|
|
67
|
-
message: event.message || String(event.error ?? "Error"),
|
|
68
|
-
errorStack: truncate(event.error?.stack),
|
|
69
|
-
traceId: activeTraceId()
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
const onRejection = (event) => {
|
|
73
|
-
const reason = event.reason;
|
|
74
|
-
safeEmit(emit, {
|
|
75
|
-
type: "error",
|
|
76
|
-
level: "error",
|
|
77
|
-
message: typeof reason === "string" ? reason : reason?.message ?? "Unhandled promise rejection",
|
|
78
|
-
errorStack: truncate(typeof reason === "object" ? reason?.stack : void 0),
|
|
79
|
-
traceId: activeTraceId()
|
|
80
|
-
});
|
|
81
|
-
};
|
|
82
|
-
window.addEventListener("error", onError);
|
|
83
|
-
window.addEventListener("unhandledrejection", onRejection);
|
|
84
|
-
return () => {
|
|
85
|
-
window.removeEventListener("error", onError);
|
|
86
|
-
window.removeEventListener("unhandledrejection", onRejection);
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function truncate(stack) {
|
|
90
|
-
if (!stack) return void 0;
|
|
91
|
-
return stack.length > MAX_STACK ? `${stack.slice(0, MAX_STACK)}…` : stack;
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region ../browser-session/src/replay/capture/interactions.ts
|
|
95
|
-
const MAX_TEXT = 120;
|
|
96
52
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
53
|
+
* `JSON.stringify` that stops descending once roughly `budget` characters have
|
|
54
|
+
* been produced. The message is cut to `MAX_MESSAGE` anyway, and serializing a
|
|
55
|
+
* whole store object on every `console.log` first was a main-thread cost paid
|
|
56
|
+
* on the host app's hot path. A replacer returning `undefined` skips a value
|
|
57
|
+
* without visiting its children, so the walk is bounded by the budget.
|
|
101
58
|
*/
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
const onInput = (event) => {
|
|
113
|
-
const target = event.target;
|
|
114
|
-
if (!(target instanceof Element)) return;
|
|
115
|
-
safeEmit(emit, {
|
|
116
|
-
type: "input",
|
|
117
|
-
targetSelector: selectorOf(target)
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
document.addEventListener("click", onClick, true);
|
|
121
|
-
document.addEventListener("input", onInput, true);
|
|
122
|
-
return () => {
|
|
123
|
-
document.removeEventListener("click", onClick, true);
|
|
124
|
-
document.removeEventListener("input", onInput, true);
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/** A short, human-readable selector: tag + #id + .first-class. */
|
|
128
|
-
function selectorOf(el) {
|
|
129
|
-
return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ""}${typeof el.className === "string" && el.className.trim() ? `.${el.className.trim().split(/\s+/)[0]}` : ""}`;
|
|
130
|
-
}
|
|
131
|
-
function textOf(el) {
|
|
132
|
-
const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
|
|
133
|
-
if (!text) return void 0;
|
|
134
|
-
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
|
|
59
|
+
function boundedStringify(value, budget) {
|
|
60
|
+
let used = 0;
|
|
61
|
+
return JSON.stringify(value, (key, nested) => {
|
|
62
|
+
if (used > budget) return void 0;
|
|
63
|
+
used += key.length + (typeof nested === "string" ? nested.length : 4);
|
|
64
|
+
if (Array.isArray(nested) && nested.length > MAX_ARRAY_ELEMENTS) return nested.slice(0, MAX_ARRAY_ELEMENTS);
|
|
65
|
+
return nested;
|
|
66
|
+
});
|
|
135
67
|
}
|
|
136
68
|
//#endregion
|
|
137
69
|
//#region ../browser-session/src/replay/capture/network.ts
|
|
@@ -145,10 +77,13 @@ function installNetworkCapture(emit, ignoreUrl) {
|
|
|
145
77
|
if (origFetch) window.fetch = async (input, init) => {
|
|
146
78
|
const url = requestUrl(input);
|
|
147
79
|
const method = requestMethod(input, init);
|
|
148
|
-
const
|
|
80
|
+
const ambientTraceId = activeTraceId();
|
|
149
81
|
const start = performance.now();
|
|
82
|
+
let traceId = ambientTraceId;
|
|
150
83
|
try {
|
|
151
|
-
const
|
|
84
|
+
const call = withStartedTraceId(() => origFetch(input, init));
|
|
85
|
+
traceId = call.traceId ?? ambientTraceId;
|
|
86
|
+
const res = await call.result;
|
|
152
87
|
record(url, method, res.status, start, traceId);
|
|
153
88
|
return res;
|
|
154
89
|
} catch (error) {
|
|
@@ -167,7 +102,7 @@ function installNetworkCapture(emit, ignoreUrl) {
|
|
|
167
102
|
durationMs: Math.round(performance.now() - start)
|
|
168
103
|
},
|
|
169
104
|
traceId,
|
|
170
|
-
...error ? { attrs: { error } } :
|
|
105
|
+
...error ? { attrs: { error } } : void 0
|
|
171
106
|
});
|
|
172
107
|
};
|
|
173
108
|
const XHR = typeof window !== "undefined" ? window.XMLHttpRequest : void 0;
|
|
@@ -186,11 +121,13 @@ function installNetworkCapture(emit, ignoreUrl) {
|
|
|
186
121
|
XHR.prototype.send = function(...args) {
|
|
187
122
|
const meta = this;
|
|
188
123
|
const start = performance.now();
|
|
189
|
-
|
|
124
|
+
let traceId = activeTraceId();
|
|
190
125
|
this.addEventListener("loadend", () => {
|
|
191
126
|
record(meta.__mapleUrl ?? "", meta.__mapleMethod ?? "GET", this.status, start, traceId);
|
|
192
127
|
});
|
|
193
|
-
|
|
128
|
+
const call = withStartedTraceId(() => origSend.apply(this, args));
|
|
129
|
+
traceId = call.traceId ?? traceId;
|
|
130
|
+
return call.result;
|
|
194
131
|
};
|
|
195
132
|
}
|
|
196
133
|
return () => {
|
|
@@ -210,22 +147,18 @@ function requestMethod(input, init) {
|
|
|
210
147
|
//#endregion
|
|
211
148
|
//#region ../browser-session/src/replay/events.ts
|
|
212
149
|
/**
|
|
213
|
-
* Install the capture modules that turn
|
|
214
|
-
*
|
|
150
|
+
* Install the capture modules that turn console output and network requests
|
|
151
|
+
* into distilled session events.
|
|
215
152
|
*
|
|
216
|
-
* Navigation
|
|
217
|
-
*
|
|
218
|
-
*
|
|
153
|
+
* Navigation, errors and interactions are deliberately absent: the sink
|
|
154
|
+
* installs those itself (see `startBaselineCapture`), because page views, error
|
|
155
|
+
* counts and click counts have to be counted even when replay is unsampled —
|
|
156
|
+
* and a second listener here would double-count every one of them.
|
|
219
157
|
*/
|
|
220
158
|
function startEventCapture(config, sessionId) {
|
|
221
159
|
const sink = startEventSink(config, sessionId);
|
|
222
160
|
const emit = sink.emit;
|
|
223
|
-
const uninstall = [
|
|
224
|
-
installInteractionCapture(emit, config.maskAllText),
|
|
225
|
-
installConsoleCapture(emit),
|
|
226
|
-
installNetworkCapture(emit, sink.ignoreUrl),
|
|
227
|
-
installErrorCapture(emit)
|
|
228
|
-
];
|
|
161
|
+
const uninstall = [installConsoleCapture(emit), installNetworkCapture(emit, sink.ignoreUrl)];
|
|
229
162
|
return {
|
|
230
163
|
stop: () => {
|
|
231
164
|
for (const off of uninstall) off();
|
|
@@ -236,6 +169,7 @@ function startEventCapture(config, sessionId) {
|
|
|
236
169
|
//#endregion
|
|
237
170
|
//#region ../browser-session/src/replay/record.ts
|
|
238
171
|
const FULL_SNAPSHOT = 2;
|
|
172
|
+
const META = 4;
|
|
239
173
|
const INCREMENTAL = 3;
|
|
240
174
|
const SOURCE_MOUSE_INTERACTION = 2;
|
|
241
175
|
const MOUSE_CLICK = 2;
|
|
@@ -243,6 +177,9 @@ const FLUSH_INTERVAL_MS = 5e3;
|
|
|
243
177
|
const FLUSH_BYTES = 102400;
|
|
244
178
|
const CHECKOUT_EVERY_MS = 3e5;
|
|
245
179
|
const MAX_BUFFER_BYTES = 4194304;
|
|
180
|
+
function warnExhausted(sessionId) {
|
|
181
|
+
console.warn(`[maple] session replay ${sessionId} reached its maximum recorded size; recording stopped for this session (metadata and events continue)`);
|
|
182
|
+
}
|
|
246
183
|
let lastDropWarnAt = 0;
|
|
247
184
|
function warnBufferDropped(bytes) {
|
|
248
185
|
const now = Date.now();
|
|
@@ -258,6 +195,8 @@ function startRecording(config, sessionId) {
|
|
|
258
195
|
let lastTimestamp = 0;
|
|
259
196
|
let droppedChunk = false;
|
|
260
197
|
let clickCount = 0;
|
|
198
|
+
let stopped = false;
|
|
199
|
+
let exhausted = false;
|
|
261
200
|
const resetBuffer = () => {
|
|
262
201
|
parts = [];
|
|
263
202
|
bufferBytes = 0;
|
|
@@ -266,27 +205,39 @@ function startRecording(config, sessionId) {
|
|
|
266
205
|
lastTimestamp = 0;
|
|
267
206
|
};
|
|
268
207
|
const flush = async (keepalive = false) => {
|
|
269
|
-
if (parts.length === 0) return;
|
|
208
|
+
if (stopped || exhausted || parts.length === 0) return;
|
|
270
209
|
const body = `[${parts.join(",")}]`;
|
|
271
210
|
const isCheckpoint = bufferHasCheckpoint;
|
|
272
211
|
const eventCount = parts.length;
|
|
273
212
|
const durationMs = Math.max(0, lastTimestamp - firstTimestamp);
|
|
274
213
|
const seq = nextChunkSeq();
|
|
275
214
|
resetBuffer();
|
|
276
|
-
|
|
277
|
-
|
|
215
|
+
let gzipped;
|
|
216
|
+
try {
|
|
217
|
+
gzipped = await gzip(new TextEncoder().encode(body));
|
|
218
|
+
} catch (error) {
|
|
219
|
+
warnDropped("chunk compression", error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (await postSessionBlob(config, {
|
|
278
223
|
sessionId,
|
|
279
224
|
chunkSeq: seq,
|
|
280
225
|
isCheckpoint,
|
|
281
226
|
eventCount,
|
|
282
227
|
durationMs
|
|
283
|
-
}, gzipped, keepalive)
|
|
228
|
+
}, gzipped, keepalive) === "exhausted" && !exhausted) {
|
|
229
|
+
exhausted = true;
|
|
230
|
+
warnExhausted(sessionId);
|
|
231
|
+
haltCapture();
|
|
232
|
+
}
|
|
284
233
|
};
|
|
234
|
+
let haltCapture = () => {};
|
|
285
235
|
const stop = record({
|
|
286
236
|
emit: (event, isCheckpoint) => {
|
|
287
237
|
const active = markActivity();
|
|
288
238
|
if (active && active.id !== sessionId) return;
|
|
289
239
|
const e = event;
|
|
240
|
+
if (e.type === META && e.data && typeof e.data.href === "string") e.data.href = scrubUrl(e.data.href);
|
|
290
241
|
const isFullSnapshot = isCheckpoint === true || e.type === FULL_SNAPSHOT;
|
|
291
242
|
if (e.type === INCREMENTAL && e.data?.source === SOURCE_MOUSE_INTERACTION && e.data.type === MOUSE_CLICK) clickCount++;
|
|
292
243
|
let json;
|
|
@@ -311,25 +262,42 @@ function startRecording(config, sessionId) {
|
|
|
311
262
|
if (bufferBytes >= FLUSH_BYTES) flush();
|
|
312
263
|
},
|
|
313
264
|
maskAllInputs: config.maskAllInputs,
|
|
314
|
-
|
|
265
|
+
blockSelector: BLOCK_SELECTOR,
|
|
266
|
+
...config.maskAllText ? { maskTextSelector: "*" } : void 0,
|
|
315
267
|
checkoutEveryNms: CHECKOUT_EVERY_MS
|
|
316
268
|
});
|
|
269
|
+
let idleHandle;
|
|
317
270
|
const scheduleFlush = () => {
|
|
318
|
-
if (typeof requestIdleCallback === "function") requestIdleCallback(() =>
|
|
271
|
+
if (typeof requestIdleCallback === "function") idleHandle = requestIdleCallback(() => {
|
|
272
|
+
idleHandle = void 0;
|
|
273
|
+
flush();
|
|
274
|
+
}, { timeout: 2e3 });
|
|
319
275
|
else flush();
|
|
320
276
|
};
|
|
321
277
|
const flushTimer = setInterval(scheduleFlush, FLUSH_INTERVAL_MS);
|
|
278
|
+
let halted = false;
|
|
279
|
+
haltCapture = () => {
|
|
280
|
+
if (halted) return;
|
|
281
|
+
halted = true;
|
|
282
|
+
clearInterval(flushTimer);
|
|
283
|
+
if (idleHandle !== void 0 && typeof cancelIdleCallback === "function") {
|
|
284
|
+
cancelIdleCallback(idleHandle);
|
|
285
|
+
idleHandle = void 0;
|
|
286
|
+
}
|
|
287
|
+
resetBuffer();
|
|
288
|
+
stop?.();
|
|
289
|
+
};
|
|
322
290
|
return {
|
|
323
291
|
stop: () => {
|
|
324
|
-
|
|
325
|
-
|
|
292
|
+
stopped = true;
|
|
293
|
+
haltCapture();
|
|
326
294
|
},
|
|
327
295
|
flush,
|
|
328
296
|
getClickCount: () => clickCount
|
|
329
297
|
};
|
|
330
298
|
}
|
|
331
299
|
//#endregion
|
|
332
|
-
//#region ../browser-session/src/replay-session.ts
|
|
300
|
+
//#region ../browser-session/src/session/replay-session.ts
|
|
333
301
|
/**
|
|
334
302
|
* Start recording the current browser session. Publishes the session sink,
|
|
335
303
|
* posts an `active` metadata row, and installs visibility handlers:
|
|
@@ -345,8 +313,10 @@ function startReplaySession(options) {
|
|
|
345
313
|
const engineConfig = {
|
|
346
314
|
endpoint: options.endpoint.replace(/\/$/, ""),
|
|
347
315
|
ingestKey: options.ingestKey,
|
|
316
|
+
sdk: options.sdk,
|
|
348
317
|
maskAllInputs: options.maskAllInputs,
|
|
349
|
-
maskAllText: options.maskAllText
|
|
318
|
+
maskAllText: options.maskAllText,
|
|
319
|
+
getIdentity: options.getIdentity
|
|
350
320
|
};
|
|
351
321
|
let recorder;
|
|
352
322
|
let events;
|