@agent-native/core-corpus 0.204.1-nightly-20261008153353 → 0.205.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 (103) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +79 -0
  3. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +6 -4
  4. package/corpus/templates/analytics/CHANGELOG.md +30 -0
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +211 -0
  6. package/corpus/templates/analytics/actions/upload-journey-frame.ts +88 -0
  7. package/corpus/templates/analytics/app/i18n/en-US.ts +4 -0
  8. package/corpus/templates/analytics/app/pages/sessions/SessionReplayFrame.tsx +192 -0
  9. package/corpus/templates/analytics/app/pages/sessions/SessionReplayStoryboardExportDialog.tsx +86 -40
  10. package/corpus/templates/analytics/app/pages/sessions/session-replay-frame.ts +92 -0
  11. package/corpus/templates/analytics/app/pages/sessions/session-replay-screenshot.ts +0 -207
  12. package/corpus/templates/analytics/app/root.tsx +8 -1
  13. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +9 -0
  14. package/corpus/templates/analytics/changelog/2026-10-07-session-storyboard-screenshots-use-private-upload-storage.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-10-08-agents-can-read-the-onboarding-journey-tree-and-render-its-frames.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-08-analytics-now-reports-uncertain-storyboard-saves-without-imp.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-10-08-replay-screenshot-exports-now-report-upload-timeouts-and-inc.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-10-08-session-replay-screenshots-capture-directly-without-requesti.md +6 -0
  19. package/corpus/templates/analytics/changelog/2026-10-08-storyboard-exports-now-ask-you-to-check-design-when-analytic.md +6 -0
  20. package/corpus/templates/analytics/package.json +1 -0
  21. package/corpus/templates/analytics/scripts/journey-capture-plan.ts +380 -0
  22. package/corpus/templates/analytics/scripts/journey-capture.ts +712 -0
  23. package/corpus/templates/analytics/server/lib/analytics-mcp.ts +1 -1
  24. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +117 -0
  25. package/corpus/templates/analytics/server/lib/journey-steps.ts +182 -0
  26. package/corpus/templates/analytics/server/lib/journey-tree.ts +384 -0
  27. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +385 -0
  28. package/corpus/templates/analytics/server/lib/session-friction.ts +213 -51
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +261 -8
  30. package/corpus/templates/analytics/server/routes/api/session-replay/storyboard.post.ts +651 -187
  31. package/corpus/templates/analytics/shared/journey-path.ts +24 -0
  32. package/corpus/templates/analytics/shared/png.ts +38 -0
  33. package/corpus/templates/clips/CHANGELOG.md +23 -0
  34. package/corpus/templates/clips/actions/burn-recording-redactions.ts +1 -0
  35. package/corpus/templates/clips/actions/cleanup-transcript.ts +5 -1
  36. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +240 -49
  37. package/corpus/templates/clips/app/components/editor/recording-editor-boundary.tsx +11 -0
  38. package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
  39. package/corpus/templates/clips/app/lib/editor-save-status.ts +205 -0
  40. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +8 -19
  41. package/corpus/templates/clips/changelog/2026-10-08-skip-first-run-setup.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-10-08-the-video-editor-confirms-when-edits-save-to-a-clip-and-prov.md +6 -0
  43. package/corpus/templates/clips/server/plugins/onboarding.ts +1 -1
  44. package/corpus/templates/content/CHANGELOG.md +21 -0
  45. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +154 -53
  46. package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +7 -1
  47. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +422 -168
  48. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +130 -3
  49. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +68 -63
  50. package/corpus/templates/content/app/components/editor/document-editor-layout.ts +10 -3
  51. package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +5 -4
  52. package/corpus/templates/content/app/components/layout/Layout.tsx +125 -103
  53. package/corpus/templates/content/app/components/layout/content-layout.ts +228 -0
  54. package/corpus/templates/content/app/components/layout/sidebar-preferences.ts +1 -12
  55. package/corpus/templates/content/app/components/layout/use-content-layout.ts +290 -0
  56. package/corpus/templates/content/app/hooks/use-element-width-value.ts +49 -0
  57. package/corpus/templates/content/app/root.tsx +1 -1
  58. package/corpus/templates/content/changelog/2026-10-02-the-sidebar-comments-and-agent-panel-share-the-window-the-si.md +6 -0
  59. package/corpus/templates/content/changelog/2026-10-05-on-phones-a-collection-s-sort-and-filter-chips-stay-on-one-l.md +6 -0
  60. package/corpus/templates/content/changelog/2026-10-05-sharing-and-focus-remain-available-when-page-toolbar-folds.md +6 -0
  61. package/corpus/templates/design/.agents/skills/journey-storyboards/SKILL.md +48 -0
  62. package/corpus/templates/design/AGENTS.md +2 -0
  63. package/corpus/templates/design/CHANGELOG.md +30 -0
  64. package/corpus/templates/design/actions/add-session-replay-screenshots-to-board.ts +239 -69
  65. package/corpus/templates/design/actions/create-journey-canvas.ts +598 -0
  66. package/corpus/templates/design/actions/update-screen-source.ts +3 -1
  67. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +39 -2
  68. package/corpus/templates/design/app/components/design/LayersPanel.tsx +9 -12
  69. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +50 -10
  70. package/corpus/templates/design/app/i18n/en-US.ts +13 -1
  71. package/corpus/templates/design/app/pages/Index.tsx +2 -2
  72. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +9 -12
  73. package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +12 -7
  74. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-history.ts +4 -1
  75. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-source-and-sync.ts +18 -0
  76. package/corpus/templates/design/app/pages/design-editor/left-sidebar-width.ts +51 -0
  77. package/corpus/templates/design/app/pages/design-editor/view/export-submenu-content.tsx +93 -0
  78. package/corpus/templates/design/app/pages/design-editor/view/left-sidebar.tsx +15 -1
  79. package/corpus/templates/design/app/pages/design-editor/view/project-menu.tsx +11 -79
  80. package/corpus/templates/design/app/pages/design-editor/view/project-title-control.tsx +253 -43
  81. package/corpus/templates/design/changelog/2026-10-07-design-storyboard-screenshots-support-encrypted-upload.md +6 -0
  82. package/corpus/templates/design/changelog/2026-10-07-layers-panel-rows-are-now-32px-tall-with-a-24px-indent-per-l.md +6 -0
  83. package/corpus/templates/design/changelog/2026-10-08-agents-can-draw-an-onboarding-journey-storyboard-on-a-design.md +6 -0
  84. package/corpus/templates/design/changelog/2026-10-08-click-a-design-s-name-in-the-editor-to-open-a-file-menu-with.md +6 -0
  85. package/corpus/templates/design/changelog/2026-10-08-design-now-reports-pending-screenshot-cleanup-and-uncertain-.md +6 -0
  86. package/corpus/templates/design/changelog/2026-10-08-failed-screenshot-exports-report-when-temporary-image-cleanu.md +6 -0
  87. package/corpus/templates/design/e2e/chrome-geometry.reference.ts +5 -4
  88. package/corpus/templates/design/server/lib/replay-screenshot-blobs.ts +169 -0
  89. package/corpus/templates/design/server/lib/visual-edit-snapshot-blobs.ts +48 -6
  90. package/corpus/templates/design/server/routes/api/design-board-replay-screenshots/[screenshotId].get.ts +11 -2
  91. package/corpus/templates/design/server/routes/api/session-replay-storyboard.post.ts +669 -0
  92. package/corpus/templates/design/shared/journey-canvas.ts +841 -0
  93. package/corpus/templates/design/shared/journey-layout.ts +366 -0
  94. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +27 -2
  95. package/corpus/templates/mail/server/lib/automation-model.ts +18 -3
  96. package/corpus/templates/slides/CHANGELOG.md +24 -0
  97. package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -1
  98. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
  99. package/corpus/templates/slides/app/components/ui/context-menu.tsx +31 -0
  100. package/corpus/templates/slides/changelog/2026-10-08-image-uploads-work-reliably-in-slides.md +6 -0
  101. package/corpus/templates/slides/changelog/2026-10-08-right-click-menus-on-slides-open-instantly-without-the-brows.md +6 -0
  102. package/corpus/templates/slides/server/handlers/assets.ts +86 -4
  103. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 11511
