@agent-native/core-corpus 0.206.0 → 0.206.1-nightly-20261009213000

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 (165) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +8 -0
  3. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +55 -4
  4. package/corpus/templates/analytics/CHANGELOG.md +2 -0
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +9 -1
  6. package/corpus/templates/analytics/actions/update-dashboard.ts +57 -0
  7. package/corpus/templates/analytics/agent-native.json +8 -1
  8. package/corpus/templates/analytics/app/i18n/en-US.ts +1 -0
  9. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +101 -0
  10. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +17 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +25 -14
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  13. package/corpus/templates/analytics/changelog/2026-10-09-compare-onboarding-follow-up-across-first-party-apps.md +6 -0
  14. package/corpus/templates/analytics/changelog/2026-10-09-dashboards-can-filter-by-several-values-at-once-with-multi-s.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-10-09-journey-screenshots-account-for-legacy-clipping-when-checkin.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-09-journey-trees-remain-available-when-a-matching-recording-is-.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-10-09-onboarding-journeys-can-find-recordings-from-exact-replay-li.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-10-09-onboarding-journeys-count-saved-clips-and-completed-deck-gen.md +6 -0
  19. package/corpus/templates/analytics/changelog/2026-10-09-session-recordings-keep-their-full-history-and-performance-i.md +6 -0
  20. package/corpus/templates/analytics/scripts/journey-capture-iframe-audit.ts +110 -14
  21. package/corpus/templates/analytics/server/db/index.ts +19 -0
  22. package/corpus/templates/analytics/server/db/schema.ts +68 -35
  23. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +123 -9
  24. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +383 -2
  25. package/corpus/templates/analytics/server/lib/bigquery.ts +65 -1
  26. package/corpus/templates/analytics/server/lib/dashboard-report.ts +23 -3
  27. package/corpus/templates/analytics/server/lib/error-capture.ts +111 -5
  28. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +2 -0
  29. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -1
  30. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +338 -5
  31. package/corpus/templates/analytics/server/lib/journey-steps.ts +105 -9
  32. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +628 -6
  33. package/corpus/templates/analytics/server/lib/replay-link-timestamp.ts +53 -0
  34. package/corpus/templates/analytics/server/lib/session-event-index.ts +137 -16
  35. package/corpus/templates/analytics/server/lib/session-friction.ts +396 -96
  36. package/corpus/templates/analytics/server/lib/session-performance.ts +273 -51
  37. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +28 -0
  38. package/corpus/templates/analytics/server/lib/session-replay.ts +405 -102
  39. package/corpus/templates/analytics/server/plugins/db.ts +111 -49
  40. package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +20 -0
  41. package/corpus/templates/brain/actions/_source-config.ts +2 -1
  42. package/corpus/templates/brain/actions/list-connection-providers.ts +1 -1
  43. package/corpus/templates/brain/app/i18n-data.ts +10 -0
  44. package/corpus/templates/brain/app/routes/sources.tsx +22 -1
  45. package/corpus/templates/brain/changelog/2026-10-07-zoom-sources-can-import-ai-companion-summaries.md +6 -0
  46. package/corpus/templates/brain/server/lib/connectors.ts +142 -14
  47. package/corpus/templates/brain/server/lib/zoom.ts +126 -0
  48. package/corpus/templates/content/actions/_content-save-outcomes.ts +280 -0
  49. package/corpus/templates/content/actions/_document-edit-mutation.ts +91 -24
  50. package/corpus/templates/content/actions/create-document.ts +9 -3
  51. package/corpus/templates/content/actions/edit-document.ts +43 -2
  52. package/corpus/templates/content/actions/record-editor-mount.ts +55 -0
  53. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +6 -2
  54. package/corpus/templates/content/actions/update-document.ts +110 -14
  55. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +660 -273
  56. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +76 -44
  57. package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +1 -6
  58. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +344 -257
  59. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +1 -1
  60. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +60 -7
  61. package/corpus/templates/content/app/components/editor/editor-mount-telemetry.ts +76 -0
  62. package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +3 -2
  63. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -1
  64. package/corpus/templates/content/app/hooks/use-documents.ts +5 -1
  65. package/corpus/templates/content/app/i18n-data.ts +2 -0
  66. package/corpus/templates/content/app/lib/content-save-telemetry.ts +17 -0
  67. package/corpus/templates/content/changelog/2026-10-09-chatgpt-and-codex-widgets-show-the-document-s-top-bar-with-a.md +6 -0
  68. package/corpus/templates/content/e2e/helpers.ts +104 -14
  69. package/corpus/templates/content/e2e/save-lineage.ts +519 -0
  70. package/corpus/templates/content/parity/matrix.md +1 -1
  71. package/corpus/templates/content/parity/matrix.ts +5 -1
  72. package/corpus/templates/content/server/lib/chatgpt-directory-tools.ts +35 -1
  73. package/corpus/templates/content/shared/editor-mount-outcomes.ts +13 -0
  74. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +5 -6
  75. package/corpus/templates/design/.agents/skills/journey-storyboards/SKILL.md +17 -0
  76. package/corpus/templates/design/AGENTS.md +1 -1
  77. package/corpus/templates/design/actions/apply-component-prop-edit.ts +4 -0
  78. package/corpus/templates/design/actions/connect-localhost.ts +16 -0
  79. package/corpus/templates/design/actions/create-file.ts +346 -39
  80. package/corpus/templates/design/actions/create-journey-canvas.ts +2 -1
  81. package/corpus/templates/design/actions/delete-file.ts +56 -10
  82. package/corpus/templates/design/actions/generate-design.ts +70 -28
  83. package/corpus/templates/design/actions/open-visual-edit.ts +1 -2
  84. package/corpus/templates/design/actions/present-design-variants.ts +5 -16
  85. package/corpus/templates/design/actions/refresh-localhost-preview-token.ts +111 -80
  86. package/corpus/templates/design/actions/update-design.ts +561 -95
  87. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +58 -7
  88. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -1
  89. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +277 -222
  90. package/corpus/templates/design/app/components/design/TokensPanel.tsx +311 -269
  91. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbenchLoader.tsx +0 -7
  92. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +131 -163
  93. package/corpus/templates/design/app/components/design/editor/EditorTopBar.tsx +87 -7
  94. package/corpus/templates/design/app/components/design/editor/LeftPanelHeader.tsx +25 -0
  95. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +4 -1
  96. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +8 -0
  97. package/corpus/templates/design/app/global.css +1 -1
  98. package/corpus/templates/design/app/hooks/use-question-flow.ts +23 -7
  99. package/corpus/templates/design/app/i18n/en-US.ts +14 -3
  100. package/corpus/templates/design/app/lib/design-save-outbox.ts +203 -1
  101. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +31 -7
  102. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +2 -2
  103. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +29 -12
  104. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +36 -6
  105. package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +42 -22
  106. package/corpus/templates/design/app/pages/design-editor/domains/localhost-preview-recovery.ts +62 -0
  107. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-active-screen-and-geometry.ts +415 -39
  108. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-canvas-and-screens.ts +73 -4
  109. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-edit-commands.ts +13 -13
  110. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +34 -5
  111. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +84 -17
  112. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +18 -4
  113. package/corpus/templates/design/app/pages/design-editor/history.ts +8 -0
  114. package/corpus/templates/design/app/pages/design-editor/mcp-widget-write-capabilities.ts +16 -0
  115. package/corpus/templates/design/app/pages/design-editor/minimal-inspector.ts +23 -0
  116. package/corpus/templates/design/app/pages/design-editor/top-bar.ts +12 -1
  117. package/corpus/templates/design/app/pages/design-editor/types.ts +1 -1
  118. package/corpus/templates/design/app/pages/design-editor/view/editor-canvas-area.tsx +4 -6
  119. package/corpus/templates/design/app/pages/design-editor/view/left-sidebar.tsx +39 -58
  120. package/corpus/templates/design/app/pages/design-editor/view/project-menu.tsx +2 -4
  121. package/corpus/templates/design/app/pages/design-editor/view/project-title-control.tsx +52 -41
  122. package/corpus/templates/design/app/pages/design-editor/view/right-rail.tsx +30 -10
  123. package/corpus/templates/design/app/root.tsx +18 -7
  124. package/corpus/templates/design/changelog/2026-10-08-fixed-local-design-edit-and-preview-regressions.md +6 -0
  125. package/corpus/templates/design/changelog/2026-10-09-ads-and-social-posts-keep-their-exact-size-with-no-extra-mob.md +6 -0
  126. package/corpus/templates/design/changelog/2026-10-09-chatgpt-widgets-show-design-title-and-sharing-controls.md +6 -0
  127. package/corpus/templates/design/changelog/2026-10-09-design-in-chatgpt-opens-on-the-whole-canvas-with-every-scree.md +6 -0
  128. package/corpus/templates/design/changelog/2026-10-09-design-widgets-include-editor-controls.md +6 -0
  129. package/corpus/templates/design/changelog/2026-10-09-journey-recording-gaps.md +6 -0
  130. package/corpus/templates/design/changelog/2026-10-09-left-rail-is-now-a-56px-file-agents-and-tokens-rail-with-lab.md +6 -0
  131. package/corpus/templates/design/changelog/2026-10-09-widget-interact-controls-clear-header.md +6 -0
  132. package/corpus/templates/design/e2e/loopback-design-provider.ts +44 -0
  133. package/corpus/templates/design/e2e/sql-inline-bytes-scan.ts +328 -0
  134. package/corpus/templates/design/playwright.config.ts +1 -0
  135. package/corpus/templates/design/server/db/schema.ts +21 -0
  136. package/corpus/templates/design/server/lib/chatgpt-directory-tools.ts +51 -26
  137. package/corpus/templates/design/server/lib/localhost-connection.ts +9 -9
  138. package/corpus/templates/design/server/lib/screen-restore-claims.ts +29 -0
  139. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  140. package/corpus/templates/design/server/plugins/core-routes.ts +15 -3
  141. package/corpus/templates/design/server/plugins/db.ts +15 -0
  142. package/corpus/templates/design/shared/canvas-dimensions.ts +24 -13
  143. package/corpus/templates/design/shared/generation-provenance.ts +66 -0
  144. package/corpus/templates/design/shared/journey-canvas-messages.ts +4 -0
  145. package/corpus/templates/design/shared/journey-canvas.ts +256 -5
  146. package/corpus/templates/design/shared/local-visual-edit-principal.ts +11 -0
  147. package/corpus/templates/design/shared/specified-design-prompt.ts +15 -0
  148. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +20 -21
  149. package/corpus/templates/slides/.agents/skills/slide-design/SKILL.md +73 -98
  150. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +108 -186
  151. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +6 -0
  152. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -1
  153. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +89 -47
  154. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +179 -158
  155. package/corpus/templates/slides/app/global.css +27 -0
  156. package/corpus/templates/slides/app/hooks/use-narrow-element.ts +30 -0
  157. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  158. package/corpus/templates/slides/app/pages/DeckEditor.tsx +410 -228
  159. package/corpus/templates/slides/app/pages/Index.tsx +1 -0
  160. package/corpus/templates/slides/changelog/2026-10-09-slides-keep-generating-when-image-search-is-unavailable.md +6 -0
  161. package/corpus/templates/slides/changelog/2026-10-09-the-slides-widget-in-chatgpt-and-codex-now-has-the-editor-s-.md +6 -0
  162. package/corpus/templates/slides/server/lib/chatgpt-directory-tools.ts +46 -14
  163. package/corpus/templates/slides/server/lib/generation-completion.ts +4 -5
  164. package/package.json +1 -1
  165. package/corpus/templates/content/app/components/editor/DirectoryWidgetFormattingToolbar.tsx +0 -73
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: 11704
35
+ - template files: 11738
@@ -328,6 +328,14 @@ Use conditional blocks for optional filters:
328
328
  {{?country}}AND country = '{{country}}'{{/country}}
