@brett_lamy/docstream 1.24.1 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/DocsRenderer.js +1 -1
- package/dist/docs/DocsRenderer.js.map +1 -1
- package/dist/gitbook/ast.d.ts +4 -0
- package/dist/gitbook/ast.d.ts.map +1 -1
- package/dist/gitbook/ast.js.map +1 -1
- package/dist/gitbook/parse.js +2 -0
- package/dist/gitbook/parse.js.map +1 -1
- package/dist/gitbook/serialize.d.ts.map +1 -1
- package/dist/gitbook/serialize.js +2 -0
- package/dist/gitbook/serialize.js.map +1 -1
- package/dist/replay/ReplayFile.d.ts +6 -2
- package/dist/replay/ReplayFile.d.ts.map +1 -1
- package/dist/replay/ReplayFile.js +20 -3
- package/dist/replay/ReplayFile.js.map +1 -1
- package/dist/replay/ReplayPlayer.d.ts +6 -1
- package/dist/replay/ReplayPlayer.d.ts.map +1 -1
- package/dist/replay/ReplayPlayer.js +4 -1
- package/dist/replay/ReplayPlayer.js.map +1 -1
- package/dist/replay/index.d.ts +1 -0
- package/dist/replay/index.d.ts.map +1 -1
- package/dist/replay/index.js +1 -0
- package/dist/replay/index.js.map +1 -1
- package/dist/replay/markers.d.ts.map +1 -1
- package/dist/replay/markers.js +13 -3
- package/dist/replay/markers.js.map +1 -1
- package/dist/replay/sources.d.ts +11 -0
- package/dist/replay/sources.d.ts.map +1 -0
- package/dist/replay/sources.js +126 -0
- package/dist/replay/sources.js.map +1 -0
- package/package.json +1 -1
- package/src/docs/DocsRenderer.tsx +1 -1
- package/src/gitbook/ast.ts +4 -0
- package/src/gitbook/parse.ts +2 -0
- package/src/gitbook/serialize.ts +2 -0
- package/src/replay/ReplayFile.tsx +35 -4
- package/src/replay/ReplayPlayer.tsx +9 -1
- package/src/replay/index.ts +1 -0
- package/src/replay/markers.ts +10 -3
- package/src/replay/sources.ts +120 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* More sources for a recording's DevTools, beside the rrweb events: a HAR (the network log browsers export) and a
|
|
2
|
+
console log (one message per line, `2026-06-12T21:01:38.123Z [warn] text`; indented lines continue the message
|
|
3
|
+
above). Both are read into the custom events devtools.ts already understands, stamped on their own absolute
|
|
4
|
+
times, so they line up with the recording when they share its clock. Anything stamped before the recording
|
|
5
|
+
starts or after it ends lands on its first or last moment. Pure readers, no rrweb import.
|
|
6
|
+
|
|
7
|
+
A HAR can also carry inspections (inspections.ts) as an extension: `log._inspections`, each
|
|
8
|
+
`{ at: ISO time, request?: entry _requestId or URL, message?: text, tab?, label? }`. */
|
|
9
|
+
import { ReplayEventType } from './types.js';
|
|
10
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
11
|
+
const str = (v) => (typeof v === 'string' && v !== '' ? v : undefined);
|
|
12
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
13
|
+
const time = (v) => {
|
|
14
|
+
const t = typeof v === 'string' ? Date.parse(v) : num(v);
|
|
15
|
+
return t !== undefined && Number.isFinite(t) ? t : undefined;
|
|
16
|
+
};
|
|
17
|
+
const custom = (timestamp, tag, payload) => ({
|
|
18
|
+
type: ReplayEventType.Custom,
|
|
19
|
+
timestamp,
|
|
20
|
+
data: { tag, payload },
|
|
21
|
+
});
|
|
22
|
+
/** HAR headers (`[{ name, value }]`) as a record. */
|
|
23
|
+
function harHeaders(list) {
|
|
24
|
+
if (!Array.isArray(list))
|
|
25
|
+
return undefined;
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const h of list) {
|
|
28
|
+
const name = str(obj(h).name);
|
|
29
|
+
if (name)
|
|
30
|
+
out[name] = String(obj(h).value ?? '');
|
|
31
|
+
}
|
|
32
|
+
return Object.keys(out).length ? out : undefined;
|
|
33
|
+
}
|
|
34
|
+
/** A HAR body: as written, or decoded when base64 and textual. */
|
|
35
|
+
function harText(content) {
|
|
36
|
+
const text = typeof content.text === 'string' ? content.text : undefined;
|
|
37
|
+
if (text === undefined || content.encoding !== 'base64')
|
|
38
|
+
return text;
|
|
39
|
+
try {
|
|
40
|
+
const bytes = Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
41
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
42
|
+
return decoded;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return `[${Math.round((text.length * 3) / 4)} bytes of binary data]`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** A HAR (object or JSON text) as `network` (and `inspect`) custom events. */
|
|
49
|
+
export function harToReplayEvents(har) {
|
|
50
|
+
let data = har;
|
|
51
|
+
if (typeof har === 'string') {
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(har);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const log = obj(obj(data).log);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const raw of Array.isArray(log.entries) ? log.entries : []) {
|
|
62
|
+
const e = obj(raw);
|
|
63
|
+
const started = time(e.startedDateTime);
|
|
64
|
+
if (started === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
const request = obj(e.request);
|
|
67
|
+
const response = obj(e.response);
|
|
68
|
+
const duration = Math.max(0, num(e.time) ?? 0);
|
|
69
|
+
const status = num(response.status);
|
|
70
|
+
const error = str(e._error) ?? str(response._error);
|
|
71
|
+
out.push(custom(started + duration, error ? 'network.error' : 'network', {
|
|
72
|
+
requestId: str(e._requestId),
|
|
73
|
+
method: str(request.method) ?? 'GET',
|
|
74
|
+
url: str(request.url) ?? '',
|
|
75
|
+
status: status === -1 ? undefined : status,
|
|
76
|
+
statusText: str(response.statusText),
|
|
77
|
+
error,
|
|
78
|
+
duration,
|
|
79
|
+
type: str(e._resourceType) ?? 'fetch',
|
|
80
|
+
requestHeaders: harHeaders(request.headers),
|
|
81
|
+
responseHeaders: harHeaders(response.headers),
|
|
82
|
+
requestBody: str(obj(request.postData).text),
|
|
83
|
+
responseBody: harText(obj(response.content)),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
for (const raw of Array.isArray(log._inspections) ? log._inspections : []) {
|
|
87
|
+
const i = obj(raw);
|
|
88
|
+
const at = time(i.at);
|
|
89
|
+
if (at === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
out.push(custom(at, 'inspect', { request: str(i.request), message: str(i.message), tab: str(i.tab), label: str(i.label), id: str(i.id) }));
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
const LINE = /^(\d{4}-\d\d-\d\dT[\d:.]+(?:Z|[+-]\d\d:?\d\d))\s+\[(\w+)\]\s?(.*)$/;
|
|
96
|
+
/** A console log (`<ISO time> [level] message`, indented continuation lines) as `console` custom events. */
|
|
97
|
+
export function consoleLogToReplayEvents(text) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const line of text.split(/\r?\n/)) {
|
|
100
|
+
const m = line.match(LINE);
|
|
101
|
+
const at = m ? time(m[1]) : undefined;
|
|
102
|
+
if (m && at !== undefined)
|
|
103
|
+
out.push({ at, level: m[2].toLowerCase(), lines: [m[3] ?? ''] });
|
|
104
|
+
else if (out.length && /^\s/.test(line))
|
|
105
|
+
out[out.length - 1].lines.push(line.replace(/^ {2}/, ''));
|
|
106
|
+
}
|
|
107
|
+
return out.map((m) => custom(m.at, 'console', { level: m.level === 'warning' ? 'warn' : m.level, message: m.lines.join('\n').trimEnd() }));
|
|
108
|
+
}
|
|
109
|
+
/** The recording's events with the HAR's and the console log's added, each stamped inside the recording. */
|
|
110
|
+
export function mergeReplaySources(events, sources) {
|
|
111
|
+
const extra = [
|
|
112
|
+
...(sources.har ? harToReplayEvents(sources.har) : []),
|
|
113
|
+
...(sources.consoleLog ? consoleLogToReplayEvents(sources.consoleLog) : []),
|
|
114
|
+
];
|
|
115
|
+
if (!extra.length || !events.length)
|
|
116
|
+
return events;
|
|
117
|
+
let first = Infinity;
|
|
118
|
+
let last = -Infinity;
|
|
119
|
+
for (const e of events) {
|
|
120
|
+
first = Math.min(first, e.timestamp);
|
|
121
|
+
last = Math.max(last, e.timestamp);
|
|
122
|
+
}
|
|
123
|
+
const clamped = extra.map((e) => (e.timestamp < first || e.timestamp > last ? { ...e, timestamp: Math.min(last, Math.max(first, e.timestamp)) } : e));
|
|
124
|
+
return [...events, ...clamped].sort((a, b) => a.timestamp - b.timestamp);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/replay/sources.ts"],"names":[],"mappings":"AAAA;;;;;;;0FAO0F;AAC1F,OAAO,EAAE,eAAe,EAAoB,MAAM,SAAS,CAAA;AAG3D,MAAM,GAAG,GAAG,CAAC,CAAU,EAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AACjF,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AACnG,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAC7G,MAAM,IAAI,GAAG,CAAC,CAAU,EAAsB,EAAE;IAC9C,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACxD,OAAO,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9D,CAAC,CAAA;AAED,MAAM,MAAM,GAAG,CAAC,SAAiB,EAAE,GAAW,EAAE,OAAa,EAAe,EAAE,CAAC,CAAC;IAC9E,IAAI,EAAE,eAAe,CAAC,MAAM;IAC5B,SAAS;IACT,IAAI,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;CACvB,CAAC,CAAA;AAEF,qDAAqD;AACrD,SAAS,UAAU,CAAC,IAAa;IAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAC1C,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;IAClD,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC;AAED,kEAAkE;AAClE,SAAS,OAAO,CAAC,OAAa;IAC5B,MAAM,IAAI,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IACxE,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACpE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;QACjE,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACvE,OAAO,OAAO,CAAA;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,wBAAwB,CAAA;IACtE,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,IAAI,IAAI,GAAY,GAAG,CAAA;IACvB,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC;YAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,EAAE,CAAA;QAAC,CAAC;IACpD,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,MAAM,GAAG,GAAkB,EAAE,CAAA;IAC7B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAChE,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;QACvC,IAAI,OAAO,KAAK,SAAS;YAAE,SAAQ;QACnC,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QAC9B,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;QACnC,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;QACnD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,EAAE;YACvE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC;YAC5B,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK;YACpC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE;YAC3B,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;YAC1C,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC;YACpC,KAAK;YACL,QAAQ;YACR,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,OAAO;YACrC,cAAc,EAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;YAC3C,eAAe,EAAE,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC;YAC7C,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;YAC5C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SAC7C,CAAC,CAAC,CAAA;IACL,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC1E,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACrB,IAAI,EAAE,KAAK,SAAS;YAAE,SAAQ;QAC9B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC5I,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,IAAI,GAAG,oEAAoE,CAAA;AAEjF,4GAA4G;AAC5G,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,MAAM,GAAG,GAAqD,EAAE,CAAA;IAChE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC1B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACrC,IAAI,CAAC,IAAI,EAAE,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;aACvF,IAAI,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;IACrG,CAAC;IACD,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAC5I,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,kBAAkB,CAAC,MAAqB,EAAE,OAA+C;IACvG,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,wBAAwB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAA;IACD,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,MAAM,CAAA;IAClD,IAAI,KAAK,GAAG,QAAQ,CAAA;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAA;IACpB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,CAAA;QACpC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAA;IACpC,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,IAAI,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrJ,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAA;AAC1E,CAAC"}
|
package/package.json
CHANGED
|
@@ -451,7 +451,7 @@ function BlockView({ block, liveRenderer, sourceRenderer }: { block: Block } & R
|
|
|
451
451
|
return <ReplayPreview source={block.url} title="Replay preview" />
|
|
452
452
|
}
|
|
453
453
|
if (isRrwebUrl(block.url)) {
|
|
454
|
-
return <ReplayFile url={block.url} title={block.title || fileName(block.url)} />
|
|
454
|
+
return <ReplayFile url={block.url} title={block.title || fileName(block.url)} {...(block.har ? { har: block.har } : {})} {...(block.log ? { log: block.log } : {})} />
|
|
455
455
|
}
|
|
456
456
|
if (isStlUrl(block.url)) {
|
|
457
457
|
return <AssetStl url={block.url} {...(block.title ? { title: block.title } : {})} />
|
package/src/gitbook/ast.ts
CHANGED
|
@@ -302,6 +302,10 @@ export interface EmbedNode extends BlockSpacing {
|
|
|
302
302
|
muted?: boolean
|
|
303
303
|
controls?: boolean
|
|
304
304
|
poster?: string
|
|
305
|
+
/** For an `.rrweb` recording: a HAR and a console log recorded alongside, whose requests and messages join
|
|
306
|
+
* the player's DevTools. */
|
|
307
|
+
har?: string
|
|
308
|
+
log?: string
|
|
305
309
|
/** The tag as written (with its `{% endembed %}`), reused while it still says the same. */
|
|
306
310
|
raw?: string
|
|
307
311
|
}
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -634,6 +634,8 @@ function parseBody(lines: string[], firstGap = 0): { blocks: Block[]; end: numbe
|
|
|
634
634
|
url: tag.attrs.url ?? "",
|
|
635
635
|
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
636
636
|
...(tag.attrs.poster ? { poster: tag.attrs.poster } : {}),
|
|
637
|
+
...(tag.attrs.har ? { har: tag.attrs.har } : {}),
|
|
638
|
+
...(tag.attrs.log ? { log: tag.attrs.log } : {}),
|
|
637
639
|
...(booleanAttr(tag.attrs, "autoplay") === undefined
|
|
638
640
|
? {}
|
|
639
641
|
: { autoplay: booleanAttr(tag.attrs, "autoplay") }),
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -389,6 +389,8 @@ function serializeBlock(b: Block): string {
|
|
|
389
389
|
attr("url", b.url),
|
|
390
390
|
b.title ? attr("title", b.title) : "",
|
|
391
391
|
b.poster ? attr("poster", b.poster) : "",
|
|
392
|
+
b.har ? attr("har", b.har) : "",
|
|
393
|
+
b.log ? attr("log", b.log) : "",
|
|
392
394
|
b.autoplay === undefined ? "" : attr("autoplay", b.autoplay),
|
|
393
395
|
b.loop === undefined ? "" : attr("loop", b.loop),
|
|
394
396
|
b.muted === undefined ? "" : attr("muted", b.muted),
|
|
@@ -3,9 +3,28 @@ import { useAsset } from '../asset-resolver';
|
|
|
3
3
|
import { ReplayPreview } from './ReplayPreview';
|
|
4
4
|
import type { ReplayPlayerOptions } from './ReplayPlayer';
|
|
5
5
|
|
|
6
|
-
export interface ReplayFileProps extends Omit<ReplayPlayerOptions, 'url'> {
|
|
6
|
+
export interface ReplayFileProps extends Omit<ReplayPlayerOptions, 'url' | 'har' | 'consoleLog'> {
|
|
7
7
|
/** The `.rrweb` file as written in the document; the host's asset resolver maps it to a loadable URL. */
|
|
8
8
|
url: string;
|
|
9
|
+
/** A `.har` file recorded alongside (resolved the same way): its requests join DevTools' Network list. */
|
|
10
|
+
har?: string;
|
|
11
|
+
/** A console log recorded alongside (`<ISO time> [level] message` lines): its messages join the Console. */
|
|
12
|
+
log?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The text of a file the asset resolver maps, fetched once `enabled`; undefined until then or when it fails. */
|
|
16
|
+
function useAssetText(url: string | undefined, enabled: boolean): { text?: string; done: boolean } {
|
|
17
|
+
const src = useAsset(url ?? '');
|
|
18
|
+
const [state, setState] = useState<{ text?: string; done: boolean }>({ done: !url });
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!url || !enabled || src === undefined) return;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
fetch(src, { signal: controller.signal })
|
|
23
|
+
.then((r) => (r.ok ? r.text() : undefined))
|
|
24
|
+
.then((text) => setState({ text, done: true }), () => !controller.signal.aborted && setState({ done: true }));
|
|
25
|
+
return () => controller.abort();
|
|
26
|
+
}, [url, src, enabled]);
|
|
27
|
+
return url ? state : { done: true };
|
|
9
28
|
}
|
|
10
29
|
|
|
11
30
|
/**
|
|
@@ -14,7 +33,7 @@ export interface ReplayFileProps extends Omit<ReplayPlayerOptions, 'url'> {
|
|
|
14
33
|
* viewport, so a page of recordings doesn't download them all up front. A media fragment on the URL,
|
|
15
34
|
* `recording.rrweb#t=4.7,7.4` (seconds), is its `timeWindow`.
|
|
16
35
|
*/
|
|
17
|
-
export function ReplayFile({ url, ...options }: ReplayFileProps) {
|
|
36
|
+
export function ReplayFile({ url, har, log, ...options }: ReplayFileProps) {
|
|
18
37
|
const { file, segment } = splitReplayFragment(url);
|
|
19
38
|
const src = useAsset(file);
|
|
20
39
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -29,8 +48,20 @@ export function ReplayFile({ url, ...options }: ReplayFileProps) {
|
|
|
29
48
|
return () => io.disconnect();
|
|
30
49
|
}, [near]);
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
const harFile = useAssetText(har, near);
|
|
52
|
+
const logFile = useAssetText(log, near);
|
|
53
|
+
|
|
54
|
+
if (!near || src === undefined || !harFile.done || !logFile.done) return <div ref={ref} className="docstream-asset-pending" data-slot="replay-file" aria-busy="true" />;
|
|
55
|
+
return (
|
|
56
|
+
<ReplayPreview
|
|
57
|
+
source={{ kind: 'events-url', url: src }}
|
|
58
|
+
chrome="browser"
|
|
59
|
+
timeWindow={segment}
|
|
60
|
+
har={harFile.text}
|
|
61
|
+
consoleLog={logFile.text}
|
|
62
|
+
{...options}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
/** Splits `#t=start,end` (a media fragment, in seconds; `#t=start` runs to the end) off a recording's URL. */
|
|
@@ -21,6 +21,7 @@ import { ReplayStage } from './ReplayStage'
|
|
|
21
21
|
import { DevtoolsButtons, DevtoolsPanel, type ReplayDevtoolsTab } from './ReplayDevtools'
|
|
22
22
|
import { getReplayDevtools, replayDevtoolsAt } from './devtools'
|
|
23
23
|
import { getReplayInspections, replayInspectionAt, resolveReplayInspections, type ReplayInspection } from './inspections'
|
|
24
|
+
import { mergeReplaySources } from './sources'
|
|
24
25
|
import { getReplayMarkers, getReplayMeta, getReplayPointerTrack, replayPointerAt } from './markers'
|
|
25
26
|
import type { ReplayMarker, ReplayMarkerKind, ReplayPointerTrack } from './markers'
|
|
26
27
|
import type { ReplayStageHandle } from './stage-types'
|
|
@@ -84,6 +85,11 @@ export interface ReplayPlayerOptions {
|
|
|
84
85
|
* tab with that request or message selected and the label above it; playback doesn't stop. Added to the
|
|
85
86
|
* recording's own `inspect` custom events, and to the rail as checkpoints. */
|
|
86
87
|
inspections?: ReplayInspection[]
|
|
88
|
+
/** A HAR (object or JSON text) recorded alongside: its requests join the DevTools Network list, on their own
|
|
89
|
+
* timestamps (sources.ts). Its `log._inspections` join `inspections`. */
|
|
90
|
+
har?: unknown
|
|
91
|
+
/** A console log recorded alongside (`<ISO time> [level] message` lines): its messages join the Console. */
|
|
92
|
+
consoleLog?: string
|
|
87
93
|
/** Draw the recorded pointer and click ripples (default true). */
|
|
88
94
|
cursor?: boolean
|
|
89
95
|
/** `width` (default): the page scales to the frame's width and sets its height. `contain`: fill the frame's
|
|
@@ -112,11 +118,13 @@ const MARKER_NOUN: Record<ReplayMarkerKind, string> = {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
export function ReplayPlayer({
|
|
115
|
-
events, url, title = 'Session replay', chrome = 'browser', trafficLights = true, controls = true,
|
|
121
|
+
events: recordingEvents, har, consoleLog, url, title = 'Session replay', chrome = 'browser', trafficLights = true, controls = true,
|
|
116
122
|
autoplay = false, loop = false, initialTime: initialTimeProp, timeWindow, speeds = DEFAULT_PLAYER_SPEEDS, markers = true, markerKinds,
|
|
117
123
|
cursor = true, disableDevtools = false, devtools = true, defaultDevtoolsTab, inspections, fit = 'width', poster, lazy = true, playerRef, onTimeUpdate, onPlayingChange, onMarkerPress,
|
|
118
124
|
className, style,
|
|
119
125
|
}: ReplayPlayerOptions & { events: ReplayEvent[] }) {
|
|
126
|
+
// The recording plus whatever was recorded beside it (a HAR, a console log), as one event list.
|
|
127
|
+
const events = useMemo(() => mergeReplaySources(recordingEvents, { har, consoleLog }), [recordingEvents, har, consoleLog])
|
|
120
128
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
121
129
|
const stage = useRef<ReplayStageHandle>(null)
|
|
122
130
|
const reduced = useReducedMotion()
|
package/src/replay/index.ts
CHANGED
|
@@ -26,3 +26,4 @@ export { isReplayQaUrl, normalizeReplayEmbedUrl } from './url';
|
|
|
26
26
|
export { ReplayFile, isRrwebUrl, splitReplayFragment } from './ReplayFile';
|
|
27
27
|
export type { ReplayFileProps } from './ReplayFile';
|
|
28
28
|
export { sanitizeReplayEvent } from './sanitize';
|
|
29
|
+
export { consoleLogToReplayEvents, harToReplayEvents, mergeReplaySources } from './sources';
|
package/src/replay/markers.ts
CHANGED
|
@@ -91,6 +91,9 @@ export function getReplayMeta(events: readonly ReplayEvent[]): ReplayMeta {
|
|
|
91
91
|
|
|
92
92
|
const ERROR_TAGS = new Set(['error', 'exception', 'console.error', 'uncaught'])
|
|
93
93
|
const NETWORK_TAGS = new Set(['network', 'network.error', 'request.failed', 'fetch.failed'])
|
|
94
|
+
const FAILED_TAGS = new Set(['network.error', 'request.failed', 'fetch.failed'])
|
|
95
|
+
// Console messages (from a console log, say) mark the rail only when they're errors.
|
|
96
|
+
const CONSOLE_TAGS = new Set(['console', 'log', 'warn', 'warning', 'info', 'debug'])
|
|
94
97
|
const NAVIGATION_TAGS = new Set(['navigation', 'navigate', 'route'])
|
|
95
98
|
|
|
96
99
|
/**
|
|
@@ -150,9 +153,13 @@ export function getReplayMarkers(events: readonly ReplayEvent[]): ReplayMarker[]
|
|
|
150
153
|
const label = text(payload.label)
|
|
151
154
|
if (ERROR_TAGS.has(tag)) push({ kind: 'error', time, label: label ?? 'Error', detail })
|
|
152
155
|
else if (NETWORK_TAGS.has(tag)) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
// A failed request marks the rail (the same rule as DevTools: an error, a failure tag, status 0 or 400 and
|
|
157
|
+
// up); one that answered, or never did within the recording, doesn't.
|
|
158
|
+
const code = typeof payload.status === 'number' ? payload.status : undefined
|
|
159
|
+
const failed = FAILED_TAGS.has(tag) || !!payload.error || code === 0 || (code !== undefined && code >= 400)
|
|
160
|
+
if (failed) push({ kind: 'network', time, label: label ?? `${text(payload.method) ?? 'GET'} ${code ?? 'failed'}`, detail })
|
|
161
|
+
} else if (CONSOLE_TAGS.has(tag)) {
|
|
162
|
+
if (payload.level === 'error' || payload.level === 'assert') push({ kind: 'error', time, label: label ?? 'Console error', detail })
|
|
156
163
|
} else if (NAVIGATION_TAGS.has(tag)) push({ kind: 'navigation', time, label: label ?? 'Navigated', detail })
|
|
157
164
|
else push({ kind: 'custom', time, label: label ?? tag, detail })
|
|
158
165
|
break
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* More sources for a recording's DevTools, beside the rrweb events: a HAR (the network log browsers export) and a
|
|
2
|
+
console log (one message per line, `2026-06-12T21:01:38.123Z [warn] text`; indented lines continue the message
|
|
3
|
+
above). Both are read into the custom events devtools.ts already understands, stamped on their own absolute
|
|
4
|
+
times, so they line up with the recording when they share its clock. Anything stamped before the recording
|
|
5
|
+
starts or after it ends lands on its first or last moment. Pure readers, no rrweb import.
|
|
6
|
+
|
|
7
|
+
A HAR can also carry inspections (inspections.ts) as an extension: `log._inspections`, each
|
|
8
|
+
`{ at: ISO time, request?: entry _requestId or URL, message?: text, tab?, label? }`. */
|
|
9
|
+
import { ReplayEventType, type ReplayEvent } from './types'
|
|
10
|
+
|
|
11
|
+
type Json = Record<string, unknown>
|
|
12
|
+
const obj = (v: unknown): Json => (v && typeof v === 'object' ? (v as Json) : {})
|
|
13
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
|
14
|
+
const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) ? v : undefined)
|
|
15
|
+
const time = (v: unknown): number | undefined => {
|
|
16
|
+
const t = typeof v === 'string' ? Date.parse(v) : num(v)
|
|
17
|
+
return t !== undefined && Number.isFinite(t) ? t : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const custom = (timestamp: number, tag: string, payload: Json): ReplayEvent => ({
|
|
21
|
+
type: ReplayEventType.Custom,
|
|
22
|
+
timestamp,
|
|
23
|
+
data: { tag, payload },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/** HAR headers (`[{ name, value }]`) as a record. */
|
|
27
|
+
function harHeaders(list: unknown): Record<string, string> | undefined {
|
|
28
|
+
if (!Array.isArray(list)) return undefined
|
|
29
|
+
const out: Record<string, string> = {}
|
|
30
|
+
for (const h of list) {
|
|
31
|
+
const name = str(obj(h).name)
|
|
32
|
+
if (name) out[name] = String(obj(h).value ?? '')
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(out).length ? out : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A HAR body: as written, or decoded when base64 and textual. */
|
|
38
|
+
function harText(content: Json): string | undefined {
|
|
39
|
+
const text = typeof content.text === 'string' ? content.text : undefined
|
|
40
|
+
if (text === undefined || content.encoding !== 'base64') return text
|
|
41
|
+
try {
|
|
42
|
+
const bytes = Uint8Array.from(atob(text), (c) => c.charCodeAt(0))
|
|
43
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
44
|
+
return decoded
|
|
45
|
+
} catch {
|
|
46
|
+
return `[${Math.round((text.length * 3) / 4)} bytes of binary data]`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A HAR (object or JSON text) as `network` (and `inspect`) custom events. */
|
|
51
|
+
export function harToReplayEvents(har: unknown): ReplayEvent[] {
|
|
52
|
+
let data: unknown = har
|
|
53
|
+
if (typeof har === 'string') {
|
|
54
|
+
try { data = JSON.parse(har) } catch { return [] }
|
|
55
|
+
}
|
|
56
|
+
const log = obj(obj(data).log)
|
|
57
|
+
const out: ReplayEvent[] = []
|
|
58
|
+
for (const raw of Array.isArray(log.entries) ? log.entries : []) {
|
|
59
|
+
const e = obj(raw)
|
|
60
|
+
const started = time(e.startedDateTime)
|
|
61
|
+
if (started === undefined) continue
|
|
62
|
+
const request = obj(e.request)
|
|
63
|
+
const response = obj(e.response)
|
|
64
|
+
const duration = Math.max(0, num(e.time) ?? 0)
|
|
65
|
+
const status = num(response.status)
|
|
66
|
+
const error = str(e._error) ?? str(response._error)
|
|
67
|
+
out.push(custom(started + duration, error ? 'network.error' : 'network', {
|
|
68
|
+
requestId: str(e._requestId),
|
|
69
|
+
method: str(request.method) ?? 'GET',
|
|
70
|
+
url: str(request.url) ?? '',
|
|
71
|
+
status: status === -1 ? undefined : status,
|
|
72
|
+
statusText: str(response.statusText),
|
|
73
|
+
error,
|
|
74
|
+
duration,
|
|
75
|
+
type: str(e._resourceType) ?? 'fetch',
|
|
76
|
+
requestHeaders: harHeaders(request.headers),
|
|
77
|
+
responseHeaders: harHeaders(response.headers),
|
|
78
|
+
requestBody: str(obj(request.postData).text),
|
|
79
|
+
responseBody: harText(obj(response.content)),
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
for (const raw of Array.isArray(log._inspections) ? log._inspections : []) {
|
|
83
|
+
const i = obj(raw)
|
|
84
|
+
const at = time(i.at)
|
|
85
|
+
if (at === undefined) continue
|
|
86
|
+
out.push(custom(at, 'inspect', { request: str(i.request), message: str(i.message), tab: str(i.tab), label: str(i.label), id: str(i.id) }))
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const LINE = /^(\d{4}-\d\d-\d\dT[\d:.]+(?:Z|[+-]\d\d:?\d\d))\s+\[(\w+)\]\s?(.*)$/
|
|
92
|
+
|
|
93
|
+
/** A console log (`<ISO time> [level] message`, indented continuation lines) as `console` custom events. */
|
|
94
|
+
export function consoleLogToReplayEvents(text: string): ReplayEvent[] {
|
|
95
|
+
const out: { at: number; level: string; lines: string[] }[] = []
|
|
96
|
+
for (const line of text.split(/\r?\n/)) {
|
|
97
|
+
const m = line.match(LINE)
|
|
98
|
+
const at = m ? time(m[1]) : undefined
|
|
99
|
+
if (m && at !== undefined) out.push({ at, level: m[2]!.toLowerCase(), lines: [m[3] ?? ''] })
|
|
100
|
+
else if (out.length && /^\s/.test(line)) out[out.length - 1]!.lines.push(line.replace(/^ {2}/, ''))
|
|
101
|
+
}
|
|
102
|
+
return out.map((m) => custom(m.at, 'console', { level: m.level === 'warning' ? 'warn' : m.level, message: m.lines.join('\n').trimEnd() }))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The recording's events with the HAR's and the console log's added, each stamped inside the recording. */
|
|
106
|
+
export function mergeReplaySources(events: ReplayEvent[], sources: { har?: unknown; consoleLog?: string }): ReplayEvent[] {
|
|
107
|
+
const extra = [
|
|
108
|
+
...(sources.har ? harToReplayEvents(sources.har) : []),
|
|
109
|
+
...(sources.consoleLog ? consoleLogToReplayEvents(sources.consoleLog) : []),
|
|
110
|
+
]
|
|
111
|
+
if (!extra.length || !events.length) return events
|
|
112
|
+
let first = Infinity
|
|
113
|
+
let last = -Infinity
|
|
114
|
+
for (const e of events) {
|
|
115
|
+
first = Math.min(first, e.timestamp)
|
|
116
|
+
last = Math.max(last, e.timestamp)
|
|
117
|
+
}
|
|
118
|
+
const clamped = extra.map((e) => (e.timestamp < first || e.timestamp > last ? { ...e, timestamp: Math.min(last, Math.max(first, e.timestamp)) } : e))
|
|
119
|
+
return [...events, ...clamped].sort((a, b) => a.timestamp - b.timestamp)
|
|
120
|
+
}
|