-
-
Notifications
You must be signed in to change notification settings - Fork 10
feat(replay): Add agent-readable replay timelines #913
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
ea6c53a
a3ae0f3
075bc41
239e2ea
fe3b35a
b675f5a
55182be
8185730
8c8f338
97de06c
bcea1b6
8b9a386
17b8e28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,15 +11,64 @@ requires: | |
|
|
||
| Search and inspect Session Replays | ||
|
|
||
| ### `sentry replay event list <replay-id-or-url...>` | ||
|
|
||
| List normalized events from a Session Replay | ||
|
|
||
| **Flags:** | ||
| - `-k, --kind <value>... - Event kind filter (navigation, click, tap, input, focus, blur, scroll, viewport, mutation, dom-snapshot, breadcrumb, network, console, error, span, web-vital, memory, video, mobile, unknown)` | ||
| - `-u, --url <value> - Filter events by current or target URL substring` | ||
| - `--path <value> - Filter events by parsed URL pathname` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can make this an optional positional argument instead as it looks quite common and looks like a natural sub-section? |
||
| - `-q, --contains <value> - Filter events by text in labels, messages, URLs, selectors, or data` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - `--selector <value> - Filter events by selector substring` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not clear how |
||
| - `--from <value> - Start offset (seconds, 90s, 01:23, or 1:02:03)` | ||
| - `--to <value> - End offset (seconds, 90s, 01:23, or 1:02:03)` | ||
| - `--around <value> - Center an evidence window around this offset` | ||
| - `--before <value> - Window before --around (default: 10s)` | ||
| - `--after <value> - Window after --around (default: 30s)` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the existing |
||
| - `-n, --limit <value> - Number of events (1-1000) - (default: "200")` | ||
| - `--raw - Include raw source frame payloads in JSON output` | ||
| - `--jsonl - Emit one JSON object per event (requires --json)` | ||
| - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` | ||
|
|
||
| **JSON Fields** (use `--json --fields` to select specific fields): | ||
|
|
||
| | Field | Type | Description | | ||
| |-------|------|-------------| | ||
| | `replayId` | string | Replay ID | | ||
| | `segmentIndex` | number | Zero-based recording segment index | | ||
| | `frameIndex` | number | Zero-based frame index within segment | | ||
| | `offsetMs` | number \| null | Milliseconds from replay start to the event | | ||
| | `timestamp` | string \| null | Event timestamp as ISO 8601 when available | | ||
| | `kind` | string | Normalized event kind | | ||
| | `category` | string | Broad event category | | ||
| | `label` | string \| null | Short event label | | ||
| | `message` | string \| null | Message or summary | | ||
| | `url` | string \| null | Current or target URL | | ||
| | `urlPath` | string \| null | Parsed URL pathname when available | | ||
| | `urlQuery` | string \| null | Parsed URL query string when available | | ||
| | `selector` | string \| null | CSS selector or target selector when available | | ||
| | `nodeId` | unknown \| null | rrweb node ID when available | | ||
| | `rawType` | string \| null | Source frame type | | ||
| | `rawSource` | string \| null | Source frame subtype | | ||
| | `data` | unknown | Kind-specific normalized fields | | ||
| | `raw` | unknown | Raw source frame, only present when requested | | ||
|
|
||
| ### `sentry replay list <org/project>` | ||
|
|
||
| List recent Session Replays | ||
|
|
||
| **Flags:** | ||
| - `-n, --limit <value> - Number of replays (1-1000) - (default: "25")` | ||
| - `-q, --query <value> - Search query (Sentry replay search syntax)` | ||
| - `-u, --url <value> - Filter by visited URL text using replay search` | ||
| - `--path <value> - Filter by actual visited URL pathname` | ||
| - `--entry-path <value> - Filter by first visited URL pathname` | ||
| - `--exit-path <value> - Filter by last visited URL pathname` | ||
| - `--friction - Only show replays with indexed friction signals (errors, warnings, rage clicks, or dead clicks)` | ||
| - `--problem-only - Only show replays with indexed errors or warnings` | ||
| - `-e, --environment <value>... - Filter by environment (repeatable, comma-separated)` | ||
| - `-s, --sort <value> - Sort by: date, oldest, duration, errors, activity, or a raw replay sort field - (default: "date")` | ||
| - `-s, --sort <value> - Sort by: date, oldest, duration, errors, warnings, rage, dead, activity, or a raw replay sort field - (default: "date")` | ||
| - `-t, --period <value> - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` | ||
| - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` | ||
| - `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)` | ||
|
|
@@ -72,6 +121,12 @@ sentry replay list my-org/ --query "environment:production" | |
| # Change the time window and sort | ||
| sentry replay list my-org/frontend --period 24h --sort errors | ||
|
|
||
| # Find recent sessions that actually visited a route path | ||
| sentry replay list my-org/frontend --path /signup --json | ||
|
|
||
| # Find recent sessions with indexed friction signals | ||
| sentry replay list my-org/frontend --path /signup --friction --json | ||
|
|
||
| # Paginate through results | ||
| sentry replay list my-org/frontend -c next | ||
| sentry replay list my-org/frontend -c prev | ||
|
|
@@ -80,6 +135,50 @@ sentry replay list my-org/frontend -c prev | |
| sentry replay list my-org/frontend --json | ||
| ``` | ||
|
|
||
| ### `sentry replay summarize <replay-id-or-url...>` | ||
|
|
||
| Summarize Session Replay behavior | ||
|
|
||
| **Flags:** | ||
| - `--path <value> - Focus summary on events from this URL pathname` | ||
| - `--limit-signals <value> - Maximum friction signals to include (0-50) - (default: "10")` | ||
| - `--limit-events <value> - Maximum notable events to include (0-50) - (default: "12")` | ||
| - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` | ||
|
|
||
| **JSON Fields** (use `--json --fields` to select specific fields): | ||
|
|
||
| | Field | Type | Description | | ||
| |-------|------|-------------| | ||
| | `replayId` | string | Replay ID | | ||
| | `org` | string | Organization slug | | ||
| | `project` | string \| null | Project slug | | ||
| | `platform` | string \| null | Replay platform | | ||
| | `sdkName` | string \| null | Replay SDK name | | ||
| | `sdkVersion` | string \| null | Replay SDK version | | ||
| | `replayType` | string \| null | Replay type | | ||
| | `startedAt` | string \| null | Replay start time | | ||
| | `durationSeconds` | number \| null | Replay duration in seconds | | ||
| | `entryUrl` | string \| null | First replay URL | | ||
| | `exitUrl` | string \| null | Last replay URL | | ||
| | `focusPath` | string \| null | Optional route path used to focus the summary | | ||
| | `counts` | object | Normalized event counts | | ||
| | `recording` | object | Downloaded recording and parser stats | | ||
| | `timings` | object | Key timing observations | | ||
| | `routes` | array | Route timeline | | ||
| | `signals` | array | Detected non-error and error friction signals | | ||
| | `notableEvents` | array | Representative events useful for agent narrative | | ||
|
|
||
| **Examples:** | ||
|
|
||
| ```bash | ||
| # Summarize route flow, event counts, timings, and friction signals | ||
| sentry replay summarize my-org/346789a703f6454384f1de473b8b9fcc --json | ||
|
|
||
| # Focus the summary on a particular route path | ||
| sentry replay summarize my-org/346789a703f6454384f1de473b8b9fcc \ | ||
| --path /signup --json | ||
| ``` | ||
|
|
||
| ### `sentry replay view <replay-id-or-url...>` | ||
|
|
||
| View a Session Replay | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,13 +127,20 @@ function isStandaloneCommand(route: RouteInfo): boolean { | |
|
|
||
| /** | ||
| * Get subcommand names for a route group (e.g., "list, view, create"). | ||
| * Extracts the last path segment from each command's path. | ||
| * Preserves nested subcommands as "parent child" so route groups do not | ||
| * collapse multiple commands to the same final segment. | ||
| */ | ||
| function getSubcommandNames(route: RouteInfo): string[] { | ||
| return route.commands.map((cmd) => { | ||
| const parts = cmd.path.split(" "); | ||
| return parts.at(-1) ?? route.name; | ||
| }); | ||
| const prefix = `sentry ${route.name} `; | ||
| return [ | ||
| ...new Set( | ||
| route.commands.map((cmd) => | ||
| cmd.path.startsWith(prefix) | ||
| ? cmd.path.slice(prefix.length) | ||
| : (cmd.path.split(" ").at(-1) ?? route.name) | ||
| ) | ||
| ), | ||
| ]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** | ||
| * sentry replay event | ||
| * | ||
| * Inspect normalized events from Session Replay recordings. | ||
| */ | ||
|
|
||
| import { buildRouteMap } from "../../../lib/route-map.js"; | ||
| import { listCommand } from "./list.js"; | ||
|
|
||
| export const eventRoute = buildRouteMap({ | ||
| routes: { | ||
| list: listCommand, | ||
| }, | ||
| defaultCommand: "list", | ||
| docs: { | ||
| brief: "Inspect normalized replay events", | ||
| fullDescription: | ||
| "Inspect normalized events extracted from Session Replay recordings.\n\n" + | ||
| "Commands:\n" + | ||
| " list List normalized replay events\n\n" + | ||
| "Alias: `sentry replay events` → `sentry replay event list`", | ||
| hideRoute: {}, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our
--jsonmode should default to JSONL in this case?