329
329
  ```
330
330
 
331
+ **Use `type: "multi-select"` for a pick list where several options can apply at once.** Give it `options` like a `select`. Its value is the selected option values joined by commas. Interpolate it as `IN ({{<id>:list}})`, which expands to one quoted literal per selected value, and wrap the clause in a conditional so an empty selection drops the filter:
332
+
333
+ ```sql
334
+ {{?plan}}AND plan IN ({{plan:list}}){{/plan}}
335
+ ```
336
+
337
+ An unwrapped `{{<id>:list}}` with no selection fails the query on purpose. Option values must not contain commas.
338
+
331
339
  Filters auto-apply on change — there is no Apply button. Each filter change writes to the URL and re-runs the affected panels. Other filters are preserved (the URL update is functional, not destructive). If you see a filter "reset" itself when another filter changes, look for a duplicate `id` first.
332
340
 
333
341
  ## Modifying A Dashboard
@@ -34,11 +34,16 @@ same counts as an indented `outline` with no examples. Use it first to choose a
34
34
  window, `app`, `maxDepth`, `minNodeSessions` (small branches merge into an
35
35
  `other` node), and `maxNodes`.
36
36
 
37
+ To include the bounded cross-session estimate from the repository root, run:
38
+ `pnpm --filter analytics action get-onboarding-journey --dateFrom=2026-08-01 --dateTo=2026-08-31 --app=all --emailFilter=exclude_builder --followUpMode=person --format=summary`.
39
+
37
40
  ```ts