35
+ - template files: 11555
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: onboarding-journeys
3
+ description: >-
4
+ Build the onboarding journey tree (signup to first output, with % splits,
5
+ drop-off per step, and example replays) and render a screenshot per step. Use
6
+ when asked for an onboarding storyboard, where new users drop off, or frames
7
+ for each onboarding step.
8
+ scope: dev
9
+ ---
10
+
11
+ # Onboarding Journeys
12
+
13
+ The pipeline runs without a UI. Codex does all four steps from the terminal:
14
+
15
+ 1. Call `get-onboarding-journey` as an MCP tool of the deployed Analytics app.
16
+ Save the JSON result as `tree.json`.
17
+ 2. Run the capture CLI from the agent-native checkout:
18
+ `pnpm --filter analytics journey:capture --tree tree.json --out ./frames`
19
+ (`--per-node 2 --concurrency 3`, `--min-aspect` / `--max-aspect`, `--dry-run`
20
+ to print the plan, `--upload` for private `attachmentRef`s).
21
+ 3. Call Design's `create-journey-canvas` with
22
+ `{ title, tree, frames: [{ nodeKey, exampleIndex, imageUrl?, attachmentRef?, width, height, capturedAt }], designId? }`
23
+ -> `{ designId, url, nodeCount, frameCount }`. Build `frames` from
24
+ `frames/manifest.json`.
25
+ 4. Lay out and review the canvas.
26
+
27
+ ## The tree
28
+
29
+ `get-onboarding-journey` returns `JourneyTree`; `format: "summary"` returns the
30
+ same counts as an indented `outline` with no examples. Use it first to choose a
31
+ window, `app`, `maxDepth`, `minNodeSessions` (small branches merge into an
32
+ `other` node), and `maxNodes`.
33
+
34
+ ```ts
35
+ type JourneyExample = { sessionId: string; recordingId: string | null; ts: string; offsetMs: number | null; viewport: { width: number; height: number } | null; viewportReason?: string; replayUrl?: string };
36
+ type JourneyNode = { key: string; label: string; parentKey: string | null; depth: number; kind: "step" | "other"; n: number; pctOfRoot: number; pctOfParent: number; dropoffN: number; dropoffPct: number; examples: JourneyExample[] };
37
+ type JourneyTree = { window: { from: string; to: string }; app: string; rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[]; notes?: string[] };
38
+ ```
39
+
40
+ - Nodes come parents first. `key` is the path of step keys joined with ` > `;
41
+ `pctOf*` are percents (0-100).
42
+ - `dropoffN` is sessions whose last observed step is that node. It is not a
43
+ confirmed exit: a blocked tracker or an event outside the window reads the
44
+ same. A node at `maxDepth` where `n - dropoffN - sum(children n)` is above
45
+ zero has sessions that carried on.
46
+ - A session is an analytics session id. One that began before `dateFrom`
47
+ starts mid-journey, so start the window a day early.
48
+ - Read `coverage` before using the numbers. `truncated: true` means the event
49
+ read hit `maxEventRows` or the node list hit `maxNodes`; `notes` says which.
50
+ Never report a truncated tree as the whole window.
51
+ - An example with `recordingId: null` has no replay the caller can open, and
52
+ `viewportReason` says why the viewport is unknown (`no_recording`,
53
+ `not_captured` for recordings before viewport capture, `unreadable`).
54
+ - Test identities are always excluded; `emailFilter` defaults to
55
+ `exclude_builder`, as in the onboarding metrics. A session with any excluded
56
+ identity is dropped whole.
57
+
58
+ ## Capture
59
+
60
+ The CLI mints a two-hour tokenized replay link per recording with
61
+ `create-session-replay-agent-link`, opens each recording once in headless
62
+ Chromium at `/sessions/:id?frame=1&agent_access=...`, seeks to each `offsetMs`,
63
+ and uses the Sessions screenshot compositor. It writes `<nodeKey>-<n>.png` and
64
+ `manifest.json` with `frames`, `failures` (explicit, with a reason), and
65
+ `skipped`. Fix failures or report them; do not paint over a missing frame.
66
+
67
+ Authenticate to the deployed app, never a local database: run
68
+ `npx -y @agent-native/core@latest connect https://analytics.agent-native.com --client codex`
69
+ (the CLI reads the bearer it writes to `~/.codex/config.toml`), or pass
70
+ `--token` / set `AGENT_NATIVE_TOKEN`. `--app-url` overrides the app. Frame mode
71
+ needs a deployment that includes `/sessions/:id?frame=1`.
72
+
73
+ Frame mode renders without a login; the signed link is the only credential. It
74
+ masks and scrubs exactly as the Sessions viewer does. Do not add viewport
75
+ clamping or pointer projection to it (see `session-replay`).
76
+
77
+ Viewport comes from the recording's first rrweb Meta event, stored by replay
78
+ ingest in `session_recordings.metadata.viewport` (`first` and `last`). Older
79
+ recordings have none.
@@ -154,10 +154,12 @@ agent answers about browser recordings in the Analytics template.
154
154
  clients send no id and are skipped: followed together, two tabs would look
