@brett_lamy/docstream 1.24.0 → 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.
Files changed (41) hide show
  1. package/dist/docs/DocsRenderer.js +1 -1
  2. package/dist/docs/DocsRenderer.js.map +1 -1
  3. package/dist/gitbook/ast.d.ts +4 -0
  4. package/dist/gitbook/ast.d.ts.map +1 -1
  5. package/dist/gitbook/ast.js.map +1 -1
  6. package/dist/gitbook/parse.js +2 -0
  7. package/dist/gitbook/parse.js.map +1 -1
  8. package/dist/gitbook/serialize.d.ts.map +1 -1
  9. package/dist/gitbook/serialize.js +2 -0
  10. package/dist/gitbook/serialize.js.map +1 -1
  11. package/dist/replay/ReplayFile.d.ts +6 -2
  12. package/dist/replay/ReplayFile.d.ts.map +1 -1
  13. package/dist/replay/ReplayFile.js +20 -3
  14. package/dist/replay/ReplayFile.js.map +1 -1
  15. package/dist/replay/ReplayPlayer.d.ts +6 -1
  16. package/dist/replay/ReplayPlayer.d.ts.map +1 -1
  17. package/dist/replay/ReplayPlayer.js +4 -1
  18. package/dist/replay/ReplayPlayer.js.map +1 -1
  19. package/dist/replay/index.d.ts +1 -0
  20. package/dist/replay/index.d.ts.map +1 -1
  21. package/dist/replay/index.js +1 -0
  22. package/dist/replay/index.js.map +1 -1
  23. package/dist/replay/markers.d.ts.map +1 -1
  24. package/dist/replay/markers.js +13 -3
  25. package/dist/replay/markers.js.map +1 -1
  26. package/dist/replay/sources.d.ts +11 -0
  27. package/dist/replay/sources.d.ts.map +1 -0
  28. package/dist/replay/sources.js +126 -0
  29. package/dist/replay/sources.js.map +1 -0
  30. package/dist/styles.css +5 -3
  31. package/package.json +1 -1
  32. package/src/docs/DocsRenderer.tsx +1 -1
  33. package/src/gitbook/ast.ts +4 -0
  34. package/src/gitbook/parse.ts +2 -0
  35. package/src/gitbook/serialize.ts +2 -0
  36. package/src/replay/ReplayFile.tsx +35 -4
  37. package/src/replay/ReplayPlayer.tsx +9 -1
  38. package/src/replay/index.ts +1 -0
  39. package/src/replay/markers.ts +10 -3
  40. package/src/replay/sources.ts +120 -0
  41. package/src/styles.css +5 -3
@@ -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":"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"}
package/dist/styles.css CHANGED
@@ -4149,9 +4149,11 @@
4149
4149
  cursor: pointer;
4150
4150
  pointer-events: auto;
4151
4151
  appearance: none;
4152
- color: var(--background, #ffffff);
4153
- background: color-mix(in srgb, var(--foreground, #18181b) 72%, transparent);
4154
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
4152
+ /* A video player's scrim, not the page's tokens: it sits over a recorded page of any color, and a host can
4153
+ redefine --foreground (BL UI's markdown sets it to currentColor), which made a white icon on a white disc. */
4154
+ color: #ffffff;
4155
+ background: rgba(9, 9, 11, 0.62);
4156
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.16);
4155
4157
  -webkit-backdrop-filter: blur(12px);
4156
4158
  backdrop-filter: blur(12px);
4157
4159
  transition: scale 0.16s ease-out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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 } : {})} />
@@ -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
  }
@@ -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") }),
@@ -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
- if (!near || src === undefined) return <div ref={ref} className="docstream-asset-pending" data-slot="replay-file" aria-busy="true" />;
33
- return <ReplayPreview source={{ kind: 'events-url', url: src }} chrome="browser" timeWindow={segment} {...options} />;
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()
@@ -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';
@@ -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
- const status = text(payload.status)
154
- const method = text(payload.method) ?? 'GET'
155
- push({ kind: 'network', time, label: label ?? `${method} ${status ?? 'failed'}`, detail })
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
+ }
package/src/styles.css CHANGED
@@ -4149,9 +4149,11 @@
4149
4149
  cursor: pointer;
4150
4150
  pointer-events: auto;
4151
4151
  appearance: none;
4152
- color: var(--background, #ffffff);
4153
- background: color-mix(in srgb, var(--foreground, #18181b) 72%, transparent);
4154
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
4152
+ /* A video player's scrim, not the page's tokens: it sits over a recorded page of any color, and a host can
4153
+ redefine --foreground (BL UI's markdown sets it to currentColor), which made a white icon on a white disc. */
4154
+ color: #ffffff;
4155
+ background: rgba(9, 9, 11, 0.62);
4156
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), inset 0 0 0 1px rgba(255, 255, 255, 0.16);
4155
4157
  -webkit-backdrop-filter: blur(12px);
4156
4158
  backdrop-filter: blur(12px);
4157
4159
  transition: scale 0.16s ease-out;