38
41
  type JourneyExample = { sessionId: string; recordingId: string | null; ts: string; offsetMs: number | null; viewport: { width: number; height: number } | null; viewportReason?: string; replayUrl?: string };
39
42
  type JourneyNode = { key: string; label: string; parentKey: string | null; depth: number; kind: "step" | "other"; n: number; pctOfRoot: number; pctOfParent: number; dropoffN: number; dropoffPct: number; deeperN: number; examples: JourneyExample[] };
40
43
  type JourneyFollowup = { status: "complete" | "incomplete"; incompleteReason?: "journey_event_read_truncated" | "journey_event_read_invalid" | "journey_event_read_may_have_shifted" | "terminal_cohort_query_too_large" | "followup_aggregate_truncated" | "followup_aggregate_invalid" | "terminal_cohort_mismatch"; observationCutoff: string; observationFollowupDurationMs: { min: number; max: number; mean: number } | null; rightCensoredAtWindowEnd: true; coverage: { journeyEventRead: { rows: number; pages: number; truncated: boolean; paginationConsistency: "stable" | "may_have_shifted" }; followupAggregateRead: { rows: number | null; queries: number; truncated: boolean }; cohortSessions: number | null }; laterRecordedActivityWithinWindow: { total: number | null; byTerminalStepKey: Record<string, number> | null }; noLaterRecordedActivityWithinWindow: { total: number | null; byTerminalStepKey: Record<string, number> | null } };