155
155
  like one tab going back.
156
156
  - A replay row counts only while `processed_chunks` equals the recording's
157
- `chunk_count`. Each batch must continue from the stored detector state and
158
- its chunk seqs must start exactly at `processed_chunks`, so a batch that
159
- cannot be measured, or arrives out of order, leaves the row behind and the
160
- recording reads as unmeasured; never restart from fresh state. An event row
157
+ `chunk_count`. Each batch continues from the stored detector state, whose
158
+ `lastSeq` is the highest chunk measured; a seq the recorder lost is skipped.
159
+ A batch that cannot be measured, or a chunk below `lastSeq`, leaves the row
160
+ behind and the recording reads as unmeasured until it ends. Then
161
+ `remeasureReplayFriction` measures every stored chunk again in seq order;
162
+ never continue a row that fell behind from fresh state. An event row
161
163
  counts only when the tenant's friction coverage began before every
162
164
  recording of the session and the session has neither an event index gap
163
165
  nor a friction gap (`analytics_session_friction_gaps`). A friction write
@@ -3,14 +3,44 @@
3
3
  All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-08
7
+
8
+ ### Added
9
+
10
+ - Agents can build the onboarding journey tree with drop-off per step and render a screenshot of each step from session replays.
11
+
12
+ ### Improved
13
+
14
+ - Session replay screenshots capture directly without requesting screen sharing.
15
+
16
+ ### Fixed
17
+
18
+ - Analytics now reports uncertain storyboard saves without implying the export definitely failed.
19
+ - Replay screenshot exports now report upload timeouts and incomplete cleanup.
20
+ - Storyboard exports now ask you to check Design when Analytics cannot confirm a save.
21
+
6
22
  ## 2026-10-07
