@brett_lamy/docstream 1.22.0 → 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.
- package/dist/replay/ReplayDevtools.d.ts +6 -3
- package/dist/replay/ReplayDevtools.d.ts.map +1 -1
- package/dist/replay/ReplayDevtools.js +44 -22
- package/dist/replay/ReplayDevtools.js.map +1 -1
- package/dist/replay/ReplayPlayer.d.ts +12 -3
- package/dist/replay/ReplayPlayer.d.ts.map +1 -1
- package/dist/replay/ReplayPlayer.js +48 -9
- package/dist/replay/ReplayPlayer.js.map +1 -1
- package/dist/replay/devtools.d.ts +5 -0
- package/dist/replay/devtools.d.ts.map +1 -1
- package/dist/replay/devtools.js +3 -2
- package/dist/replay/devtools.js.map +1 -1
- package/dist/replay/index.d.ts +2 -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/inspections.d.ts +36 -0
- package/dist/replay/inspections.d.ts.map +1 -0
- package/dist/replay/inspections.js +64 -0
- package/dist/replay/inspections.js.map +1 -0
- package/dist/styles.css +62 -21
- package/package.json +1 -1
- package/src/replay/ReplayDevtools.tsx +53 -26
- package/src/replay/ReplayPlayer.tsx +60 -11
- package/src/replay/devtools.ts +8 -2
- package/src/replay/index.ts +2 -0
- package/src/replay/inspections.ts +105 -0
- package/src/styles.css +62 -21
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
plays the recording (rrweb loads lazily, on first render); this adds the chrome around it: a URL bar that
|
|
3
3
|
follows the recording's navigations, play/pause, a scrubber with the recording's moments on a rail above
|
|
4
4
|
it, a speed control, fullscreen, a drawn cursor whose clicks ripple, and DevTools: Console and Network buttons
|
|
5
|
-
in the top bar with what had been logged and requested by the playhead, which open a DevTools panel over the page
|
|
5
|
+
in the top bar with what had been logged and requested by the playhead, which open a DevTools panel over the page,
|
|
6
|
+
and inspections (inspections.ts) that open it on a request or message as playback reaches them. Until the player is ready a
|
|
6
7
|
poster (a page-shaped skeleton by default) holds the frame's exact size, so nothing jumps.
|
|
7
8
|
|
|
8
9
|
Plain React + plain CSS (`@layer docstream.replay` in styles.css, `[data-slot]` hooks). Keys (focus inside
|
|
@@ -18,6 +19,7 @@ import { Lock, Maximize, Minimize, Pause, Play, RotateCcw } from 'lucide-react'
|
|
|
18
19
|
import { ReplayStage } from './ReplayStage'
|
|
19
20
|
import { DevtoolsButtons, DevtoolsPanel, type ReplayDevtoolsTab } from './ReplayDevtools'
|
|
20
21
|
import { getReplayDevtools, replayDevtoolsAt } from './devtools'
|
|
22
|
+
import { getReplayInspections, replayInspectionAt, resolveReplayInspections, type ReplayInspection } from './inspections'
|
|
21
23
|
import { getReplayMarkers, getReplayMeta, getReplayPointerTrack, replayPointerAt } from './markers'
|
|
22
24
|
import type { ReplayMarker, ReplayMarkerKind, ReplayPointerTrack } from './markers'
|
|
23
25
|
import type { ReplayStageHandle } from './stage-types'
|
|
@@ -36,6 +38,8 @@ export interface ReplayPreviewHandle {
|
|
|
36
38
|
seek(ms: number): void
|
|
37
39
|
toggle(): void
|
|
38
40
|
getCurrentTime(): number
|
|
41
|
+
/** Jump to an inspection (by its id) and open DevTools on what it found. */
|
|
42
|
+
inspect(id: string): void
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** Options shared by every `ReplayPreview` (an `events` player or a `source` embed). */
|
|
@@ -63,11 +67,17 @@ export interface ReplayPlayerOptions {
|
|
|
63
67
|
/** Which recorded marker kinds to show (default all). */
|
|
64
68
|
markerKinds?: ReplayMarkerKind[]
|
|
65
69
|
/** Console and Network buttons (in the top bar, or beside the controls without the browser chrome) that open a
|
|
66
|
-
* DevTools panel cut at the playhead
|
|
67
|
-
*
|
|
70
|
+
* DevTools panel cut at the playhead, shown when the recording has console messages, requests or inspections.
|
|
71
|
+
* `disableDevtools` removes them, the panel and the inspections. */
|
|
72
|
+
disableDevtools?: boolean
|
|
73
|
+
/** @deprecated Use `disableDevtools`. `false` is the same as `disableDevtools`. */
|
|
68
74
|
devtools?: boolean
|
|
69
75
|
/** Open the DevTools panel on this tab at mount. */
|
|
70
76
|
defaultDevtoolsTab?: ReplayDevtoolsTab
|
|
77
|
+
/** Moments where a request or console message was inspected. As playback reaches one, DevTools opens on its
|
|
78
|
+
* tab with that request or message selected and the label above it; playback doesn't stop. Added to the
|
|
79
|
+
* recording's own `inspect` custom events, and to the rail as checkpoints. */
|
|
80
|
+
inspections?: ReplayInspection[]
|
|
71
81
|
/** Draw the recorded pointer and click ripples (default true). */
|
|
72
82
|
cursor?: boolean
|
|
73
83
|
/** `width` (default): the page scales to the frame's width and sets its height. `contain`: fill the frame's
|
|
@@ -98,7 +108,7 @@ const MARKER_NOUN: Record<ReplayMarkerKind, string> = {
|
|
|
98
108
|
export function ReplayPlayer({
|
|
99
109
|
events, url, title = 'Session replay', chrome = 'browser', trafficLights = true, controls = true,
|
|
100
110
|
autoplay = false, loop = false, initialTime = 0, speeds = DEFAULT_PLAYER_SPEEDS, markers = true, markerKinds,
|
|
101
|
-
cursor = true, devtools = true, defaultDevtoolsTab, fit = 'width', poster, lazy = true, playerRef, onTimeUpdate, onPlayingChange, onMarkerPress,
|
|
111
|
+
cursor = true, disableDevtools = false, devtools = true, defaultDevtoolsTab, inspections, fit = 'width', poster, lazy = true, playerRef, onTimeUpdate, onPlayingChange, onMarkerPress,
|
|
102
112
|
className, style,
|
|
103
113
|
}: ReplayPlayerOptions & { events: ReplayEvent[] }) {
|
|
104
114
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
@@ -107,14 +117,26 @@ export function ReplayPlayer({
|
|
|
107
117
|
const meta = useMemo(() => getReplayMeta(events), [events])
|
|
108
118
|
const track = useMemo(() => getReplayPointerTrack(events), [events])
|
|
109
119
|
const recorded = useMemo(() => getReplayMarkers(events), [events])
|
|
110
|
-
const shown = useMemo(() => {
|
|
111
|
-
const list = Array.isArray(markers) ? markers : markers ? recorded : []
|
|
112
|
-
return markerKinds ? list.filter((m) => markerKinds.includes(m.kind)) : list
|
|
113
|
-
}, [markers, recorded, markerKinds])
|
|
114
120
|
|
|
121
|
+
const toolsOn = devtools && !disableDevtools
|
|
115
122
|
const tools = useMemo(() => getReplayDevtools(events), [events])
|
|
116
|
-
const
|
|
123
|
+
const resolved = useMemo(
|
|
124
|
+
() => (toolsOn ? resolveReplayInspections([...getReplayInspections(events), ...(inspections ?? [])], tools) : []),
|
|
125
|
+
[toolsOn, events, inspections, tools],
|
|
126
|
+
)
|
|
127
|
+
const showTools = toolsOn && (tools.console.length > 0 || tools.network.length > 0 || resolved.length > 0)
|
|
117
128
|
const [panel, setPanel] = useState<ReplayDevtoolsTab | null>(defaultDevtoolsTab ?? null)
|
|
129
|
+
// Whether an inspection (rather than a press) opened the panel: it then leaves focus where it was.
|
|
130
|
+
const [autoOpened, setAutoOpened] = useState(false)
|
|
131
|
+
|
|
132
|
+
const shown = useMemo(() => {
|
|
133
|
+
// Inspections from the prop join the rail as checkpoints (the recording's own are already among `recorded`).
|
|
134
|
+
const own: ReplayMarker[] = toolsOn ? (inspections ?? []).map((inspection, i) => ({
|
|
135
|
+
id: `inspection-${i}`, kind: 'custom', time: Math.max(0, inspection.time), label: inspection.label ?? 'Inspected',
|
|
136
|
+
})) : []
|
|
137
|
+
const list = Array.isArray(markers) ? markers : markers ? [...recorded, ...own].sort((a, b) => a.time - b.time) : []
|
|
138
|
+
return markerKinds ? list.filter((m) => markerKinds.includes(m.kind)) : list
|
|
139
|
+
}, [markers, recorded, markerKinds, inspections, toolsOn])
|
|
118
140
|
|
|
119
141
|
const [visible, setVisible] = useState(!lazy)
|
|
120
142
|
const [ready, setReady] = useState(false)
|
|
@@ -151,12 +173,22 @@ export function ReplayPlayer({
|
|
|
151
173
|
return () => clearTimeout(timer)
|
|
152
174
|
}, [ready, reduced])
|
|
153
175
|
|
|
176
|
+
const resolvedRef = useRef(resolved)
|
|
177
|
+
resolvedRef.current = resolved
|
|
154
178
|
const handle = useMemo<ReplayPreviewHandle>(() => ({
|
|
155
179
|
play: (offset) => { setFinished(false); stage.current?.play(offset) },
|
|
156
180
|
pause: (offset) => stage.current?.pause(offset),
|
|
157
181
|
seek: (ms) => { setFinished(false); stage.current?.seek(ms) },
|
|
158
182
|
toggle: () => { setFinished(false); stage.current?.toggle() },
|
|
159
183
|
getCurrentTime: () => stage.current?.getCurrentTime() ?? 0,
|
|
184
|
+
inspect: (id) => {
|
|
185
|
+
const inspection = resolvedRef.current.find((x) => x.id === id)
|
|
186
|
+
if (!inspection) return
|
|
187
|
+
setFinished(false)
|
|
188
|
+
stage.current?.seek(inspection.time)
|
|
189
|
+
setPanel(inspection.tab)
|
|
190
|
+
setAutoOpened(false)
|
|
191
|
+
},
|
|
160
192
|
}), [])
|
|
161
193
|
useImperativeHandle(playerRef, () => handle, [handle])
|
|
162
194
|
|
|
@@ -209,7 +241,23 @@ export function ReplayPlayer({
|
|
|
209
241
|
}, [url, meta.href, recorded, time])
|
|
210
242
|
|
|
211
243
|
const toolsState = useMemo(() => replayDevtoolsAt(tools, time), [tools, time])
|
|
212
|
-
const openTools = (tab: ReplayDevtoolsTab) => setPanel((p) => (p === tab ? null : tab))
|
|
244
|
+
const openTools = (tab: ReplayDevtoolsTab) => { setAutoOpened(false); setPanel((p) => (p === tab ? null : tab)) }
|
|
245
|
+
|
|
246
|
+
// Inspect mode: the inspection at the playhead opens DevTools on what it found. Only a change of inspection does
|
|
247
|
+
// (closing the panel keeps it closed until the next one); scrubbing back before the first closes what it opened.
|
|
248
|
+
const inspection = showTools ? replayInspectionAt(resolved, time) : undefined
|
|
249
|
+
const inspectionId = inspection?.id
|
|
250
|
+
const inspectionTab = inspection?.tab
|
|
251
|
+
const lastInspection = useRef<string | undefined>(undefined)
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (inspectionId === lastInspection.current) return
|
|
254
|
+
const had = lastInspection.current
|
|
255
|
+
lastInspection.current = inspectionId
|
|
256
|
+
if (inspectionTab) {
|
|
257
|
+
setPanel(inspectionTab)
|
|
258
|
+
setAutoOpened(true)
|
|
259
|
+
} else if (had !== undefined && autoOpened) setPanel(null)
|
|
260
|
+
}, [inspectionId, inspectionTab, autoOpened])
|
|
213
261
|
const toolButtons = showTools ? <DevtoolsButtons state={toolsState} open={panel} onOpen={openTools} /> : null
|
|
214
262
|
|
|
215
263
|
const w = meta.width || 1280
|
|
@@ -269,11 +317,12 @@ export function ReplayPlayer({
|
|
|
269
317
|
{showTools && panel ? (
|
|
270
318
|
<DevtoolsPanel
|
|
271
319
|
state={toolsState}
|
|
272
|
-
at={time}
|
|
273
320
|
tab={panel}
|
|
274
321
|
onTab={setPanel}
|
|
275
322
|
onClose={() => { setPanel(null); rootRef.current?.focus({ preventScroll: true }) }}
|
|
276
323
|
onSeek={(ms) => handle.seek(ms)}
|
|
324
|
+
inspection={inspection}
|
|
325
|
+
autoFocus={!autoOpened}
|
|
277
326
|
/>
|
|
278
327
|
) : null}
|
|
279
328
|
|
package/src/replay/devtools.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type ReplayConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug'
|
|
|
17
17
|
export interface ReplayConsoleEntry {
|
|
18
18
|
/** Stable id (`console-<n>`). */
|
|
19
19
|
id: string
|
|
20
|
+
/** The recording's own id for the message (`payload.id` of a custom event), which an inspection can name. */
|
|
21
|
+
key?: string
|
|
20
22
|
/** Milliseconds from the start of the recording. */
|
|
21
23
|
time: number
|
|
22
24
|
level: ReplayConsoleLevel
|
|
@@ -33,6 +35,9 @@ export interface ReplayConsoleEntry {
|
|
|
33
35
|
export interface ReplayRequest {
|
|
34
36
|
/** Stable id (`request-<n>`). */
|
|
35
37
|
id: string
|
|
38
|
+
/** The recording's own id for the request (`id` / `requestId`, e.g. a Replay request id), which an
|
|
39
|
+
* inspection can name. */
|
|
40
|
+
key?: string
|
|
36
41
|
/** When the request was sent, in ms from the start of the recording. */
|
|
37
42
|
time: number
|
|
38
43
|
/** How long it took, when known: the response arrived at `time + duration`. */
|
|
@@ -153,6 +158,7 @@ export function getReplayDevtools(events: readonly ReplayEvent[]): ReplayDevtool
|
|
|
153
158
|
// request was sent that far before it (its own duration, for a lone request).
|
|
154
159
|
const sent = Math.max(0, startTime !== undefined && batchEnd !== undefined ? at - (batchEnd - startTime) : at - (duration ?? 0))
|
|
155
160
|
requests.push({
|
|
161
|
+
key: str(raw.requestId) ?? str(raw.id),
|
|
156
162
|
time: sent,
|
|
157
163
|
duration,
|
|
158
164
|
method: (str(raw.method) ?? 'GET').toUpperCase(),
|
|
@@ -197,10 +203,10 @@ export function getReplayDevtools(events: readonly ReplayEvent[]): ReplayDevtool
|
|
|
197
203
|
const detail = str(payload.message) ?? str(payload.detail)
|
|
198
204
|
const message = detail ? `Uncaught ${label}: ${detail}` : `Uncaught ${label}`
|
|
199
205
|
const trace = Array.isArray(payload.stack) ? payload.stack.map(String) : str(payload.stack)?.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
200
|
-
consoleEntries.push({ time, level: 'error', message, args: [message], trace: trace?.length ? trace : undefined, source: 'exception' })
|
|
206
|
+
consoleEntries.push({ key: str(payload.id), time, level: 'error', message, args: [message], trace: trace?.length ? trace : undefined, source: 'exception' })
|
|
201
207
|
} else if (CONSOLE_TAGS.has(tag)) {
|
|
202
208
|
const args = Array.isArray(payload.args) ? payload.args.map((a) => asText(a) ?? '') : [str(payload.message) ?? str(payload.label) ?? asText(payload.detail) ?? '']
|
|
203
|
-
consoleEntries.push({ time, level: levelOf(payload.level ?? tag), message: args.join(' '), args, source: 'custom' })
|
|
209
|
+
consoleEntries.push({ key: str(payload.id), time, level: levelOf(payload.level ?? tag), message: args.join(' '), args, source: 'custom' })
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
}
|
package/src/replay/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type { ReplayMarker, ReplayMarkerKind, ReplayMeta, ReplayPointerPoint, Re
|
|
|
18
18
|
export { getReplayDevtools, replayDevtoolsAt, requestName } from './devtools';
|
|
19
19
|
export type { ReplayConsoleEntry, ReplayConsoleLevel, ReplayDevtools, ReplayDevtoolsState, ReplayRequest } from './devtools';
|
|
20
20
|
export type { ReplayDevtoolsTab } from './ReplayDevtools';
|
|
21
|
+
export { getReplayInspections, replayInspectionAt, resolveReplayInspections } from './inspections';
|
|
22
|
+
export type { ReplayInspection, ReplayInspectionTab, ResolvedReplayInspection } from './inspections';
|
|
21
23
|
export { ReplayEventType, ReplayIncrementalSource, ReplayMouseInteraction } from './types';
|
|
22
24
|
export type { ReplayEvent } from './types';
|
|
23
25
|
export { isReplayQaUrl, normalizeReplayEmbedUrl } from './url';
|
|
@@ -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
|
@@ -4440,6 +4440,14 @@
|
|
|
4440
4440
|
/* ── Replay DevTools: the Console / Network buttons in the top bar and the panel over the page ──
|
|
4441
4441
|
Chrome's DevTools, in the player's tokens. Failed requests and errors read --destructive, warnings --warning. */
|
|
4442
4442
|
@layer docstream.replay {
|
|
4443
|
+
/* Text tones that keep 4.5:1 on the card in light and dark: the theme's muted, red and amber, pulled toward the
|
|
4444
|
+
foreground (a theme's own --muted-foreground or --destructive is often tuned for larger text). */
|
|
4445
|
+
:where([data-slot="replay-preview"]) {
|
|
4446
|
+
--replay-devtools-dim: color-mix(in srgb, var(--foreground, #18181b) 68%, var(--card, #ffffff));
|
|
4447
|
+
--replay-devtools-red: color-mix(in srgb, var(--destructive, #dc2626) 74%, var(--foreground, #18181b));
|
|
4448
|
+
--replay-devtools-amber: color-mix(in srgb, var(--warning, #b45309) 56%, var(--foreground, #18181b));
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4443
4451
|
:where([data-slot="replay-preview-actions"]) {
|
|
4444
4452
|
display: flex;
|
|
4445
4453
|
flex-shrink: 0;
|
|
@@ -4465,7 +4473,7 @@
|
|
|
4465
4473
|
outline: none;
|
|
4466
4474
|
cursor: pointer;
|
|
4467
4475
|
appearance: none;
|
|
4468
|
-
color: var(--
|
|
4476
|
+
color: var(--replay-devtools-dim);
|
|
4469
4477
|
background: transparent;
|
|
4470
4478
|
font: inherit;
|
|
4471
4479
|
font-size: 11.5px;
|
|
@@ -4489,12 +4497,12 @@
|
|
|
4489
4497
|
}
|
|
4490
4498
|
|
|
4491
4499
|
:where([data-slot="replay-preview"] [data-part="failed"]) {
|
|
4492
|
-
color: var(--
|
|
4500
|
+
color: var(--replay-devtools-red);
|
|
4493
4501
|
font-weight: 600;
|
|
4494
4502
|
}
|
|
4495
4503
|
|
|
4496
4504
|
:where([data-slot="replay-preview"] [data-part="warn"]) {
|
|
4497
|
-
color: var(--
|
|
4505
|
+
color: var(--replay-devtools-amber);
|
|
4498
4506
|
font-weight: 600;
|
|
4499
4507
|
}
|
|
4500
4508
|
|
|
@@ -4549,7 +4557,7 @@
|
|
|
4549
4557
|
outline: none;
|
|
4550
4558
|
cursor: pointer;
|
|
4551
4559
|
appearance: none;
|
|
4552
|
-
color: var(--
|
|
4560
|
+
color: var(--replay-devtools-dim);
|
|
4553
4561
|
background: transparent;
|
|
4554
4562
|
font: inherit;
|
|
4555
4563
|
}
|
|
@@ -4563,19 +4571,47 @@
|
|
|
4563
4571
|
box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
|
|
4564
4572
|
}
|
|
4565
4573
|
|
|
4566
|
-
:where([data-slot="replay-devtools-
|
|
4574
|
+
:where([data-slot="replay-devtools-tablist"]) {
|
|
4575
|
+
display: flex;
|
|
4576
|
+
align-items: stretch;
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
:where([data-slot="replay-devtools-tablist"] > button) {
|
|
4567
4580
|
display: inline-flex;
|
|
4568
4581
|
align-items: center;
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4582
|
+
gap: 5px;
|
|
4583
|
+
margin: 0;
|
|
4584
|
+
padding: 0 10px;
|
|
4585
|
+
border: 0;
|
|
4586
|
+
border-bottom: 2px solid transparent;
|
|
4587
|
+
outline: none;
|
|
4588
|
+
cursor: pointer;
|
|
4589
|
+
appearance: none;
|
|
4590
|
+
color: var(--replay-devtools-dim);
|
|
4591
|
+
background: transparent;
|
|
4592
|
+
font: inherit;
|
|
4573
4593
|
}
|
|
4574
4594
|
|
|
4595
|
+
:where([data-slot="replay-devtools-tablist"] > button[aria-selected="true"]) {
|
|
4596
|
+
color: var(--foreground, #18181b);
|
|
4597
|
+
border-bottom-color: var(--primary, #2563eb);
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
:where([data-slot="replay-devtools-tablist"] > button:focus-visible) {
|
|
4601
|
+
box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
|
|
4605
|
+
|
|
4606
|
+
|
|
4575
4607
|
:where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"], [data-slot="replay-devtools-detail-tabs"] > [data-slot="replay-devtools-close"]) {
|
|
4576
4608
|
padding: 0 8px;
|
|
4577
4609
|
}
|
|
4578
4610
|
|
|
4611
|
+
:where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"]) {
|
|
4612
|
+
margin-inline-start: auto;
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4579
4615
|
:where([data-slot="replay-devtools-pane"]) {
|
|
4580
4616
|
display: flex;
|
|
4581
4617
|
flex: 1 1 0%;
|
|
@@ -4635,7 +4671,7 @@
|
|
|
4635
4671
|
outline: none;
|
|
4636
4672
|
cursor: pointer;
|
|
4637
4673
|
appearance: none;
|
|
4638
|
-
color: var(--
|
|
4674
|
+
color: var(--replay-devtools-dim);
|
|
4639
4675
|
background: transparent;
|
|
4640
4676
|
font: inherit;
|
|
4641
4677
|
}
|
|
@@ -4669,19 +4705,19 @@
|
|
|
4669
4705
|
}
|
|
4670
4706
|
|
|
4671
4707
|
:where([data-slot="replay-devtools-message"][data-level="warn"]) {
|
|
4672
|
-
color:
|
|
4708
|
+
color: var(--replay-devtools-amber);
|
|
4673
4709
|
background: color-mix(in srgb, var(--warning, #f59e0b) 10%, transparent);
|
|
4674
4710
|
border-bottom-color: color-mix(in srgb, var(--warning, #f59e0b) 25%, transparent);
|
|
4675
4711
|
}
|
|
4676
4712
|
|
|
4677
4713
|
:where([data-slot="replay-devtools-message"][data-level="error"]) {
|
|
4678
|
-
color: var(--
|
|
4714
|
+
color: var(--replay-devtools-red);
|
|
4679
4715
|
background: color-mix(in srgb, var(--destructive, #dc2626) 8%, transparent);
|
|
4680
4716
|
border-bottom-color: color-mix(in srgb, var(--destructive, #dc2626) 22%, transparent);
|
|
4681
4717
|
}
|
|
4682
4718
|
|
|
4683
4719
|
:where([data-slot="replay-devtools-message"][data-level="debug"]) {
|
|
4684
|
-
color: var(--
|
|
4720
|
+
color: var(--replay-devtools-dim);
|
|
4685
4721
|
}
|
|
4686
4722
|
|
|
4687
4723
|
:where([data-slot="replay-devtools-message"] > details, [data-slot="replay-devtools-message"] > [data-part="line"]) {
|
|
@@ -4737,7 +4773,7 @@
|
|
|
4737
4773
|
outline: none;
|
|
4738
4774
|
cursor: pointer;
|
|
4739
4775
|
appearance: none;
|
|
4740
|
-
color: var(--
|
|
4776
|
+
color: var(--replay-devtools-dim);
|
|
4741
4777
|
background: transparent;
|
|
4742
4778
|
font: inherit;
|
|
4743
4779
|
font-variant-numeric: tabular-nums;
|
|
@@ -4756,7 +4792,7 @@
|
|
|
4756
4792
|
:where([data-slot="replay-devtools-empty"], [data-slot="replay-devtools-later"]) {
|
|
4757
4793
|
margin: 0;
|
|
4758
4794
|
padding: 10px 12px;
|
|
4759
|
-
color: var(--
|
|
4795
|
+
color: var(--replay-devtools-dim);
|
|
4760
4796
|
}
|
|
4761
4797
|
|
|
4762
4798
|
:where([data-slot="replay-devtools-later"]) {
|
|
@@ -4796,7 +4832,7 @@
|
|
|
4796
4832
|
height: 22px;
|
|
4797
4833
|
padding: 0 8px;
|
|
4798
4834
|
border-bottom: 1px solid var(--border, #e4e4e7);
|
|
4799
|
-
color: var(--
|
|
4835
|
+
color: var(--replay-devtools-dim);
|
|
4800
4836
|
background: var(--card, #ffffff);
|
|
4801
4837
|
font-weight: 500;
|
|
4802
4838
|
text-align: start;
|
|
@@ -4844,13 +4880,18 @@
|
|
|
4844
4880
|
:where([data-slot="replay-devtools-table"] tbody tr:focus-visible) {
|
|
4845
4881
|
box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
|
|
4846
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
|
+
|
|
4847
4888
|
|
|
4848
4889
|
:where([data-slot="replay-devtools-table"] tr[data-failed]) {
|
|
4849
|
-
color: var(--
|
|
4890
|
+
color: var(--replay-devtools-red);
|
|
4850
4891
|
}
|
|
4851
4892
|
|
|
4852
4893
|
:where([data-slot="replay-devtools-table"] tr[data-pending]) {
|
|
4853
|
-
color: var(--
|
|
4894
|
+
color: var(--replay-devtools-dim);
|
|
4854
4895
|
}
|
|
4855
4896
|
|
|
4856
4897
|
:where([data-slot="replay-devtools-table"] td[data-col="waterfall"]) {
|
|
@@ -4871,7 +4912,7 @@
|
|
|
4871
4912
|
}
|
|
4872
4913
|
|
|
4873
4914
|
:where([data-slot="replay-devtools-table"] tr[data-pending] [data-part="bar"]) {
|
|
4874
|
-
background: color-mix(in srgb, var(--
|
|
4915
|
+
background: color-mix(in srgb, var(--replay-devtools-dim) 50%, transparent);
|
|
4875
4916
|
}
|
|
4876
4917
|
|
|
4877
4918
|
:where([data-slot="replay-devtools-status"]) {
|
|
@@ -4882,7 +4923,7 @@
|
|
|
4882
4923
|
height: 22px;
|
|
4883
4924
|
padding: 0 8px;
|
|
4884
4925
|
border-top: 1px solid var(--border, #e4e4e7);
|
|
4885
|
-
color: var(--
|
|
4926
|
+
color: var(--replay-devtools-dim);
|
|
4886
4927
|
font-variant-numeric: tabular-nums;
|
|
4887
4928
|
}
|
|
4888
4929
|
|
|
@@ -4939,7 +4980,7 @@
|
|
|
4939
4980
|
:where([data-slot="replay-devtools-section"] dt) {
|
|
4940
4981
|
flex-shrink: 0;
|
|
4941
4982
|
min-width: 130px;
|
|
4942
|
-
color: var(--
|
|
4983
|
+
color: var(--replay-devtools-dim);
|
|
4943
4984
|
}
|
|
4944
4985
|
|
|
4945
4986
|
:where([data-slot="replay-devtools-section"] dd) {
|