41
- type JourneyTree = { window: { from: string; to: string }; app: string; rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[]; followUp: JourneyFollowup; standaloneSetup?: { rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[] }; notes?: string[] };
44
+ type JourneyPersonFollowupCounts = { canonicalPeople: number; laterActivityInSelectedSession: number; laterActivityOutsideSelectedSessionOrApp: number; laterActivityInBothSelectedAndOutside: number; laterActivityObservedAnywhere: number; noActivityObservedWithinHorizon: number; rightCensoredHorizon: number; fullyObservedCanonicalPeople: number; noActivityObservedWithinHorizonPctOfFullyObservedCanonicalPeople: number | null; identityUnavailableSessions: number; identityUnavailableSessionEvidence: { laterActivityInSelectedSession: number; laterActivityOutsideSelectedSessionOrApp: number } };
45
+ type JourneyPersonFollowup = { status: "complete" | "incomplete"; incompleteReason?: "journey_event_read_truncated" | "journey_event_read_invalid" | "journey_event_read_may_have_shifted" | "terminal_cohort_too_large" | "terminal_cohort_invalid" | "person_followup_aggregate_truncated" | "person_followup_query_cost_limited" | "person_followup_aggregate_invalid" | "person_followup_terminal_cohort_mismatch"; horizonDays: 30; horizonMs: number; observationWatermark: string; observationFollowupDurationMs: { min: number; max: number; mean: number } | null; coverage: { journeyEventRead: { rows: number; pages: number; truncated: boolean; paginationConsistency: "stable" | "may_have_shifted" }; followupAggregateRead: { status: "complete" | "truncated" | "incomplete" | "not_run"; rows: number | null; queries: number; truncated: boolean }; terminalSessions: number | null; sessionsWithoutSelectedStep: number | null; identityJoin: { status: "complete" | "partial" | "unavailable" | "not_applicable" | "unknown"; terminalSessions: number | null; sessionsWithCanonicalIdentity: number | null; sessionsWithoutCanonicalIdentity: number | null; uniqueCanonicalPeople: number | null; coveragePct: number | null } }; total: JourneyPersonFollowupCounts | null; byTerminalStepKey: Record<string, JourneyPersonFollowupCounts> | null };
46
+ type JourneyTree = { window: { from: string; to: string }; app: string; rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[]; followUp: JourneyFollowup; personFollowUp?: JourneyPersonFollowup; standaloneSetup?: { rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[] }; notes?: string[] };
42
47
  ```
43
48
 
44
49
  Analytics returns cohort nodes with counts. When extending a Design storyboard
@@ -47,6 +52,25 @@ only to the Design input, set `referenceOnly: true`, and omit the cohort metric
47
52
  fields. This annotation is for visual references and is not emitted by
48
53
  `get-onboarding-journey`.
49
54
 
55
+ Saved outputs require source-confirmed completion events. Clips uses
56
+ `recording_ready`; `recording_started`, transcript-only `recording_completed`,
57
+ and `clip_viewed` are not saved outputs. Slides uses explicit
58
+ `generation_completed`; `generation_request_accepted`,
59
+ `generation_outcome_unresolved`, `generation_failed`, `generation_stuck`,
60
+ `generation_cancelled`, and `generation_abandoned` remain distinct attempt
61
+ steps. An unresolved event can carry `persisted_output: true`, but it still does
62
+ not substitute for `generation_completed`. `deck_edited`, `output_viewed`, and
63
+ current deck state also cannot substitute for that event. A missing terminal
64
+ event stays absent. Generation and recording starts remain attempt steps, and
65
+ retry attempts that reuse a deck's output ID remain separate by their exact
66
+ attempt ID. A sessionless Clips completion or Slides lifecycle event is
67
+ attached only when its exact output and attempt pair maps to one distinct
68
+ eligible onboarding session (`recording_started` for Clips;
69
+ `generation_started`, `generation_request_accepted`, or `output_viewed` for
70
+ Slides); missing or ambiguous
71
+ matches stay unattributed. Output and attempt IDs are never included in the
72
+ returned tree.
73
+
50
74
  The journey projection also retains `integration_setup_exposed`,
51
75
  `integration_method_clicked`, and `integration_method_outcome` as separate
52
76
  `integration:<flow>:...` steps. These record setup exposure, the selected
@@ -89,6 +113,32 @@ top-level tree to `journey:capture`; the capture CLI reads top-level `nodes`.
89
113
  identifies the limiting read. Do not report percentages from that partial
90
114
  result. Existing journey counts and denominators remain independent of this
91
115
  follow-up read.
116
+ - Set `followUpMode: "person"` to add `personFollowUp` without changing the
117
+ session tree or same-session `followUp`. Each direct `properties.auth_user_id`
118
+ is counted once and assigned to that person's latest terminal selected step
119
+ in the requested cohort. No email, anonymous, user-key, or organization
120
+ pseudo-ID fallback is used for cross-session matching. The query searches
121
+ later events across first-party apps under the same authenticated org,
122
+ Builder.io email, and test-identity filters. Selected-session/app and
123
+ outside-session/app activity are overlapping evidence classes;
124
+ `laterActivityInBothSelectedAndOutside` reports their overlap, and
125
+ `laterActivityObservedAnywhere` is their union. A member with activity in
126
+ either class is not counted as having no activity. The result also separates
127
+ no activity after a fully observed 30-day horizon, right-censored horizons,
128
+ and identity-unavailable sessions. Identity-unavailable session evidence can
129
+ report activity known from that exact session ID, but those sessions are not
130
+ people and never enter person percentages.
131
+ - `personFollowUp.observationWatermark` freezes both event and receive time at a UTC minute boundary, so repeated requests in that minute use the same read boundary.
132
+ `observationFollowupDurationMs` summarizes capped follow-up among canonical
133
+ people. `noActivityObservedWithinHorizonPctOfFullyObservedCanonicalPeople`
134
+ uses only identified people whose complete 30-day horizon elapsed. It
135
+ measures bounded Analytics coverage, never abandonment or permanent churn.
136
+ Sessions without a selected journey step appear in
137
+ `coverage.sessionsWithoutSelectedStep` and are not eligible no-activity
138
+ observations; a missing next step does not mean no later activity. Read
139
+ `coverage.identityJoin` and `coverage.followupAggregateRead`; if a raw read
140
+ is incomplete or BigQuery rejects its billed-byte cap, counts and percentages are null with
141
+ `status: "incomplete"`.
92
142
  - `maxDepth` defaults to 8 and is bounded at 40. Request `maxDepth: 40` for a
93
143
  deeper pass. When sessions continue past the requested depth,
94
144
  `coverage.truncated` is true and the boundary node's `deeperN` says how many
@@ -145,9 +195,10 @@ chunk. Fix failures or report them; do not paint over a missing frame.
145
195
  the PNG. Every visible iframe must have a corresponding recorded child document
146
196
  by the requested replay time. If the child document is missing, the frame is
147
197
  listed as a failure with code `replay_iframe_content_unavailable` and counts in
148
- `diagnostics`. If unsupported 3D projection, rounded ancestor clipping, CSS
149
- clip paths, masks, or visibility-altering filters prevent the audit from
150
- verifying visibility, the frame is listed with code
198
+ `diagnostics`. If unsupported 3D projection, rounded ancestor clipping,
199
+ unsupported CSS `clip-path` shapes, unrecognized legacy CSS `clip` values,
200
+ masks, or visibility-altering filters prevent the audit from verifying
201
+ visibility, the frame is listed with code
151
202
  `replay_iframe_visibility_unverifiable`; do not treat uncertainty as either
152
203
  missing content or a successful audit. The check covers every visible iframe
153
204
  because replay can omit its original source attribute while rebuilding an
@@ -15,6 +15,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
15
15
 
16
16
  - Journey captures now flag visible previews missing from the recording instead of saving blank screenshots
17
17
  - Journey recordings flag iframe content when clipping, masks, or filters make its visibility uncertain.
18
+ - Onboarding journeys count saved clips and completed deck generations as outputs while showing Slides attempt outcomes separately
19
+ - Session recordings keep their full history and performance insights across browser session changes and large replays.
18
20
  - Large onboarding journeys keep their step counts when follow-up coverage exceeds query limits
19
21
 
20
22
  ## 2026-10-08
@@ -34,7 +34,7 @@ const isoDate = z
34
34
 
35
35
  export default defineAction({
36
36
  description:
37
- '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 } for sessions that entered onboarding, plus standaloneSetup for Home chat integration events in sessions without onboarding; its root and percentages have a separate denominator. Each node has n, pctOfRoot, pctOfParent, dropoffN/dropoffPct (last observed step, not a confirmed exit), deeperN (sessions with a later observed step omitted below this node), and example sessions with recordingId, a recording-start-relative offsetMs, and viewport. Both formats include followUp counts of sessions with and without any later native Analytics event in the same session after each terminal selected step, observed before one frozen observationCutoff. followUp.rightCensoredAtWindowEnd is true: no later event means none was recorded within that bounded window, not abandonment or churn. If the event read may be incomplete, the aggregate query truncates or exceeds the SQL size or parser-token limit, or cohort coverage mismatches, followUp.status is incomplete, incompleteReason names the cause, and its cohort counts and duration are null. format "summary" returns the same tree counts as indented outlines with no examples. Check each tree\'s coverage.truncated first: true means its event sample, requested depth, or node list cut that tree; notes explain why. Use it to plan onboarding storyboards or see each cohort\'s last recorded selected step. For a standalone storyboard, pass standaloneSetup as the top-level tree to journey:capture.',
37
+ 'Return an access-scoped per-session onboarding tree through explicit saved-output events, preserving session counts and direct-parent denominators. followUpMode "session" keeps existing bounded same-session counts. "person" adds aggregate-only activity joined by direct canonical auth_user_id across first-party sessions and apps over a fixed 30-day horizon. It separates selected-session activity, outside-session/app activity and their overlap, no activity after a fully observed horizon, right-censoring, unknown identity, identity coverage, and read completeness. Its minute-aligned observationWatermark freezes event and receive time. Unknown identity and sessions without a selected step never count as inactive; no-activity is window-bounded evidence, not permanent churn. Output and attempt IDs are never returned. Check coverage.truncated before interpreting counts.',
38
38
  schema: z.object({
39
39
  dateFrom: isoDate.describe(
40
40
  "Inclusive UTC start date, YYYY-MM-DD. Sessions that began earlier appear mid-journey, so start a day before the period you care about.",
@@ -56,6 +56,13 @@ export default defineAction({
56
56
  .describe(
57
57
  'Builder-employee scope, as in the onboarding metrics: "exclude_builder" (default), "only_builder", or "all". Test identities are always excluded.',
58
58
  ),
59
+ followUpMode: z
60
+ .enum(["session", "person"])
61
+ .optional()
62
+ .default("session")
63
+ .describe(
64
+ 'Follow-up grain: "session" (default) keeps existing same-session counts; "person" adds a scoped 30-day cross-session, cross-app estimate joined only by direct canonical auth_user_id.',
65
+ ),
59
66
  format: z
60
67
  .enum(["tree", "summary"])
61
68
  .optional()
@@ -184,6 +191,7 @@ export default defineAction({
184
191
  dateTo: args.dateTo,
185
192
  app: args.app,
186
193
  emailFilter: args.emailFilter,
194
+ followUpMode: args.followUpMode,
187
195
  format: args.format,
188
196
  maxNodes: args.maxNodes,
189
197
  maxDepth: args.maxDepth,
@@ -9,6 +9,10 @@ import {
9
9
  import { track } from "@agent-native/core/tracking";
10
10
  import { z } from "zod";
11
11
 
12
+ import {
13
+ MULTI_SELECT_EMPTY,
14
+ normalizeMultiSelectValue,
15
+ } from "../app/pages/adhoc/sql-dashboard/filter-vars";
12
16
  import {
13
17
  interpolate,
14
18
  interpolateDashboardPanelSql,
@@ -292,6 +296,59 @@ function collectDashboardConfigIssues(
292
296
  }
293
297
  const id = typeof f.id === "string" ? f.id.trim() : "";
294
298
  if (!id) return dashboardIssue(`config.filters[${i}].id is required`);
299
+ if (f.type === "multi-select") {
300
+ // The selection is comma-joined in the URL, so a value cannot be empty or contain ",". MULTI_SELECT_EMPTY is reserved for the cleared state.
301
+ if (f.options !== undefined && !Array.isArray(f.options)) {
302
+ return dashboardIssue(
303
+ `config.filters[${i}].options must be an array`,
304
+ );
305
+ }
306
+ const options: unknown[] = Array.isArray(f.options) ? f.options : [];
307
+ for (let j = 0; j < options.length; j++) {
308
+ const option = options[j] as {
309
+ value?: unknown;
310
+ label?: unknown;
311
+ } | null;
312
+ if (
313
+ !option ||
314
+ typeof option !== "object" ||
315
+ typeof option.value !== "string" ||
316
+ typeof option.label !== "string"
317
+ ) {
318
+ return dashboardIssue(
319
+ `config.filters[${i}].options[${j}] must be an object with string value and label`,
320
+ );
321
+ }
322
+ if (
323
+ option.value === "" ||
324
+ option.value.includes(",") ||
325
+ option.value === MULTI_SELECT_EMPTY
326
+ ) {
327
+ return dashboardIssue(
328
+ `config.filters[${i}].options[${j}].value must be non-empty, cannot contain ",", and cannot be "${MULTI_SELECT_EMPTY}" in a multi-select filter`,
329
+ );
330
+ }
331
+ }
332
+ if (typeof f.default === "string" && f.default !== "") {
333
+ const named = normalizeMultiSelectValue(f.default);
334
+ // A default that normalizes to nothing would show All while the query still gets a non-empty value.
335
+ if (named === "") {
336
+ return dashboardIssue(
337
+ `config.filters[${i}].default must name at least one option value in a multi-select filter`,
338
+ );
339
+ }
340
+ // Each default token must be a configured option: a token with no checkbox would be applied to SQL unseen.
341
+ const values = new Set(
342
+ options.map((option) => (option as { value: string }).value),
343
+ );
344
+ const unknown = named.split(",").find((token) => !values.has(token));
345
+ if (unknown !== undefined) {
346
+ return dashboardIssue(
347
+ `config.filters[${i}].default names "${unknown}", which is not one of its options`,
348
+ );
349
+ }
350
+ }
351
+ }
295
352
  if (seen.has(id)) continue;
296
353
  seen.add(id);
297
354
  deduped.push(f);
@@ -7,6 +7,13 @@
7
7
  }
8
8
  },
9
9
  "doctor": {
10
- "failOnBuild": false
10
+ "failOnBuild": false,
11
+ "dbToolScopingDenylist": {
12
+ "bigquery_cache": "provider cache, not a user-facing resource",
13
+ "dashboard_name_locks": "internal dashboard-name serialization rows, never exposed to raw DB tools",
14
+ "dashboard_views": "view telemetry, scoped by dashboard or action",
15
+ "first_party_analytics_cache": "internal query cache, accessed through scoped analytics queries",
16
+ "session_recording_session_associations": "child rows are scoped through authorized session recordings"
17
+ }
11
18
  }
12
19
  }
@@ -757,6 +757,7 @@ export default {
757
757
  },
758
758
  sqlDashboard: {
759
759
  customRange: "Custom range",
760
+ allValues: "All",
760
761
  untitledDashboard: "Untitled Dashboard",
761
762
  dashboardFallback: "dashboard",
762
763
  viewOnly: "You have view-only access to this dashboard.",
@@ -24,6 +24,11 @@ import {
24
24
  DialogFooter,
25
25
  } from "@/components/ui/dialog";
26
26
  import { Input } from "@/components/ui/input";
27
+ import {
28
+ Popover,
29
+ PopoverContent,
30
+ PopoverTrigger,
31
+ } from "@/components/ui/popover";
27
32
  import {
28
33
  Select,
29
34
  SelectContent,
@@ -37,6 +42,7 @@ import { DateRangeInput } from "../_shared/components/DateRangeInput";
37
42
  import {
38
43
  FILTER_PARAM_PREFIX,
39
44
  isDateRangePresetFilter,
45
+ MULTI_SELECT_EMPTY,
40
46
  resolveDefault,
41
47
  resolveFilterVars,
42
48
  } from "./filter-vars";
@@ -340,6 +346,30 @@ export function DashboardFilterBar({
340
346
  );
341
347
  }
342
348
 
349
+ function MultiSelectOption({
350
+ label,
351
+ checked,
352
+ onCheckedChange,
353
+ }: {
354
+ label: string;
355
+ checked: boolean;
356
+ onCheckedChange: (checked: boolean) => void;
357
+ }) {
358
+ const id = useId();
359
+ return (
360
+ <div className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-xs hover:bg-accent">
361
+ <Checkbox
362
+ id={id}
363
+ checked={checked}
364
+ onCheckedChange={(next) => onCheckedChange(next === true)}
365
+ />
366
+ <label htmlFor={id} className="min-w-0 flex-1 cursor-pointer truncate">
367
+ {label}
368
+ </label>
369
+ </div>
370
+ );
371
+ }
372
+
343
373
  interface FilterControlProps {
344
374
  filter: DashboardFilter;
345
375
  vars: Record<string, string>;
@@ -354,6 +384,8 @@ function FilterControl({
354
384
  setValue,
355
385
  }: FilterControlProps) {
356
386
  const t = useT();
387
+ const labelId = useId();
388
+ const triggerId = useId();
357
389
  if (filter.type === "date-range") {
358
390
  const startKey = `${filter.id}Start`;
359
391
  const endKey = `${filter.id}End`;
@@ -447,6 +479,75 @@ function FilterControl({
447
479
  return selectControl;
448
480
  }
449
481
 
482
+ if (filter.type === "multi-select") {
483
+ // Values are comma-joined in the URL, so option values must not contain ",".
484
+ const options = filter.options ?? [];
485
+ const selected = (vars[filter.id] || "").split(",").filter(Boolean);
486
+ // Label the values the query receives, including URL values with no matching option, so the trigger never reads "All" over a filtered query.
487
+ const selectedLabels = selected.map(
488
+ (value) => options.find((opt) => opt.value === value)?.label ?? value,
489
+ );
490
+ const setSelected = (next: string[]) =>
491
+ setValue({
492
+ [filter.id]: next.length > 0 ? next.join(",") : MULTI_SELECT_EMPTY,
493
+ });
494
+ const toggle = (value: string, checked: boolean) =>
495
+ setSelected(
496
+ options
497
+ .filter((opt) =>
498
+ opt.value === value ? checked : selected.includes(opt.value),
499
+ )
500
+ .map((opt) => opt.value),
501
+ );
502
+ return (
503
+ <div className="flex flex-col gap-1">
504
+ <label
505
+ id={labelId}
506
+ className="text-xs text-muted-foreground font-medium"
507
+ >
508
+ {filter.label}
509
+ </label>
510
+ <Popover>
511
+ <PopoverTrigger asChild>
512
+ <Button
513
+ id={triggerId}
514
+ aria-labelledby={`${labelId} ${triggerId}`}
515
+ variant="outline"
516
+ size="sm"
517
+ className="w-[160px] justify-start text-xs font-normal"
518
+ >
519
+ <span className="min-w-0 truncate">
520
+ {selectedLabels.length > 0
521
+ ? selectedLabels.join(", ")
522
+ : t("sqlDashboard.allValues")}
523
+ </span>
524
+ </Button>
525
+ </PopoverTrigger>
526
+ <PopoverContent align="start" className="w-[200px] p-1">
527
+ {options.map((opt) => (
528
+ <MultiSelectOption
529
+ key={opt.value}
530
+ label={opt.label}
531
+ checked={selected.includes(opt.value)}
532
+ onCheckedChange={(checked) => toggle(opt.value, checked)}
533
+ />
534
+ ))}
535
+ {selected.length > 0 && (
536
+ <Button
537
+ variant="ghost"
538
+ size="sm"
539
+ className="mt-1 h-6 w-full justify-start px-2 text-xs text-muted-foreground"
540
+ onClick={() => setSelected([])}
541
+ >
542
+ {t("sqlDashboard.clearAll")}
543
+ </Button>
544
+ )}
545
+ </PopoverContent>
546
+ </Popover>
547
+ </div>
548
+ );
549
+ }
550
+
450
551
  if (filter.type === "toggle") {
451
552
  const active = hasParam(filter.id) && !!vars[filter.id];
452
553
  return (
@@ -2,6 +2,17 @@ import type { DashboardFilter, FilterType } from "./types";
2
2
 
3
3
  export const FILTER_PARAM_PREFIX = "f_";
4
4
 
5
+ // URL marker for a multi-select with every option unchecked. An absent param
6
+ // falls back to the filter default, so an empty selection needs its own value.
7
+ export const MULTI_SELECT_EMPTY = "__empty__";
8
+
9
+ // The one reduction of a multi-select value: the page, saved reports, and
10
+ // save-time validation all read the selection through it.
11
+ export function normalizeMultiSelectValue(raw: string): string {
12
+ if (raw === MULTI_SELECT_EMPTY) return "";
13
+ return raw.split(",").filter(Boolean).join(",");
14
+ }
15
+
5
16
  const ALL_TIME_START = "1970-01-01";
6
17
  const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
7
18
  const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
@@ -13,6 +24,7 @@ const FILTER_TYPES: DashboardFilter["type"][] = [
13
24
  "date",
14
25
  "date-range",
15
26
  "select",
27
+ "multi-select",
16
28
  "toggle",
17
29
  "text",
18
30
  "toggle-date",
@@ -187,6 +199,11 @@ export function resolveFilterVars(
187
199
  fallbackEnd,
188
200
  fallbackEnd,
189
201
  );
202
+ } else if (filter.type === "multi-select") {
203
+ const value = getParam(filter.id);
204
+ out[filter.id] = normalizeMultiSelectValue(
205
+ value || resolveDefault(filter.default, filter.type),
206
+ );
190
207
  } else {
191
208
  const value = getParam(filter.id);
192
209
  out[filter.id] =
@@ -183,18 +183,29 @@ export function interpolate(
183
183
  },
184
184
  );
185
185
 
186
- return withConditionals.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
187
- const value = vars[name];
188
- if (
189
- options.failClosedTimeVariables &&
190
- isTimeVariable(name) &&
191
- (value == null || value.length === 0)
192
- ) {
193
- return "__missing_dashboard_time_filter__";
194
- }
195
- if (value == null) return "";
196
- return options.googleSqlValues
197
- ? escapeGoogleSqlValue(String(value))
198
- : escapeSqlValue(String(value));
199
- });
186
+ const escapeValue = options.googleSqlValues
187
+ ? escapeGoogleSqlValue
188
+ : escapeSqlValue;
189
+ // One pass, so a value that itself contains "{{x}}" is never re-expanded.
190
+ return withConditionals.replace(
191
+ /\{\{(\w+)(:list)?\}\}/g,
192
+ (_match, name, list) => {
193
+ const value = vars[name];
194
+ if (list) {
195
+ // Multi-select values are comma-joined in the URL, so an option value containing "," splits into two items.
196
+ const items = (value ?? "").split(",").filter(Boolean);
197
+ if (items.length === 0) return "__empty_list_filter__";
198
+ return items.map((item) => `'${escapeValue(item)}'`).join(", ");
199
+ }
200
+ if (
201
+ options.failClosedTimeVariables &&
202
+ isTimeVariable(name) &&
203
+ (value == null || value.length === 0)
204
+ ) {
205
+ return "__missing_dashboard_time_filter__";
206
+ }
207
+ if (value == null) return "";
208
+ return escapeValue(String(value));
209
+ },
210
+ );
200
211
  }
@@ -28,6 +28,7 @@ export type FilterType =
28
28
  | "date"
29
29
  | "date-range"
30
30
  | "select"
31
+ | "multi-select"
31
32
  | "toggle"
32
33
  | "text"
33
34
  | "toggle-date";
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Compare later onboarding activity across first-party sessions and apps, with unknown identities and incomplete observation windows called out.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Dashboards can filter by several values at once with multi-select filters.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Journey screenshots account for legacy clipping when checking embedded replay content.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Journey trees remain available when a matching recording is not visible
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Onboarding journeys can find recordings from exact replay links even when session IDs differ
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Onboarding journeys count saved clips and completed deck generations as outputs while showing Slides attempt outcomes separately
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-09
4
+ ---
5
+
6
+ Session recordings keep their full history and performance insights across browser session changes and large replays.