7
23
 
8
24
  ### Added
9
25
 
26
+ - Selected session replays can be captured at exact timestamps and sent together to a private Design storyboard.
10
27
  - Session replay screenshots can be copied directly into Design.
11
28
 
29
+ ### Improved
30
+
31
+ - Analytics chat follows short follow-ups like "what about EMEA?" and recovers from an unknown BigQuery column in one step
32
+ - Thumbs-down on a chat answer now offers quick reasons and a Copy details button that copies the run id and thread link for a report
33
+
12
34
  ### Fixed
13
35
 
36
+ - Analytics now treats chart filter changes as data requests and reports when reference catalogs are incomplete.
37
+ - Analytics chat no longer stays on "running" after the agent has finished; the thread reloads so the change is visible without asking again
38
+ - Analytics chat stops retrying a data source that is not connected and says who can connect it
39
+ - Analytics keeps S3 replay setup available when storage status cannot be checked.
40
+ - Automatic learning captures stay private to the user, and shared learning updates require approval.
41
+ - Builder storage setup now waits for renewed file access before confirming it is ready.
42
+ - Personal Analytics memories stay limited to unscoped conversations, keeping organization-specific guidance from following users between organizations.
43
+ - Session replay storyboard screenshots upload directly to Design's private storage, so Analytics does not need its own blob storage provider.
14
44
  - Session replay screenshot export captures readable video frames and safe poster images.
15
45
 
16
46
  ## 2026-10-06
