@brett_lamy/docstream 1.22.1 → 1.23.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.
@@ -0,0 +1,105 @@
1
+ /* Inspections: moments in a recording where someone (a person, a debugging agent) looked at a request or a console
2
+ message — "at 0:35 we inspected GET /api/search?q=te". The player follows them by default: as the playhead
3
+ reaches one, DevTools opens on its tab with that request (or message) selected and the inspection's label above
4
+ it, and playback carries on.
5
+
6
+ They come from the `inspections` prop or from the recording itself, as custom events tagged `inspect`
7
+ (`record.addCustomEvent('inspect', { tab, request, message, label })`), so an .rrweb file can carry its own.
8
+ An inspection names its target by the recording's id for it (a request's `requestId` / `id`, a custom console
9
+ event's `id`) or, failing that, by URL (the request with that URL sent nearest the inspection) or by text (the
10
+ last message containing it). Pure readers, no rrweb import. */
11
+ import type { ReplayConsoleEntry, ReplayDevtools, ReplayRequest } from './devtools'
12
+ import { ReplayEventType, type ReplayEvent } from './types'
13
+
14
+ export type ReplayInspectionTab = 'network' | 'console'
15
+
16
+ export interface ReplayInspection {
17
+ /** Stable id; defaults to `inspect-<n>`. */
18
+ id?: string
19
+ /** Milliseconds from the start of the recording. */
20
+ time: number
21
+ /** The DevTools tab to open (default: `network` when `request` is set, else `console`). */
22
+ tab?: ReplayInspectionTab
23
+ /** The request to select: the recording's id for it (`key`), or its URL. */
24
+ request?: string
25
+ /** The console message to select: the recording's id for it (`key`), or text it contains. */
26
+ message?: string
27
+ /** What was found, shown above the panel while the inspection is current. */
28
+ label?: string
29
+ }
30
+
31
+ /** An inspection with its defaults filled in and its target resolved against the recording's DevTools. */
32
+ export interface ResolvedReplayInspection {
33
+ id: string
34
+ time: number
35
+ tab: ReplayInspectionTab
36
+ label?: string
37
+ /** The `ReplayRequest.id` to select, when the target was found. */
38
+ requestId?: string
39
+ /** The `ReplayConsoleEntry.id` to select, when the target was found. */
40
+ messageId?: string
41
+ }
42
+
43
+ export const INSPECT_TAG = 'inspect'
44
+
45
+ const str = (value: unknown): string | undefined => (typeof value === 'string' && value ? value : undefined)
46
+
47
+ /** The `inspect` custom events in a recording, as inspections (times in ms from its first event). */
48
+ export function getReplayInspections(events: readonly ReplayEvent[]): ReplayInspection[] {
49
+ const list = [...events].sort((a, b) => a.timestamp - b.timestamp)
50
+ const start = list[0]?.timestamp ?? 0
51
+ const out: ReplayInspection[] = []
52
+ for (const event of list) {
53
+ if (event.type !== ReplayEventType.Custom) continue
54
+ const data = (event.data ?? {}) as { tag?: unknown; payload?: Record<string, unknown> }
55
+ if (data.tag !== INSPECT_TAG) continue
56
+ const p = data.payload ?? {}
57
+ const tab = p.tab === 'network' || p.tab === 'console' ? p.tab : undefined
58
+ out.push({ id: str(p.id), time: event.timestamp - start, tab, request: str(p.request), message: str(p.message), label: str(p.label) })
59
+ }
60
+ return out
61
+ }
62
+
63
+ function findRequest(network: readonly ReplayRequest[], target: string, time: number): ReplayRequest | undefined {
64
+ const byKey = network.find((r) => r.key === target)
65
+ if (byKey) return byKey
66
+ // By URL (exact, else the address without its origin): the one sent nearest the inspection.
67
+ const matches = network.filter((r) => r.url === target || r.url.replace(/^\w+:\/\/[^/]+/, '') === target)
68
+ return matches.sort((a, b) => Math.abs(a.time - time) - Math.abs(b.time - time))[0]
69
+ }
70
+
71
+ function findMessage(messages: readonly ReplayConsoleEntry[], target: string, time: number): ReplayConsoleEntry | undefined {
72
+ const byKey = messages.find((m) => m.key === target)
73
+ if (byKey) return byKey
74
+ const matches = messages.filter((m) => m.message.includes(target))
75
+ // The last one logged by the inspection, else the first after it.
76
+ return matches.filter((m) => m.time <= time).at(-1) ?? matches[0]
77
+ }
78
+
79
+ /** Fills in each inspection's id and tab and finds its request or message; sorted by time. */
80
+ export function resolveReplayInspections(inspections: readonly ReplayInspection[], tools: ReplayDevtools): ResolvedReplayInspection[] {
81
+ return inspections
82
+ .map((inspection, i) => {
83
+ const request = inspection.request ? findRequest(tools.network, inspection.request, inspection.time) : undefined
84
+ const message = inspection.message ? findMessage(tools.console, inspection.message, inspection.time) : undefined
85
+ return {
86
+ id: inspection.id ?? `inspect-${i}`,
87
+ time: Math.max(0, inspection.time),
88
+ tab: inspection.tab ?? (inspection.request ? 'network' : 'console'),
89
+ label: inspection.label,
90
+ requestId: request?.id,
91
+ messageId: message?.id,
92
+ } satisfies ResolvedReplayInspection
93
+ })
94
+ .sort((a, b) => a.time - b.time)
95
+ }
96
+
97
+ /** The inspection current at `time`: the latest at or before it. */
98
+ export function replayInspectionAt(inspections: readonly ResolvedReplayInspection[], time: number): ResolvedReplayInspection | undefined {
99
+ let current: ResolvedReplayInspection | undefined
100
+ for (const inspection of inspections) {
101
+ if (inspection.time > time) break
102
+ current = inspection
103
+ }
104
+ return current
105
+ }
package/src/styles.css CHANGED
@@ -4601,19 +4601,17 @@
4601
4601
  box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
4602
4602
  }
4603
4603
 
4604
- :where([data-slot="replay-devtools-tabs"] [data-part="at"]) {
4605
- display: inline-flex;
4606
- align-items: center;
4607
- margin-inline-start: auto;
4608
- padding-inline: 8px;
4609
- color: var(--replay-devtools-dim);
4610
- font-variant-numeric: tabular-nums;
4611
- }
4604
+
4605
+
4612
4606
 
4613
4607
  :where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"], [data-slot="replay-devtools-detail-tabs"] > [data-slot="replay-devtools-close"]) {
4614
4608
  padding: 0 8px;
4615
4609
  }
4616
4610
 
4611
+ :where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"]) {
4612
+ margin-inline-start: auto;
4613
+ }
4614
+
4617
4615
  :where([data-slot="replay-devtools-pane"]) {
4618
4616
  display: flex;
4619
4617
  flex: 1 1 0%;
@@ -4882,6 +4880,11 @@
4882
4880
  :where([data-slot="replay-devtools-table"] tbody tr:focus-visible) {
4883
4881
  box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
4884
4882
  }
4883
+ /* The inspected request or message: a bar down its leading edge (the selection tint shows it's open). */
4884
+ :where([data-slot="replay-devtools-table"] tbody tr[data-inspected], [data-slot="replay-devtools-message"][data-inspected]) {
4885
+ box-shadow: inset 3px 0 0 var(--primary, #2563eb);
4886
+ }
4887
+
4885
4888
 
4886
4889
  :where([data-slot="replay-devtools-table"] tr[data-failed]) {
4887
4890
  color: var(--replay-devtools-red);