Skip to main content

session-replay

Inspect, troubleshoot, and extend Analytics session replay recordings.

소스 정보

저장소
BuilderIO/agent-native
최근 소스 활동
2026년 10월 10일 23:49
감지된 SKILL.md 언어
영어
스타
7,119
포크
646

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
session-replay
description
Inspect, troubleshoot, and extend Analytics session replay recordings.
scope
dev
# Session Replay Use this skill when working on `/sessions`, replay ingest, replay storage, or agent answers about browser recordings in the Analytics template. ## Source Of Truth - Replay ingest writes `session_recordings` and `session_replay_chunks`. - The UI and agent must use `list-session-recordings`, `get-session-replay-summary`, and `get-session-replay-events`. - `/sessions/:recordingId` is keyed by `session_recordings.id`, not `analytics_events.session_id`. - Do not add actions that synthesize "sessions" from `analytics_events`. Events can be linked beside a recording through `session_id`, but they are not playable replay rows by themselves. ## Storage And Access - Never expose object-storage URLs or raw `session_replay_chunks` rows to the browser or agent. - Playback bytes must go through scoped server helpers that check `session-recording` access before reading private blob refs. - First-party SQL (`query-agent-native-analytics`, dashboard panels) reads `session_recordings` through the same `session-recording` access rule as the UI: the caller's own recordings, plus recordings in the active org that are org-visible or shared with the caller or that org. Any new SQL path over recordings must use that rule, not an `org_id` match. - SQL inline chunks are a local/dev fallback only; production should use private or encrypted blob storage. - A local Analytics app pointed at a production database must also use the key that encrypted those replay blobs. Set `ANALYTICS_SECRETS_ENCRYPTION_KEY` in an untracked local env file; do not replace the workspace-wide `BETTER_AUTH_SECRET` just to read production replay storage. - When sharing a replay with an external agent, use `create-session-replay-agent-link`. It mints a two-hour `agent_access` URL scoped to the recording, embeds a small SSR discovery payload on `/sessions/:recordingId`, and advertises `/api/session-replay/agent-context.json` plus bounded `/api/session-replay/agent-events.json` and `/api/session-replay/agent-diagnostics.json` reads. - Do not make session recordings public just so an agent can inspect them. Tokenized agent links are the intended handoff path. ## Console And Network Capture - While recording, the core client (`session-replay.ts` in `@agent-native/core`) patches console (`log`/`info`/`warn`/`error`/`debug`), window `error` / `unhandledrejection`, `fetch`, and XHR, and emits rrweb custom events tagged `agent-native.console` and `agent-native.network`. - Capture is on by default whenever session replay is enabled. Tune or disable it with the `console` / `network` options on the session replay config; each accepts a boolean or an options object (`{ maxEvents?: number }`); `network` also accepts `captureErrorBodies` (default true) and `maxErrorBodyLength` (default 2048) to control the bounded 5xx response-body snippet. - Privacy bounds: request bodies and headers are never captured. Response bodies are captured only as a bounded, redacted snippet for 5xx (server error) responses, capped at `maxErrorBodyLength` chars; non-5xx and network-failure (status 0) responses never carry a body. URLs are scrubbed, messages are truncated, and recorder self-traffic (the replay ingest and tracking endpoints) is excluded. - Per-session budgets: 1000 console events and 2000 network events, with a truncation notice event once a budget is hit. - On ingest, `deriveReplaySignals` computes the real `errorCount` from tagged console events plus the additive `networkErrorCount` column on `session_recordings`. Keep new columns additive. - Error, network-error, and rage-click deltas are added only for chunks newly stored by the ingest transaction, so retries do not count the same events twice. SQL increments keep concurrent uploads from overwriting those totals. Page counts remain max-based because the parser counts distinct URLs within each payload; older clients' cumulative overrides are also max-based. ## App Events In Sessions - `trackEvent` also emits an `agent-native.event` custom event holding only `{ name }` (120 chars max, 1000 per replay, counted across page reloads). Telemetry names such as `pageview`, `action.response`, and `session status` stay unmarked; lifecycle aliases don't get a second marker. Never add event properties to the payload. - The replay viewer shows these markers, plus failed `/_agent-native/actions/<name>` requests as "Action failed", only while the Sessions triage Lab is on. With the Lab off the viewer and the agent timeline keep their earlier shape. - `recordAnalyticsEvents` writes the per-session event index (`analytics_session_events`) and a per-tenant coverage start inside the transaction that stores the events, in every sink mode. After that transaction commits it writes the daily catalog (`analytics_event_catalog_daily`) and each event's latest sighting (`analytics_event_catalog_latest`) best-effort, together in one short transaction, because the catalog lists events from the latest table. A catalog failure only warns, and the catalog never decides a filter. Lists, did/didn't filters, and the catalog read only these tables, never BigQuery. The catalog keeps the 1,000 most recently seen events, sorted by volume, and sets `truncated` when it cut the list; its app flags still count every event in the range. - Indexes hold caller text raw, and one entry over Postgres's limit fails the whole batch. Ingest cuts every indexed value with `server/lib/indexed-text.ts` (event name, app, template, path) before any table sees it. A long user key keeps a prefix plus a hash of the whole value (`boundedIdentity`), so two users never merge into one. Row ids built from caller text go through `indexedRowId`, which hashes an id that percent-encoding made too long. Session and recording ids are never cut, because a cut id could merge two sessions: replay ingest rejects session and recording ids over 256 characters, and an event's longer session id skips the session index. Bound any new indexed caller value there. - Public ingest (`/track`, `/api/analytics/replay`) returns a thrown message only for an error built with `requestError` (a numeric `statusCode`). Any other failure is logged and answered with a generic 500, because its message can quote internal database details. - Event filters exclude a session if any of its recordings started before the tenant's coverage start, because one analytics session can span tabs. Coverage starts only after a session write succeeds, and the reported start is the latest among the viewer's tenants. "Didn't" also needs at least one index row for the session and no gap marker: a failed index write rolls back to a savepoint and records the batch's sessions in `analytics_session_event_gaps` in the same transaction, so a later successful batch cannot make them look complete. If the marker cannot be written either, the batch fails and its events are not stored. Keep session index writes inside that transaction. Deploys ship code before the scheduled migration creates these tables, so until `analytics_session_event_coverage` exists ingest stores events unindexed and warns: with no coverage, no session can read as complete. Reads in that window report no coverage instead of failing: no event names, a null coverage start, an empty catalog, and no session matching an event filter. That is the only unmarked gap, and it holds only while the coverage table is the last index table a migration creates. The retention sweep removes a session's index rows together, once all of them are two days past replay retention, and its gap marker after that. The BigQuery-cutover purge leaves these tables alone. ## Friction Signals - Friction is derived at ingest into Analytics' own tables and read only from them, in every sink mode. Replay signals (dead clicks, Sonner error toasts, retry loops, leaving within 30 seconds of an error, stalled requests over `STALLED_REQUEST_THRESHOLD_MS`, 4xx and 5xx responses) come from the rrweb chunks in `recordSessionReplayChunks`, one row per recording in `session_recording_friction`. Event signals (failed actions, agent failures, stuck chats, thumbs-down, quick backs, cancelled runs) and their trouble groups come from tracked events in a savepoint nested inside the event index savepoint, one row per session in `analytics_session_friction` plus `analytics_session_trouble`. - A request the page aborted itself (status 0 with an error `isBenignAbort` in core recognizes, or the recorder's `XMLHttpRequest aborted`) is not a failure: it never feeds retry loops or leaving after an error. Neither is one the recorder marked `pageLeaving`: the browser cancels in-flight requests when the page navigates or reloads, with the same "Failed to fetch" as a network failure, so only the recorder can tell them apart. It still counts as a stalled request. Any other status-0 failure counts. - Never store page text or URLs: detector state keeps timestamps, rrweb node ids, and hashed request keys; quick backs compare hashed paths. - One session id spans every tab, so a quick back compares pages only within one page load (`page_load_id` on each pageview). Pageviews from older clients send no id and are skipped: followed together, two tabs would look like one tab going back. - A replay row counts only while `processed_chunks` equals the recording's `chunk_count`. Each batch continues from the stored detector state, whose `lastSeq` is the highest chunk measured; a seq the recorder lost is skipped. A batch that cannot be measured, or a chunk below `lastSeq`, leaves the row behind and the recording reads as unmeasured until it ends. Then `remeasureReplayFriction` measures every stored chunk again in seq order; never continue a row that fell behind from fresh state. An event row counts only when the tenant's friction coverage began before every recording of the session and the session has neither an event index gap nor a friction gap (`analytics_session_friction_gaps`). A friction write failure rolls back only friction and records a friction gap; the index write stays. Only a failed friction gap insert fails the index savepoint. - Thumbs-down, cancelled runs, and quick backs are measured only for sessions whose pageviews carried `agent_signals` (`AGENT_SIGNALS_PAGEVIEW_PROPERTY` in core): older clients sampled stops, sent no ratings, and sent no `page_load_id`. One session id spans every tab, so an old tab can share a session with a new one: any unmarked pageview or sampled stop sets `agent_signals_missing`, and both flags are OR-merged. Unless `agent_signals_measured` is set and `agent_signals_missing` is not, these counts read as null, stay out of the score, never match a filter, and sort last. A stop counts only when sent unsampled (`sample_rate` absent or 1). - Reads must keep "unmeasured" (null) apart from "measured, no friction" (0). Until the migration creates `analytics_session_friction_coverage` (created last), ingest skips friction, filters match nothing, friction sorts fall back to newest, and details report every part as null. A friction filter or sort always returns the paginated shape with `frictionCoverageStartedAt`, and `view-screen` passes it on, so an empty match is read against coverage, never as zero. It covers the viewer's own org and personal tenants: the latest start among those with coverage, or null when one without coverage has recordings the viewer sees in the range. Recordings shared from other tenants read unmeasured on their own rows. - `errorIssues` links occurrences in `error_events`, which keeps only each issue's newest ones, then falls back to issues whose `last_session_recording_id` is the recording, with a null count. A recording with errors Monitoring could capture and still no issue reads null (unknown), never [], unless its owner scope has no issues at all: it does not capture errors as issues, so [] is the truth there. Only captured exceptions can become issues, and a plain `console.error` never does: the recorder marks console errors `exception: false` or `true`, and the replay row counts the others in `issue_errors`, so a recording whose only errors were plain console errors reads []. A row measured before that column, or not measured from its start, falls back to the recording's `errorCount`. - Agent failures group by a named cause from `AGENT_TROUBLE_CAUSES` in core (`no_model_connected`, `rate_limit`, `context_overflow`, `provider_error`), else by error code, never by message text. Failed actions group by action and status. New causes need product approval; add them to that one list. - The score is a weighted sum with each signal capped at `SESSION_FRICTION_SIGNAL_CAP`, computed per part at ingest and summed at read. Change weights only in `SESSION_FRICTION_WEIGHTS`. - Every friction surface is behind the Sessions triage Lab, in the UI and in `list-session-recordings`, `list-session-friction`, `view-screen`, and `get-session-replay-summary`; a 403 names what the Lab gates (`assertSessionsTriageLabEnabled` takes every feature the request used). With the Lab off, Sessions looks and behaves exactly as before; ingest still records friction. Waiting on the Lab state is shared with the speed filter; see "Performance In Sessions". - Without a friction filter or sort, the list loads without friction and its rows read friction from `list-session-friction`, one page of ids at most, through the recordings' access filter. A failed friction read then leaves the list in place with a retry. With a friction filter or sort, friction comes with the list and a failure is a list error. - The replay's dev tools show a Friction tab with every signal's count, its trouble groups, and its issue links. `get-session-replay-summary` returns the same `friction` for the agent. A failed Lab state or friction read is reported as `labStateError` or `frictionError`, never as no friction. Each is a fixed message, because the cause can quote database details; the server log keeps it. On the list, `view-screen` reports them beside the base list, and its `activeFilters` echo the filters it applied, not the URL's. Its row excerpt drops trailing rows to stay under the agent's 50,000-character tool-result limit, so the page metadata after the rows always arrives; a cut sets `truncated`, and `fullPageAction` reads the whole page. Keep it that way when adding per-row fields: a bigger row means fewer rows, not a lost page. ## Performance In Sessions - Core's tracker sends one `web_vitals` event per page view (`route`, `navigation_type`, and whichever of `ttfb_ms`, `lcp_ms`, `inp_ms`, `cls` were measured; an unmeasured metric is absent, never 0) and an `agent-native.vitals` replay marker with the same numbers. `action.response` carries the `route` its request started on. `route` is a React Router template such as `/sessions/:id`; when no manifest route matches it is omitted, never a raw path, which can hold slugs and emails. - Requests under `SLOW_REQUEST_THRESHOLD_MS` (`shared/slow-request.ts`, 1 s) are sampled at 10% with `sample_weight: 10`; slow, failed, and 4xx responses always send with weight 1, so slow-request counts are exact. Use that constant for every "slow request" rule. Requests made while the page was hidden or cancelled are not counted. Slow requests (the app's own actions, 1 s, counted from events) are not friction's stalled requests (any captured replay request, `STALLED_REQUEST_THRESHOLD_MS`, 3 s): keep their names, fields, columns, and copy apart, and never show one number under the other's label. - Ingest strips NUL and replaces lone surrogates in `route`, `app`, and the session id before hashing, as Postgres would store them; otherwise two values that store alike collide in one upsert and fail the batch. Measurements above `performanceCeiling` (the open top bucket's floor: 64 s, CLS 5) are capped there rather than dropped, and `sample_weight` at 10,000. A session value at the ceiling is a floor: summaries list it in `atLeast` and the row hint shows it with ≥. - `recordSessionPerformance` keeps each session's worst vitals and its slow requests in `analytics_session_performance`, inside the ingest transaction under a savepoint, with the same gap and coverage rules as the event index. `recordRoutePerformance` adds weights to fixed histogram buckets in `analytics_route_performance_daily` after commit, in its own short transaction. A failed write records a gap: `session_id = ''` marks a route day, read as `incompleteDates`; a session id marks that session, read as `performance.incomplete` and kept by `slow: any` only, since missing data cannot rule it out; `vitals` and `requests` need a measured slow value. The slow filter correlates on each recording's own tenant. As with event friction, a share grants the recording, not its tenant's events: a recording shared from another tenant has no speed summary (null) and never matches a slow filter. A summary's `slowRequests` is null when the session made no measured request. A null summary is a session never measured: its row says "Speed not measured", and a slow match or a range from before coverage shows when speed coverage began, so it never reads as fast. On `list-session-recordings`, `slow` and `includePerformance` always return the paginated shape with `performanceCoverageStartedAt`, as friction does. Bucket edges are positional, so changing them needs a new `PERFORMANCE_HISTOGRAM_VERSION`. - Percentiles interpolate inside one bucket. Past the first bucket each edge is at most 28% above the last, so they are within about 28% of the exact value; in the first bucket, within 1 ms (CLS 0.001). Keep that step when adding edges, and keep every rating threshold an edge. A value in the open top bucket is reported as `atLeast`. A metric with no samples is null: no data, never fast. Before the migration creates `analytics_performance_coverage`, ingest stores events without these aggregates and warns, the slow filter matches nothing, and reads report no coverage. - The replay's slow-request markers come from the `agent-native.slow_request` events core records beside each slow `action.response` someone waited for, with that event's own duration, status, and outcome. Core and the timeline both apply the count's rule (`isWaitedActionResponse`, from core), so a marker matches exactly what the row counts and background requests never spend the replay's marker budget. Never derive them from replay network events: those stop timing at the response headers. Replays from clients without these events show no slow-request markers. - `slow` and `includePerformance` on `list-session-recordings`, `list-session-performance`, `list-route-performance`, the replay's vitals and slow-request markers, and `performance` on `get-session-replay-summary` exist only while the Sessions triage Lab is on. - Lab state never holds up the base list. The page waits for it only when the URL carries Lab-only conditions (did/didn't events, friction signals or sorts, `slow`), with one wait for all of them, and for at most 5 s; a failed or hung read lists sessions without them and says so once, with a retry, rather than telling the user to turn on a Lab that may be on. Row speed hints load beside the list through `list-session-performance`, like row friction, keyed on the visible recording ids; when they fail the page says speed data could not load instead of showing no hints, apart from any friction failure. `view-screen` reads Lab state in its own `try` and reads row friction and speed hints beside the base list, reporting `labStateError`, `frictionError`, and `performanceError` separately; its `fullPageAction` asks for both. ## Agent Diagnostics Surface - `buildSessionReplayAgentContext` includes a `diagnostics` section: up to 50 console entries and 50 network entries, errors/failures first, with totals and truncated flags. Agent-context instructions steer agents to diagnostics as the primary debugging signal. - The agent timeline includes `console-error` / `network-error` markers; error markers are kept preferentially under the 200-marker cap. App event, Web Vitals and slow-request markers belong to the Sessions triage Lab and stay off it (`SESSIONS_TRIAGE_MARKER_TAGS`); add any new Lab marker tag there. - `apis.diagnostics` advertises the fuller bounded list: `GET /api/session-replay/agent-diagnostics.json?id=<recordingId>&agent_access=<token>&kind=console|network|all&level=<level>&limit=<n>&offset=<n>&fromMs=<n>&toMs=<n>` (limit defaults to 200, max 500). It uses the same recording-scoped `agent_access` token as the other agent JSON APIs. - `offset` and `fromMs`/`toMs` (inclusive offsetMs window) enable full enumeration of a session's captured entries: page with `offset`, or window with `fromMs`/`toMs` around a timeline marker's `offsetMs`. Providing any of these switches ordering to strictly chronological (no errors-first reshuffle) so pages are stable and disjoint. `total`/`errorCount`/ `warnCount`/`failedCount` reflect the filtered (windowed/level/kind) population, not just the returned page, and each kind's response includes `hasMore` alongside `truncated` so an agent can tell whether more entries remain. Route validation rejects negative/non-numeric `offset`/`fromMs`/ `toMs` and `fromMs > toMs` with 400. ## Dev Tools Panel - The `/sessions/:recordingId` replay player has a Dev Tools toggle that opens a panel with Console and Network tabs: filter chips, search, an error-count badge, and playback-time highlighting. - Rows expand inline under the selected line (Chrome-style). Expanding a row does not seek; use Jump to to move the playhead. Extend this panel instead of adding a separate debugging surface. ## Playback Viewer - Stream only when the manifest has dense, chronological chunk ranges and each available chunk's event count and timestamps match its manifest range. Start with the contiguous prefix from sequence 0, which must contain both rrweb Meta and FullSnapshot events. If any chunk is unavailable or any check fails, discard partial playback data and wait for the complete response before creating the normal player. - For a validated prefix, create one `Replayer` with `liveMode: true` and keep that instance for the recording. Append later chunks with `addEvent`; never rebuild the player as batches arrive, because that desynchronizes scrubbing and the playhead. Do not call `startLive()`: retain rrweb's normal play/pause state machine. Use the recording summary for the full duration while chunks are still loading. - Keep backward and in-range seeks immediate. If playback reaches the loaded horizon, or a seek targets later data, pause and show buffering; continue at the exact buffered playhead after the contiguous prefix arrives. Do not skip inactive ranges past the loaded horizon. When the recording completes, restore normal end-of-recording behavior. - Pass normal events to `Replayer` untouched. rrweb rebuilds them in a sandboxed iframe; pre-processing DOM, stylesheet, resource, or mutation payloads makes playback diverge from the captured page. In particular, never rewrite `href`, `src`, `_cssText`, CSS `url()`, or Meta URLs to `about:blank`; that exact remediation broke historical replay CSS in PR #2040. Handle request privacy at capture or the sandbox boundary instead of mutating stored rrweb events. Historical captures without inlined resources require live stylesheet/image/font requests for accurate rendering; the viewer accepts that fidelity tradeoff, uses rrweb's script-disabled sandbox plus `referrerpolicy="no-referrer"`, and must never add credentials or proxy those URLs through a privileged server. - Capture-time URL scrubbing must preserve load-bearing DOM resource attributes: `src`, `srcset`, `poster`, `data`, and `href` only on resource links such as stylesheets, preloads, and icons. Signed CDN query parameters are part of the resource identity; redacting them produces missing CSS, fonts, images, and oversized fallback icons. Keep scrubbing Meta/navigation URLs, anchor hrefs, and console/network diagnostics. Captured `_cssText` and CSS `@import`/`url()` values must remain byte-identical. - rrweb rebuilds into an `about:srcdoc` iframe, which inherits the Analytics document's CSP. Analytics currently sends no CSP header; if a future change adds restrictive `style-src`, `font-src`, or `img-src` directives, verify historical replays and resolve external imports/fonts at capture before blocking the recorded resource origins. Do not diagnose current font loss as CSP without checking the deployed response headers first. - Let rrweb own iframe sizing entirely via Meta / ViewportResize, and keep the outer wrapper on the exact same raw dimensions for fit-to-stage scaling. Player geometry and pointer coordinates are fully stock and untouched — do not add width/aspect-ratio "recovery" heuristics or pointer-coordinate projection. There is no such thing as a stored recording with corrupt viewport geometry: a census of all production recordings found zero stored widths >= 3,000px. The 2026-07 "ultra-wide replay" bugs (stages rendered 3,000–9,500px wide, frozen/teleporting cursors, giant icons) were caused entirely by demo mode's fetch interceptor: its number redactor faked any
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기