@@ -0,0 +1,211 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ FIRST_PARTY_TEMPLATE_NAMES,
10
+ isCalendarDate,
11
+ } from "../server/lib/first-party-metric-catalog.js";
12
+ import {
13
+ getOnboardingJourney,
14
+ JourneyRecordingsError,
15
+ } from "../server/lib/onboarding-journey.js";
16
+
17
+ const MAX_WINDOW_DAYS = 90;
18
+
19
+ function resolveScope() {
20
+ const userEmail = getRequestUserEmail();
21
+ if (!userEmail)
22
+ fail("Sign in to use this action.", {
23
+ errorCode: "unauthenticated",
24
+ statusCode: 401,
25
+ });
26
+ return { userEmail, orgId: getRequestOrgId() || null };
27
+ }
28
+
29
+ const isoDate = z
30
+ .string()
31
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
32
+ .refine(isCalendarDate, { message: "must be a real calendar date" });
33
+
34
+ export default defineAction({
35
+ description:
36
+ 'Build the onboarding journey tree for a date window: per-session step sequences (signup, onboarding questions, setup method, app entry, first action, first output) folded into a prefix tree. format "tree" returns { window, app, rootN, coverage, nodes }: each node has n, pctOfRoot, pctOfParent, dropoffN/dropoffPct (sessions whose last observed step is that node, not a confirmed exit) and example sessions with recordingId, offsetMs to seek to, and the recording\'s viewport. format "summary" returns the same counts as an indented text outline with no examples. Check coverage.truncated first: true means counts are a partial sample or the node list was cut (see notes). Use it to plan onboarding storyboards or find where new users stop; feed the tree to the journey:capture CLI to render frames.',
37
+ schema: z.object({
38
+ dateFrom: isoDate.describe(
39
+ "Inclusive UTC start date, YYYY-MM-DD. Sessions that began earlier appear mid-journey, so start a day before the period you care about.",
40
+ ),
41
+ dateTo: isoDate.describe(
42
+ `Inclusive UTC end date, YYYY-MM-DD, at most ${MAX_WINDOW_DAYS} days after dateFrom. Events are windowed before any join.`,
43
+ ),
44
+ app: z
45
+ .enum(["all", ...FIRST_PARTY_TEMPLATE_NAMES])
46
+ .optional()
47
+ .default("all")
48
+ .describe(
49
+ 'Template filter: "all" or one first-party template such as clips, design, or slides. Defaults to "all".',
50
+ ),
51
+ emailFilter: z
52
+ .enum(["all", "exclude_builder", "only_builder"])
53
+ .optional()
54
+ .default("exclude_builder")
55
+ .describe(
56
+ 'Builder-employee scope, as in the onboarding metrics: "exclude_builder" (default), "only_builder", or "all". Test identities are always excluded.',
57
+ ),
58
+ format: z
59
+ .enum(["tree", "summary"])
60
+ .optional()
61
+ .default("tree")
62
+ .describe(
63
+ 'Output shape: "tree" (default) with example sessions, or "summary", an indented text outline of counts and drop-off with no examples.',
64
+ ),
65
+ maxNodes: z.coerce
66
+ .number()
67
+ .int()
68
+ .min(1)
69
+ .max(500)
70
+ .optional()
71
+ .default(60)
72
+ .describe(
73
+ "Most nodes returned, largest first. When the tree has more, coverage.truncated is true and notes says how many were cut. Defaults to 60.",
74
+ ),
75
+ maxDepth: z.coerce
76
+ .number()
77
+ .int()
78
+ .min(1)
79
+ .max(20)
80
+ .optional()
81
+ .default(8)
82
+ .describe(
83
+ "Steps kept per session before sessions are folded into the deeperN count of the node at this depth. Defaults to 8.",
84
+ ),
85
+ minNodeSessions: z.coerce
86
+ .number()
87
+ .int()
88
+ .min(1)
89
+ .optional()
90
+ .default(1)
91
+ .describe(
92
+ 'Branches with fewer sessions than this merge into one "other" child per parent. Defaults to 1 (nothing merges).',
93
+ ),
94
+ examplesPerNode: z.coerce
95
+ .number()
96
+ .int()
97
+ .min(1)
98
+ .max(10)
99
+ .optional()
100
+ .default(3)
101
+ .describe("Example sessions returned per node. Defaults to 3."),
102
+ maxEventRows: z.coerce
103
+ .number()
104
+ .int()
105
+ .min(1000)
106
+ .max(200000)
107
+ .optional()
108
+ .default(40000)
109
+ .describe(
110
+ "Event rows to read before stopping and reporting coverage.truncated. Each 4,000 rows is one query; raise it only for a wide window. Defaults to 40000.",
111
+ ),
112
+ recency: z
113
+ .enum(["newest", "none"])
114
+ .optional()
115
+ .default("newest")
116
+ .describe(
117
+ 'Example order after replay availability: "newest" (default) prefers recent sessions, "none" orders by session id.',
118
+ ),
119
+ settleMs: z.coerce
120
+ .number()
121
+ .int()
122
+ .min(0)
123
+ .max(5000)
124
+ .optional()
125
+ .default(300)
126
+ .describe(
127
+ "Added to each example's offsetMs so the frame has rendered. Defaults to 300.",
128
+ ),
129
+ minAspect: z.coerce
130
+ .number()
131
+ .positive()
132
+ .optional()
133
+ .describe("Only examples whose viewport width/height is at least this."),
134
+ maxAspect: z.coerce
135
+ .number()
136
+ .positive()
137
+ .optional()
138
+ .describe("Only examples whose viewport width/height is at most this."),
139
+ minWidth: z.coerce
140
+ .number()
141
+ .int()
142
+ .positive()
143
+ .optional()
144
+ .describe("Only examples whose viewport is at least this many px wide."),
145
+ maxWidth: z.coerce
146
+ .number()
147
+ .int()
148
+ .positive()
149
+ .optional()
150
+ .describe("Only examples whose viewport is at most this many px wide."),
151
+ requireKnownViewport: z
152
+ .boolean()
153
+ .optional()
154
+ .describe(
155
+ "Leave out examples whose viewport was not recorded or cannot be read. Without it they are kept with viewport null and a viewportReason.",
156
+ ),
157
+ }),
158
+ http: { method: "GET" },
159
+ readOnly: true,
160
+ mcpTool: true,
161
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
162
+ grounding: true,
163
+ run: async (args) => {
164
+ const days =
165
+ (Date.parse(`${args.dateTo}T00:00:00Z`) -
166
+ Date.parse(`${args.dateFrom}T00:00:00Z`)) /
167
+ 86_400_000;
168
+ if (days < 0) fail("dateTo must not be before dateFrom.");
169
+ if (days > MAX_WINDOW_DAYS) {
170
+ fail(`The window may span at most ${MAX_WINDOW_DAYS} days.`);
171
+ }
172
+ const viewport = {
173
+ ...(args.minAspect !== undefined ? { minAspect: args.minAspect } : {}),
174
+ ...(args.maxAspect !== undefined ? { maxAspect: args.maxAspect } : {}),
175
+ ...(args.minWidth !== undefined ? { minWidth: args.minWidth } : {}),
176
+ ...(args.maxWidth !== undefined ? { maxWidth: args.maxWidth } : {}),
177
+ ...(args.requireKnownViewport ? { requireKnown: true } : {}),
178
+ };
179
+ const scope = resolveScope();
180
+ try {
181
+ return await getOnboardingJourney(scope, {
182
+ dateFrom: args.dateFrom,
183
+ dateTo: args.dateTo,
184
+ app: args.app,
185
+ emailFilter: args.emailFilter,
186
+ format: args.format,
187
+ maxNodes: args.maxNodes,
188
+ maxDepth: args.maxDepth,
189
+ minNodeSessions: args.minNodeSessions,
190
+ examplesPerNode: args.examplesPerNode,
191
+ maxEventRows: args.maxEventRows,
192
+ settleMs: args.settleMs,
193
+ recency: args.recency,
194
+ viewport: Object.keys(viewport).length ? viewport : undefined,
195
+ });
196
+ } catch (error) {
197
+ // Causes can quote database details; the server log keeps them.
198
+ console.error("[get-onboarding-journey] failed", error);
199
+ if (error instanceof JourneyRecordingsError) {
200
+ fail(
201
+ `${error.message} Examples would misreport which sessions have a replay, so no tree was built; retry, or use format "summary" for counts only.`,
202
+ { errorCode: "journey_recordings_unreadable", statusCode: 502 },
203
+ );
204
+ }
205
+ fail(
206
+ "Onboarding journey events could not be read, so no tree was built. Retry with a narrower window or app.",
207
+ { errorCode: "journey_events_unreadable", statusCode: 502 },
208
+ );
209
+ }
210
+ },
211
+ });
@@ -0,0 +1,88 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import { mintAttachmentRef } from "@agent-native/core/private-blob";
3
+ import {
4
+ getRequestOrgId,
5
+ getRequestUserEmail,
6
+ } from "@agent-native/core/server";
7
+ import { z } from "zod";
8
+
9
+ import { getSessionReplaySummary } from "../server/lib/session-replay.js";
10
+ import { pngDimensions } from "../shared/png.js";
11
+
12
+ const MAX_PNG_BYTES = 3_500_000;
13
+
14
+ export default defineAction({
15
+ description:
16
+ "Store one onboarding journey frame (a PNG screenshot of a session replay) in the caller's private storage and return an opaque attachmentRef. Used by the journey:capture CLI with --upload; frames are never public and only the caller can open the ref. The caller must be able to open the recording.",
17
+ schema: z.object({
18
+ recordingId: z
19
+ .string()
20
+ .min(1)
21
+ .max(128)
22
+ .describe("The recording the frame shows"),
23
+ offsetMs: z.coerce
24
+ .number()
25
+ .int()
26
+ .min(0)
27
+ .describe("Where in the recording the frame was captured"),
28
+ png: z
29
+ .string()
30
+ .min(1)
31
+ .max(Math.ceil((MAX_PNG_BYTES * 4) / 3) + 8)
32
+ .describe(`Base64 PNG, at most ${MAX_PNG_BYTES} bytes decoded`),
33
+ }),
34
+ agentTool: false,
35
+ run: async (args) => {
36
+ const userEmail = getRequestUserEmail();
37
+ if (!userEmail) {
38
+ fail("Sign in to use this action.", {
39
+ errorCode: "unauthenticated",
40
+ statusCode: 401,
41
+ });
42
+ }
43
+ const orgId = getRequestOrgId() || null;
44
+ const recording = await getSessionReplaySummary(args.recordingId, {
45
+ userEmail,
46
+ orgId,
47
+ });
48
+ const data = Buffer.from(args.png, "base64");
49
+ if (data.byteLength === 0 || data.byteLength > MAX_PNG_BYTES) {
50
+ fail(`The frame must be a PNG of at most ${MAX_PNG_BYTES} bytes.`, {
51
+ errorCode: "journey_frame_invalid",
52
+ statusCode: 413,
53
+ });
54
+ }
55
+ const dimensions = pngDimensions(data);
56
+ if (!dimensions) {
57
+ fail("The frame is not a PNG of a plausible screenshot size.", {
58
+ errorCode: "journey_frame_invalid",
59
+ });
60
+ }
61
+ const minted = await mintAttachmentRef({
62
+ data,
63
+ filename: `${recording.id}-${String(args.offsetMs).padStart(8, "0")}.png`,
64
+ mimeType: "image/png",
65
+ ownerEmail: userEmail,
66
+ orgId: null,
67
+ metadata: {
68
+ appId: "analytics",
69
+ resourceType: "journey-frame",
70
+ resourceId: recording.id,
71
+ replayId: recording.id,
72
+ offsetMs: args.offsetMs,
73
+ },
74
+ });
75
+ if (minted.status !== "ok") {
76
+ fail(
77
+ "Private frame storage is not available for Analytics, so the frame was not stored.",
78
+ { errorCode: "private_storage_unavailable", statusCode: 503 },
79
+ );
80
+ }
81
+ return {
82
+ attachmentRef: minted.ref,
83
+ width: dimensions.width,
84
+ height: dimensions.height,
85
+ bytes: data.byteLength,
86
+ };
87
+ },
88
+ });
@@ -1698,8 +1698,12 @@ export default {
1698
1698
  storyboardNoDesignResponse: "Design returned no storyboard result.",
1699
1699
  storyboardTemporaryCleanupPending:
1700
1700
  "The storyboard was saved, but temporary screenshot files could not be removed.",
1701
+ storyboardTemporaryCleanupFailed:
1702
+ "Temporary screenshot cleanup is still pending.",
1701
1703
  storyboardUnexpectedResponse:
1702
1704
  "The screenshot export returned an unreadable response. Retry the export.",
1705
+ storyboardSaveOutcomeUnknown:
1706
+ "Design may have saved the storyboard. Check Design before retrying.",
1703
1707
  openStoryboard: "Open storyboard in Design",
1704
1708
  cancelStoryboardCapture: "Cancel capture",
1705
1709
  captureToDesign: "Capture and add to Design",
@@ -0,0 +1,192 @@
1
+ import { isScreenshotSize } from "@shared/png";
2
+ import { SESSION_REPLAY_AGENT_ACCESS_PARAM } from "@shared/session-replay-agent-access";
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ import {
6
+ blobToBase64,
7
+ replayFrameFailureReason,
8
+ replayFramePath,
9
+ type ReplayFrameCapture,
10
+ } from "./session-replay-frame";
11
+ import { captureReplayScreenshot } from "./session-replay-screenshot";
12
+ import {
13
+ buildReplayViewportTimeline,
14
+ fetchSessionReplayPlayback,
15
+ normalizeReplayEvents,
16
+ REPLAY_OVERLAY_STYLE_RULES,
17
+ replayAvailabilityErrorKey,
18
+ replayInitialViewportDimensions,
19
+ replayRouteAtOffset,
20
+ replayViewportDimensionsAtTime,
21
+ } from "./SessionDetailPage";
22
+
23
+ function nextPaint(): Promise<void> {
24
+ return new Promise((resolve) =>
25
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
26
+ );
27
+ }
28
+
29
+ export default function SessionReplayFrame({
30
+ recordingId,
31
+ }: {
32
+ recordingId: string;
33
+ }) {
34
+ const stageRef = useRef<HTMLDivElement>(null);
35
+ const stageRootRef = useRef<HTMLDivElement>(null);
36
+ const [status, setStatus] = useState<"loading" | "ready" | "error">(
37
+ "loading",
38
+ );
39
+
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ let replayer: any = null;
43
+ window.__anReplayFrame = { status: "loading" };
44
+
45
+ const fail = (error: unknown) => {
46
+ if (cancelled) return;
47
+ window.__anReplayFrame = {
48
+ status: "error",
49
+ reason: replayFrameFailureReason(error),
50
+ };
51
+ setStatus("error");
52
+ };
53
+
54
+ async function boot() {
55
+ const stage = stageRef.current;
56
+ const stageRoot = stageRootRef.current;
57
+ if (!stage || !stageRoot) throw new Error("frame_not_mounted");
58
+ const agentAccessToken =
59
+ new URLSearchParams(window.location.search).get(
60
+ SESSION_REPLAY_AGENT_ACCESS_PARAM,
61
+ ) ?? "";
62
+ const playback = await fetchSessionReplayPlayback(recordingId, {
63
+ agentAccessToken,
64
+ });
65
+ if (
66
+ !playback.isComplete ||
67
+ playback.unavailableChunks > 0 ||
68
+ playback.chunks.some((chunk) => chunk.unavailable)
69
+ ) {
70
+ throw new Error("replay_incomplete");
71
+ }
72
+ const events = normalizeReplayEvents(
73
+ playback.chunks.flatMap((chunk) => chunk.events),
74
+ );
75
+ const unavailable = replayAvailabilityErrorKey(events);
76
+ if (unavailable) throw new Error(unavailable);
77
+ const initial = replayInitialViewportDimensions(events);
78
+ if (!initial) throw new Error("viewport_unavailable");
79
+ const timeline = buildReplayViewportTimeline(events);
80
+ // Sizes come from the recorded events, so one the browser could not
81
+ // render is refused before any surface is allocated for it.
82
+ if (
83
+ !timeline.every((change) =>
84
+ isScreenshotSize(change.width, change.height),
85
+ )
86
+ ) {
87
+ throw new Error("viewport_out_of_range");
88
+ }
89
+
90
+ await import("@rrweb/replay/dist/style.css");
91
+ const { Replayer } = await import("@rrweb/replay");
92
+ if (cancelled) return;
93
+
94
+ const sizeStage = (width: number, height: number) => {
95
+ stageRoot.style.width = `${width}px`;
96
+ stageRoot.style.height = `${height}px`;
97
+ stageRoot.style.transform = "none";
98
+ stageRoot.style.setProperty("--an-replay-cursor-scale", "1");
99
+ };
100
+ sizeStage(initial.width, initial.height);
101
+ replayer = new Replayer(events as any[], {
102
+ root: stageRoot,
103
+ speed: 1,
104
+ skipInactive: false,
105
+ showWarning: false,
106
+ showDebug: false,
107
+ mouseTail: false,
108
+ triggerFocus: true,
109
+ insertStyleRules: REPLAY_OVERLAY_STYLE_RULES,
110
+ });
111
+ replayer.iframe?.setAttribute?.("referrerpolicy", "no-referrer");
112
+ const totalTimeMs = Number(replayer.getMetaData?.().totalTime ?? 0);
113
+ try {
114
+ replayer.play?.(0);
115
+ } catch {
116
+ // rrweb throws when the first event cannot start playback; a pause at
117
+ // 0 still builds the first snapshot, which is all a seek needs.
118
+ replayer.pause?.(0);
119
+ }
120
+ await nextPaint();
121
+
122
+ let queue: Promise<unknown> = Promise.resolve();
123
+ const captureOne = async (
124
+ offsetMs: number,
125
+ ): Promise<ReplayFrameCapture> => {
126
+ if (!Number.isFinite(offsetMs) || offsetMs < 0) {
127
+ throw new Error("offset_invalid");
128
+ }
129
+ if (offsetMs > totalTimeMs) throw new Error("offset_out_of_range");
130
+ const dimensions =
131
+ replayViewportDimensionsAtTime(timeline, offsetMs) ?? initial;
132
+ replayer.pause(offsetMs);
133
+ replayer.handleResize?.(dimensions);
134
+ sizeStage(dimensions.width, dimensions.height);
135
+ await nextPaint();
136
+ const iframe = replayer.iframe as HTMLIFrameElement | undefined;
137
+ if (!iframe) throw new Error("replay_frame_missing");
138
+ const blob = await captureReplayScreenshot(stage, stageRoot, iframe);
139
+ return {
140
+ offsetMs,
141
+ width: dimensions.width,
142
+ height: dimensions.height,
143
+ route: replayFramePath(replayRouteAtOffset(events, offsetMs)),
144
+ capturedAt: new Date().toISOString(),
145
+ png: await blobToBase64(blob),
146
+ };
147
+ };
148
+
149
+ window.__anReplayFrame = {
150
+ status: "ready",
151
+ recordingId,
152
+ totalTimeMs,
153
+ eventCount: playback.recording.eventCount,
154
+ // One seek at a time: rrweb has a single playhead.
155
+ capture: (offsetMs) => {
156
+ const run = queue.then(() => captureOne(offsetMs));
157
+ queue = run.catch(() => undefined);
158
+ return run;
159
+ },
160
+ };
161
+ setStatus("ready");
162
+ }
163
+
164
+ boot().catch(fail);
165
+ return () => {
166
+ cancelled = true;
167
+ try {
168
+ replayer?.pause?.();
169
+ replayer?.destroy?.();
170
+ // coercion-ok: teardown of a player the page is leaving; nothing can act on a failure here.
171
+ } catch {
172
+ // Nothing to report: the page is going away.
173
+ }
174
+ window.__anReplayFrame = undefined;
175
+ };
176
+ }, [recordingId]);
177
+
178
+ return (
179
+ <div
180
+ ref={stageRef}
181
+ data-replay-frame={status}
182
+ // guard:allow-raw-color — a recording with no page background renders on the browser's default white, and the frame must match it in every theme
183
+ style={{ position: "fixed", left: 0, top: 0, background: "#fff" }}
184
+ >
185
+ <div
186
+ ref={stageRootRef}
187
+ className="an-replay-stage-root"
188
+ style={{ position: "relative", overflow: "hidden" }}
189
+ />
190
+ </div>
191
+ );
192
+ }