@agent-native/core 0.77.24 → 0.78.1

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 (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/template-analytics.md +5 -0
  4. package/corpus/core/docs/content/tracking.md +45 -0
  5. package/corpus/core/package.json +3 -1
  6. package/corpus/core/src/agent/types.ts +22 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +34 -18
  8. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  9. package/corpus/core/src/client/agent-chat.ts +173 -0
  10. package/corpus/core/src/client/analytics.ts +141 -5
  11. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  12. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  13. package/corpus/core/src/client/composer/types.ts +22 -0
  14. package/corpus/core/src/client/index.ts +14 -0
  15. package/corpus/core/src/client/session-replay.ts +800 -0
  16. package/corpus/core/src/db/schema.ts +12 -0
  17. package/corpus/core/src/deploy/build.ts +75 -0
  18. package/corpus/core/src/private-blob/index.ts +18 -0
  19. package/corpus/core/src/private-blob/registry.ts +241 -0
  20. package/corpus/core/src/private-blob/types.ts +55 -0
  21. package/corpus/core/src/scripts/utils.ts +34 -6
  22. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  23. package/corpus/core/src/server/email.ts +5 -0
  24. package/corpus/core/src/server/index.ts +1 -0
  25. package/corpus/core/src/styles/agent-native.css +24 -0
  26. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
  27. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
  28. package/corpus/templates/analytics/AGENTS.md +32 -1
  29. package/corpus/templates/analytics/DEVELOPING.md +9 -1
  30. package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
  31. package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
  32. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
  33. package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
  34. package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
  35. package/corpus/templates/analytics/actions/navigate.ts +15 -5
  36. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  37. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
  38. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
  39. package/corpus/templates/analytics/actions/view-screen.ts +50 -2
  40. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
  41. package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
  44. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
  45. package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
  53. package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
  54. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
  55. package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
  56. package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
  60. package/corpus/templates/analytics/package.json +5 -2
  61. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
  62. package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
  63. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
  64. package/corpus/templates/analytics/server/db/index.ts +12 -0
  65. package/corpus/templates/analytics/server/db/schema.ts +131 -0
  66. package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
  67. package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
  68. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
  69. package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
  73. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
  74. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
  75. package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
  76. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  77. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
  78. package/corpus/templates/analytics/server/plugins/db.ts +200 -0
  79. package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
  80. package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
  81. package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
  82. package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
  83. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
  84. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
  86. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
  87. package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
  88. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  89. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  90. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  91. package/corpus/templates/assets/AGENTS.md +18 -3
  92. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  93. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  94. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  95. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  96. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  97. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  98. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  99. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  100. package/corpus/templates/assets/actions/navigate.ts +1 -1
  101. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  102. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  103. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  104. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  105. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  106. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  107. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  108. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  109. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  110. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  111. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  112. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  113. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  114. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  115. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  116. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  117. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  118. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  119. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  120. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  121. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  122. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  123. package/corpus/templates/assets/server/db/index.ts +1 -1
  124. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  125. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  126. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  127. package/corpus/templates/assets/shared/api.ts +6 -3
  128. package/dist/agent/types.d.ts +21 -0
  129. package/dist/agent/types.d.ts.map +1 -1
  130. package/dist/agent/types.js.map +1 -1
  131. package/dist/client/AgentPanel.d.ts +6 -2
  132. package/dist/client/AgentPanel.d.ts.map +1 -1
  133. package/dist/client/AgentPanel.js +10 -4
  134. package/dist/client/AgentPanel.js.map +1 -1
  135. package/dist/client/AssistantChat.d.ts +6 -0
  136. package/dist/client/AssistantChat.d.ts.map +1 -1
  137. package/dist/client/AssistantChat.js +15 -3
  138. package/dist/client/AssistantChat.js.map +1 -1
  139. package/dist/client/agent-chat.d.ts +32 -0
  140. package/dist/client/agent-chat.d.ts.map +1 -1
  141. package/dist/client/agent-chat.js +113 -0
  142. package/dist/client/agent-chat.js.map +1 -1
  143. package/dist/client/analytics.d.ts +22 -3
  144. package/dist/client/analytics.d.ts.map +1 -1
  145. package/dist/client/analytics.js +96 -4
  146. package/dist/client/analytics.js.map +1 -1
  147. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  148. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  149. package/dist/client/composer/TiptapComposer.js +228 -56
  150. package/dist/client/composer/TiptapComposer.js.map +1 -1
  151. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  152. package/dist/client/composer/extensions/MentionReference.js +3 -0
  153. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  154. package/dist/client/composer/types.d.ts +21 -0
  155. package/dist/client/composer/types.d.ts.map +1 -1
  156. package/dist/client/composer/types.js.map +1 -1
  157. package/dist/client/index.d.ts +2 -2
  158. package/dist/client/index.d.ts.map +1 -1
  159. package/dist/client/index.js +2 -2
  160. package/dist/client/index.js.map +1 -1
  161. package/dist/client/session-replay.d.ts +41 -0
  162. package/dist/client/session-replay.d.ts.map +1 -0
  163. package/dist/client/session-replay.js +595 -0
  164. package/dist/client/session-replay.js.map +1 -0
  165. package/dist/collab/awareness.d.ts +2 -2
  166. package/dist/collab/awareness.d.ts.map +1 -1
  167. package/dist/collab/routes.d.ts +2 -2
  168. package/dist/db/schema.d.ts +3 -1
  169. package/dist/db/schema.d.ts.map +1 -1
  170. package/dist/db/schema.js +6 -2
  171. package/dist/db/schema.js.map +1 -1
  172. package/dist/deploy/build.d.ts +8 -0
  173. package/dist/deploy/build.d.ts.map +1 -1
  174. package/dist/deploy/build.js +66 -0
  175. package/dist/deploy/build.js.map +1 -1
  176. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +8 -8
  179. package/dist/private-blob/index.d.ts +3 -0
  180. package/dist/private-blob/index.d.ts.map +1 -0
  181. package/dist/private-blob/index.js +2 -0
  182. package/dist/private-blob/index.js.map +1 -0
  183. package/dist/private-blob/registry.d.ts +10 -0
  184. package/dist/private-blob/registry.d.ts.map +1 -0
  185. package/dist/private-blob/registry.js +152 -0
  186. package/dist/private-blob/registry.js.map +1 -0
  187. package/dist/private-blob/types.d.ts +51 -0
  188. package/dist/private-blob/types.d.ts.map +1 -0
  189. package/dist/private-blob/types.js +2 -0
  190. package/dist/private-blob/types.js.map +1 -0
  191. package/dist/resources/handlers.d.ts +3 -3
  192. package/dist/scripts/utils.d.ts +4 -4
  193. package/dist/scripts/utils.d.ts.map +1 -1
  194. package/dist/scripts/utils.js +30 -6
  195. package/dist/scripts/utils.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +5 -0
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  200. package/dist/server/email.d.ts +2 -0
  201. package/dist/server/email.d.ts.map +1 -1
  202. package/dist/server/email.js +3 -0
  203. package/dist/server/email.js.map +1 -1
  204. package/dist/server/index.d.ts +1 -1
  205. package/dist/server/index.d.ts.map +1 -1
  206. package/dist/server/index.js.map +1 -1
  207. package/dist/server/transcribe-voice.d.ts +1 -1
  208. package/dist/styles/agent-native.css +24 -0
  209. package/docs/content/template-analytics.md +5 -0
  210. package/docs/content/tracking.md +45 -0
  211. package/package.json +3 -1
  212. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
@@ -72,15 +72,46 @@ details live in `.agents/skills/`.
72
72
  it as an extension instead and tell the user why.
73
73
  - Use framework sharing and access helpers for dashboards, analyses, and saved
74
74
  resources.
75
+ - Dashboard email reports live in SQL via the
76
+ `dashboard-report-subscriptions` actions. They send daily snapshots scoped to
77
+ the exact user/org context that created the subscription with saved URL
78
+ filters; do not hand-wire custom email routes around that action surface.
79
+ Report PNGs are Playwright captures of the real dashboard route in
80
+ `reportScreenshot=1` mode, authenticated by a short-lived embed-session token
81
+ and embedded inline in email with a CID image. Netlify builds emit a scheduled
82
+ trigger plus a background worker from
83
+ `scripts/emit-netlify-dashboard-report-cron.ts`, using a per-deploy internal
84
+ token and disabling the in-process interval scheduler on Netlify to avoid
85
+ duplicate sends. External cron callers can still sweep due reports by POSTing
86
+ `/api/dashboard-reports/run` with
87
+ `Authorization: Bearer $DASHBOARD_REPORTS_CRON_SECRET`. PNG rendering uses
88
+ local Chrome in development and `playwright-core` plus
89
+ `@sparticuz/chromium-min` in serverless runtimes; set
90
+ `DASHBOARD_REPORT_CHROMIUM_PACK_URL` only when overriding the default
91
+ Chromium pack location.
75
92
 
76
93
  ## Application State
77
94
 
78
95
  - `navigation` exposes current dashboard, analysis, source, chart, and selected
79
96
  context.
80
97
  - `navigate` moves the user to the relevant analytics view, including
81
- `view="catalog"` for the template catalog.
98
+ `view="catalog"` for the template catalog and `view="sessions"` for session
99
+ replay.
82
100
  - Use `view-screen` when the active dashboard/chart context is unclear.
83
101
 
102
+ ## Session Replay
103
+
104
+ - `/sessions` lists scoped first-party session recordings. Filters live in the
105
+ URL (`range`, `app`, `q`) and are mirrored through application state so
106
+ the agent sees the same list the user sees.
107
+ - `/sessions/:recordingId` loads summary/timeline data through
108
+ `get-session-replay-summary` and fetches rrweb payloads only through scoped
109
+ chunk routes. Do not expose storage/provider URLs, raw chunk table access, or
110
+ unscoped replay blobs in UI, actions, dashboards, docs, or prompts.
111
+ - Dashboard rows that include `recording_id` should link to
112
+ `/sessions/:recordingId`; rows that only include `session_id` can link to a
113
+ filtered `/sessions` search.
114
+
84
115
  ## Dashboard Template Catalog
85
116
 
86
117
  - To build or extend a LARGE first-party dashboard, prefer `compose-dashboard`:
@@ -148,12 +148,20 @@ When a user asks for a **chart, metrics view, or data breakdown** → add it to
148
148
  ## Build & Dev Commands
149
149
 
150
150
  ```bash
151
- pnpm dev # Start dev server (frontend + backend, port 8080)
151
+ pnpm dev # Start dev server with local SQLite (frontend + backend, port 8080)
152
152
  pnpm build # Production build
153
153
  pnpm typecheck # TypeScript validation
154
154
  pnpm test # Run Vitest tests
155
155
  ```
156
156
 
157
+ `pnpm dev` pins `DATABASE_URL` to `file:./data/app.db` so local work does not
158
+ accidentally touch a production database from `.env`. To intentionally test
159
+ against another local database, set `ANALYTICS_DATABASE_URL` for that command:
160
+
161
+ ```bash
162
+ ANALYTICS_DATABASE_URL=postgres://localhost/analytics_dev pnpm dev
163
+ ```
164
+
157
165
  ## Creating Scripts
158
166
 
159
167
  ```typescript
@@ -0,0 +1,24 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { deleteDashboardReportSubscription } from "../server/lib/dashboard-report-subscriptions";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Delete a daily email report subscription for an analytics dashboard.",
13
+ schema: z.object({
14
+ id: z.string().describe("Subscription ID to delete"),
15
+ }),
16
+ http: { method: "DELETE" },
17
+ run: async (args) => {
18
+ const email = getRequestUserEmail();
19
+ if (!email) throw new Error("no authenticated user");
20
+ const orgId = getRequestOrgId() || null;
21
+ await deleteDashboardReportSubscription(args.id, { email, orgId });
22
+ return { id: args.id, success: true };
23
+ },
24
+ });
@@ -0,0 +1,50 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { getSessionReplayEvents } from "../server/lib/session-replay.js";
9
+
10
+ function resolveScope() {
11
+ const userEmail = getRequestUserEmail();
12
+ if (!userEmail) throw new Error("no authenticated user");
13
+ return { userEmail, orgId: getRequestOrgId() || null };
14
+ }
15
+
16
+ export default defineAction({
17
+ description:
18
+ "Read sanitized replay events for a first-party Analytics session recording. Results are capped and never expose storage provider URLs or raw chunk table access.",
19
+ schema: z.object({
20
+ recordingId: z.string().describe("The session_recordings id"),
21
+ startSeq: z.coerce
22
+ .number()
23
+ .int()
24
+ .min(0)
25
+ .optional()
26
+ .describe("Optional first chunk sequence to include"),
27
+ endSeq: z.coerce
28
+ .number()
29
+ .int()
30
+ .min(0)
31
+ .optional()
32
+ .describe("Optional last chunk sequence to include"),
33
+ limit: z.coerce
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(10000)
38
+ .optional()
39
+ .describe("Maximum replay events to return"),
40
+ }),
41
+ http: { method: "GET" },
42
+ readOnly: true,
43
+ run: async (args) => {
44
+ return getSessionReplayEvents(args.recordingId, resolveScope(), {
45
+ startSeq: args.startSeq,
46
+ endSeq: args.endSeq,
47
+ limit: args.limit,
48
+ });
49
+ },
50
+ });
@@ -0,0 +1,27 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { getSessionReplaySummary } from "../server/lib/session-replay.js";
9
+
10
+ function resolveScope() {
11
+ const userEmail = getRequestUserEmail();
12
+ if (!userEmail) throw new Error("no authenticated user");
13
+ return { userEmail, orgId: getRequestOrgId() || null };
14
+ }
15
+
16
+ export default defineAction({
17
+ description:
18
+ "Get a scoped summary for one first-party Analytics session replay recording. Does not return raw chunks or storage references.",
19
+ schema: z.object({
20
+ recordingId: z.string().describe("The session_recordings id"),
21
+ }),
22
+ http: { method: "GET" },
23
+ readOnly: true,
24
+ run: async (args) => {
25
+ return getSessionReplaySummary(args.recordingId, resolveScope());
26
+ },
27
+ });
@@ -0,0 +1,24 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { listDashboardReportSubscriptions } from "../server/lib/dashboard-report-subscriptions";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "List daily email report subscriptions for an analytics dashboard.",
13
+ schema: z.object({
14
+ dashboardId: z.string().optional().describe("Optional dashboard ID filter"),
15
+ }),
16
+ http: { method: "GET" },
17
+ readOnly: true,
18
+ run: async (args) => {
19
+ const email = getRequestUserEmail();
20
+ if (!email) throw new Error("no authenticated user");
21
+ const orgId = getRequestOrgId() || null;
22
+ return listDashboardReportSubscriptions({ email, orgId }, args.dashboardId);
23
+ },
24
+ });
@@ -0,0 +1,59 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { listSessionRecordings } from "../server/lib/session-replay.js";
9
+
10
+ function resolveScope() {
11
+ const userEmail = getRequestUserEmail();
12
+ if (!userEmail) throw new Error("no authenticated user");
13
+ return { userEmail, orgId: getRequestOrgId() || null };
14
+ }
15
+
16
+ export default defineAction({
17
+ description:
18
+ "List first-party Analytics session replay recordings accessible to the current user/org. Returns scoped recording summaries only, not raw replay chunks.",
19
+ schema: z.object({
20
+ query: z
21
+ .string()
22
+ .optional()
23
+ .describe(
24
+ "Optional broad search across recording, session, visitor, URL, app, and template fields",
25
+ ),
26
+ app: z.string().optional().describe("Optional app filter"),
27
+ template: z.string().optional().describe("Optional template filter"),
28
+ sessionId: z.string().optional().describe("Optional analytics session id"),
29
+ userId: z.string().optional().describe("Optional signed-in user id"),
30
+ anonymousId: z.string().optional().describe("Optional anonymous user id"),
31
+ path: z.string().optional().describe("Optional exact path filter"),
32
+ from: z
33
+ .string()
34
+ .optional()
35
+ .describe("Inclusive started_at lower bound as an ISO timestamp"),
36
+ to: z
37
+ .string()
38
+ .optional()
39
+ .describe("Inclusive started_at upper bound as an ISO timestamp"),
40
+ minDurationMs: z.coerce
41
+ .number()
42
+ .int()
43
+ .min(0)
44
+ .optional()
45
+ .describe("Only include recordings at least this long"),
46
+ hasErrors: z.boolean().optional().describe("Only recordings with errors"),
47
+ hasRageClicks: z
48
+ .boolean()
49
+ .optional()
50
+ .describe("Only recordings with detected rage clicks"),
51
+ status: z.enum(["active", "completed"]).optional(),
52
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
53
+ }),
54
+ http: { method: "GET" },
55
+ readOnly: true,
56
+ run: async (args) => {
57
+ return listSessionRecordings(resolveScope(), args);
58
+ },
59
+ });
@@ -4,13 +4,13 @@ import { z } from "zod";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "Navigate the UI to a specific view or dashboard. For filter changes (dashboard filter query params like ?f_date=...), use the framework-level `set-search-params` tool instead of this action.",
7
+ "Navigate the UI to a specific view, dashboard, analysis, extension, or Analytics session recording. For filter changes (dashboard filter query params like ?f_date=... or session filters like ?range=30d&q=signup), use the framework-level `set-search-params` tool instead of this action.",
8
8
  schema: z.object({
9
9
  view: z
10
10
  .string()
11
11
  .optional()
12
12
  .describe(
13
- "View to navigate to (overview, ask, adhoc, analyses, extensions, catalog, data-dictionary, data-sources, settings)",
13
+ "View to navigate to (ask, adhoc, analyses, extensions, sessions, catalog, data-dictionary, data-sources, settings)",
14
14
  ),
15
15
  dashboardId: z
16
16
  .string()
@@ -24,6 +24,10 @@ export default defineAction({
24
24
  .string()
25
25
  .optional()
26
26
  .describe("Extension ID to open (used with view=extensions)"),
27
+ recordingId: z
28
+ .string()
29
+ .optional()
30
+ .describe("Session recording id to open (used with view=sessions)"),
27
31
  }),
28
32
  http: false,
29
33
  run: async (args) => {
@@ -31,14 +35,15 @@ export default defineAction({
31
35
  !args.view &&
32
36
  !args.dashboardId &&
33
37
  !args.analysisId &&
34
- !args.extensionId
38
+ !args.extensionId &&
39
+ !args.recordingId
35
40
  ) {
36
41
  throw new Error(
37
- "At least --view, --dashboardId, --analysisId, or --extensionId is required.",
42
+ "At least --view, --dashboardId, --analysisId, --extensionId, or --recordingId is required.",
38
43
  );
39
44
  }
40
45
  const nav: Record<string, string> = {};
41
- if (args.view) nav.view = args.view;
46
+ if (args.view) nav.view = args.view === "overview" ? "ask" : args.view;
42
47
  if (args.dashboardId) {
43
48
  nav.dashboardId = args.dashboardId;
44
49
  if (!args.view) nav.view = "adhoc";
@@ -51,6 +56,10 @@ export default defineAction({
51
56
  nav.extensionId = args.extensionId;
52
57
  if (!args.view) nav.view = "extensions";
53
58
  }
59
+ if (args.recordingId) {
60
+ nav.recordingId = args.recordingId;
61
+ if (!args.view) nav.view = "sessions";
62
+ }
54
63
  await writeAppState("navigate", nav);
55
64
 
56
65
  const parts: string[] = [];
@@ -58,6 +67,7 @@ export default defineAction({
58
67
  if (nav.dashboardId) parts.push(`dashboard:${nav.dashboardId}`);
59
68
  if (nav.analysisId) parts.push(`analysis:${nav.analysisId}`);
60
69
  if (nav.extensionId) parts.push(`extension:${nav.extensionId}`);
70
+ if (nav.recordingId) parts.push(`recording:${nav.recordingId}`);
61
71
  return `Navigating to ${parts.join(" ")}`;
62
72
  },
63
73
  });
@@ -15,12 +15,12 @@ function resolveScope() {
15
15
 
16
16
  export default defineAction({
17
17
  description:
18
- "Query first-party analytics events recorded through this app's analytics collector endpoint (/track). Use this for questions about app/site traffic, product events, template/app usage, conversions, and other first-party event data in the analytics_events table when the user is asking about data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events only; reads are automatically scoped to the current user/org.",
18
+ "Query first-party analytics events recorded through this app's analytics collector endpoint (/track) and session replay summaries recorded through /api/analytics/replay. Use this for questions about app/site traffic, product events, template/app usage, conversions, session recordings, and other first-party data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events and session_recordings only; session_replay_chunks is intentionally unavailable, and reads are automatically scoped to the current user/org.",
19
19
  schema: z.object({
20
20
  sql: z
21
21
  .string()
22
22
  .describe(
23
- "Read-only SQL over analytics_events, e.g. SELECT event_name, COUNT(*) AS events FROM analytics_events WHERE timestamp >= '2026-05-01T04:00:00Z' AND timestamp < '2026-05-02T04:00:00Z' GROUP BY event_name ORDER BY events DESC",
23
+ "Read-only SQL over analytics_events and session_recordings, e.g. SELECT event_name, COUNT(*) AS events FROM analytics_events WHERE timestamp >= '2026-05-01T04:00:00Z' AND timestamp < '2026-05-02T04:00:00Z' GROUP BY event_name ORDER BY events DESC",
24
24
  ),
25
25
  }),
26
26
  http: false,
@@ -0,0 +1,36 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { saveDashboardReportSubscription } from "../server/lib/dashboard-report-subscriptions";
9
+
10
+ export default defineAction({
11
+ description:
12
+ "Create or update a daily email report subscription for an analytics dashboard.",
13
+ schema: z.object({
14
+ id: z.string().optional().describe("Existing subscription ID to update"),
15
+ dashboardId: z.string().describe("Dashboard ID to email"),
16
+ name: z.string().optional().describe("Human-readable subscription name"),
17
+ recipients: z.array(z.string().email()).min(1).describe("Email recipients"),
18
+ filters: z
19
+ .record(z.string(), z.string())
20
+ .optional()
21
+ .describe("Dashboard URL filters to apply, including f_ keys"),
22
+ timeOfDay: z
23
+ .string()
24
+ .regex(/^([01]\d|2[0-3]):([0-5]\d)$/)
25
+ .describe("Local send time in HH:mm"),
26
+ timezone: z.string().describe("IANA timezone for the send time"),
27
+ enabled: z.boolean().default(true),
28
+ }),
29
+ http: { method: "POST" },
30
+ run: async (args) => {
31
+ const email = getRequestUserEmail();
32
+ if (!email) throw new Error("no authenticated user");
33
+ const orgId = getRequestOrgId() || null;
34
+ return saveDashboardReportSubscription(args, { email, orgId });
35
+ },
36
+ });
@@ -0,0 +1,60 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { sendDashboardReportSubscription } from "../server/lib/dashboard-report";
9
+ import {
10
+ claimDashboardReportSubscription,
11
+ getDashboardReportSubscription,
12
+ markDashboardReportResult,
13
+ } from "../server/lib/dashboard-report-subscriptions";
14
+
15
+ export default defineAction({
16
+ description:
17
+ "Send a dashboard email report subscription immediately to its saved recipients.",
18
+ schema: z.object({
19
+ id: z.string().describe("Subscription ID to send now"),
20
+ }),
21
+ http: { method: "POST" },
22
+ needsApproval: true,
23
+ run: async (args) => {
24
+ const email = getRequestUserEmail();
25
+ if (!email) throw new Error("no authenticated user");
26
+ const orgId = getRequestOrgId() || null;
27
+ const sub = await getDashboardReportSubscription(args.id, {
28
+ email,
29
+ orgId,
30
+ });
31
+ if (!sub) {
32
+ throw Object.assign(new Error("Report subscription not found"), {
33
+ statusCode: 404,
34
+ });
35
+ }
36
+
37
+ const claimed = await claimDashboardReportSubscription(sub.id, {
38
+ email,
39
+ orgId,
40
+ });
41
+ if (!claimed) {
42
+ throw Object.assign(new Error("Report subscription is already sending"), {
43
+ statusCode: 409,
44
+ });
45
+ }
46
+
47
+ try {
48
+ const result = await sendDashboardReportSubscription(claimed);
49
+ await markDashboardReportResult(claimed, "success");
50
+ return { id: claimed.id, success: true, ...result };
51
+ } catch (err: any) {
52
+ await markDashboardReportResult(
53
+ claimed,
54
+ "error",
55
+ err?.message ?? String(err),
56
+ );
57
+ throw err;
58
+ }
59
+ },
60
+ });
@@ -12,10 +12,19 @@ import { z } from "zod";
12
12
  import { listDashboardCatalog } from "../server/lib/dashboard-catalog";
13
13
  import { getAnalysis, getDashboard } from "../server/lib/dashboards-store";
14
14
  import { listAnalyticsPublicKeys } from "../server/lib/first-party-analytics.js";
15
+ import {
16
+ getSessionReplaySummary,
17
+ listSessionRecordings,
18
+ replayRangeToIso,
19
+ type ReplayRange,
20
+ } from "../server/lib/session-replay.js";
21
+
22
+ const SESSION_FILTER_KEYS = new Set(["range", "app", "q"]);
23
+ const REPLAY_RANGES = new Set(["24h", "7d", "30d", "90d", "all"]);
15
24
 
16
25
  export default defineAction({
17
26
  description:
18
- "See what the user is currently looking at on screen. Returns the current view, dashboard config (if on a dashboard), analysis details (if on an analysis), and any active URL filter params. Prefer the auto-included <current-screen> block; call this only when you need a refreshed snapshot.",
27
+ "See what the user is currently looking at on screen. Returns the current view, dashboard config (if on a dashboard), analysis details (if on an analysis), Analytics session replay context, and any active URL filter params. Prefer the auto-included <current-screen> block; call this only when you need a refreshed snapshot.",
19
28
  schema: z.object({}),
20
29
  http: false,
21
30
  readOnly: true,
@@ -38,6 +47,13 @@ export default defineAction({
38
47
  const activeFilters: Record<string, string> = {};
39
48
  for (const [k, v] of Object.entries(url.searchParams)) {
40
49
  if (k.startsWith("f_") && v) activeFilters[k] = v;
50
+ if (
51
+ url.pathname?.startsWith("/sessions") &&
52
+ SESSION_FILTER_KEYS.has(k) &&
53
+ v
54
+ ) {
55
+ activeFilters[k] = v;
56
+ }
41
57
  }
42
58
  if (Object.keys(activeFilters).length > 0) {
43
59
  screen.activeFilters = activeFilters;
@@ -107,8 +123,34 @@ export default defineAction({
107
123
  if (nav?.extensionId) {
108
124
  screen.extensionId = nav.extensionId;
109
125
  }
126
+ } else if (nav?.view === "sessions") {
127
+ screen.page = nav?.recordingId ? "session-replay-detail" : "sessions";
128
+ const email = getRequestUserEmail();
129
+ if (email) {
130
+ const scope = { userEmail: email, orgId: getRequestOrgId() || null };
131
+ try {
132
+ if (nav?.recordingId) {
133
+ screen.sessionReplay = await getSessionReplaySummary(
134
+ nav.recordingId,
135
+ scope,
136
+ );
137
+ } else {
138
+ const params = url?.searchParams ?? {};
139
+ const sessions = await listSessionRecordings(scope, {
140
+ from:
141
+ replayRangeToIso(readReplayRange(params.range)) ?? undefined,
142
+ app: params.app,
143
+ query: params.q,
144
+ limit: 25,
145
+ });
146
+ screen.sessionReplays = sessions;
147
+ }
148
+ } catch (error: any) {
149
+ screen.sessionReplayError = error?.message || String(error);
150
+ }
151
+ }
110
152
  } else if (nav?.view === "overview" || nav?.view === "home" || !nav?.view) {
111
- screen.page = "overview";
153
+ screen.page = "ask";
112
154
  } else if (nav?.view === "ask") {
113
155
  screen.page = "ask";
114
156
  } else if (nav?.view === "query") {
@@ -161,3 +203,9 @@ export default defineAction({
161
203
  return JSON.stringify(screen, null, 2);
162
204
  },
163
205
  });
206
+
207
+ function readReplayRange(value: unknown): ReplayRange {
208
+ return typeof value === "string" && REPLAY_RANGES.has(value)
209
+ ? (value as ReplayRange)
210
+ : "30d";
211
+ }
@@ -906,7 +906,12 @@ export function SqlChart({
906
906
  const placeholderPadY = isMetric ? "py-2" : "py-8";
907
907
 
908
908
  if (!loadData || isLoading) {
909
- return <Skeleton className={`w-full flex-1 ${placeholderMinH}`} />;
909
+ return (
910
+ <Skeleton
911
+ data-dashboard-report-loading="true"
912
+ className={`w-full flex-1 ${placeholderMinH}`}
913
+ />
914
+ );
910
915
  }
911
916
 
912
917
  if (error) {
@@ -1131,6 +1136,35 @@ function renderDeltaCell(value: unknown): ReactNode {
1131
1136
  );
1132
1137
  }
1133
1138
 
1139
+ function rowString(
1140
+ row: Record<string, unknown>,
1141
+ keys: string[],
1142
+ ): string | null {
1143
+ for (const key of keys) {
1144
+ const value = row[key];
1145
+ if (typeof value === "string" && value.trim()) return value.trim();
1146
+ }
1147
+ return null;
1148
+ }
1149
+
1150
+ export function sessionReplayHref(row: Record<string, unknown>): string | null {
1151
+ const recordingId = rowString(row, [
1152
+ "recording_id",
1153
+ "session_recording_id",
1154
+ "sessionRecordingId",
1155
+ ]);
1156
+ if (recordingId) return `/sessions/${encodeURIComponent(recordingId)}`;
1157
+
1158
+ const sessionId = rowString(row, ["session_id", "sessionId"]);
1159
+ if (!sessionId) return null;
1160
+ const params = new URLSearchParams({ range: "all", q: sessionId });
1161
+ return `/sessions?${params.toString()}`;
1162
+ }
1163
+
1164
+ function isSessionColumn(key: string): boolean {
1165
+ return key === "session_id" || key === "sessionId";
1166
+ }
1167
+
1134
1168
  function TableRenderer({
1135
1169
  rows,
1136
1170
  panel,
@@ -1303,6 +1337,9 @@ function TableRenderer({
1303
1337
  col.format === "delta"
1304
1338
  ? renderDeltaCell(raw)
1305
1339
  : formatCell(raw, col.format);
1340
+ const replayHref = isSessionColumn(col.key)
1341
+ ? sessionReplayHref(row)
1342
+ : null;
1306
1343
  return (
1307
1344
  <td
1308
1345
  key={col.key}
@@ -1310,7 +1347,19 @@ function TableRenderer({
1310
1347
  numeric ? "text-right tabular-nums" : ""
1311
1348
  }`}
1312
1349
  >
1313
- {content}
1350
+ {replayHref ? (
1351
+ <span className="inline-flex flex-col gap-0.5">
1352
+ <span>{content}</span>
1353
+ <a
1354
+ href={replayHref}
1355
+ className="text-xs font-medium text-primary hover:underline"
1356
+ >
1357
+ {t("sessions.watchReplay")}
1358
+ </a>
1359
+ </span>
1360
+ ) : (
1361
+ content
1362
+ )}
1314
1363
  </td>
1315
1364
  );
1316
1365
  })}
@@ -13,11 +13,13 @@ import {
13
13
  } from "./HeaderActions";
14
14
 
15
15
  const pageTitleKeys: Record<string, string> = {
16
- "/": "navigation.overview",
16
+ "/": "navigation.ask",
17
+ "/ask": "navigation.ask",
17
18
  "/data-sources": "navigation.dataSources",
18
19
  "/data-dictionary": "navigation.dataDictionary",
19
20
  "/catalog": "navigation.templateCatalog",
20
21
  "/analyses": "navigation.analyses",
22
+ "/sessions": "navigation.sessions",
21
23
  "/dashboards/explorer": "navigation.explorer",
22
24
  "/team": "navigation.team",
23
25
  "/settings": "navigation.settings",
@@ -34,6 +36,7 @@ function resolveTitle(pathname: string, t: (key: string) => string): ReactNode {
34
36
  }
35
37
 
36
38
  if (pathname.startsWith("/analyses/")) return t("navigation.analyses");
39
+ if (pathname.startsWith("/sessions/")) return t("navigation.sessions");
37
40
 
38
41
  return t("navigation.brand");
39
42
  }
@@ -31,12 +31,14 @@ export function Layout({ children }: LayoutProps) {
31
31
  const location = useLocation();
32
32
  const navigate = useNavigate();
33
33
  const t = useT();
34
+ const reportScreenshot =
35
+ new URLSearchParams(location.search).get("reportScreenshot") === "1";
34
36
 
35
37
  // Analytics has two distinct "primary resources" — dashboards
36
38
  // (`/dashboards/:id`, legacy `/adhoc/:id`) and ad-hoc analyses
37
39
  // (`/analyses/:id`). Each binds the chat to that artifact so a dashboard
38
40
  // chat doesn't leak into a different analysis (and vice versa). The list
39
- // pages and overview leave scope null so general data questions still work.
41
+ // pages and Ask leave scope null so general data questions still work.
40
42
  const analyticsScope = useMemo(() => {
41
43
  const dashMatch = location.pathname.match(
42
44
  /^\/(?:adhoc|dashboards)\/([^/]+)/,
@@ -84,7 +86,7 @@ export function Layout({ children }: LayoutProps) {
84
86
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
85
87
  storageKey: "analytics",
86
88
  activePath: location.pathname,
87
- enabled: !isAskRoute,
89
+ enabled: !isAskRoute && !reportScreenshot,
88
90
  });
89
91
  useAgentChatHomeHandoffLinks({
90
92
  storageKey: "analytics",
@@ -101,6 +103,16 @@ export function Layout({ children }: LayoutProps) {
101
103
  return <>{children}</>;
102
104
  }
103
105
 
106
+ if (reportScreenshot) {
107
+ return (
108
+ <HeaderActionsProvider>
109
+ <main className="min-h-screen bg-background p-6 text-foreground md:p-8">
110
+ {children}
111
+ </main>
112
+ </HeaderActionsProvider>
113
+ );
114
+ }
115
+
104
116
  const contentFrame = (
105
117
  <div className="flex h-full flex-1 flex-col overflow-hidden">
106
118
  <MobileNav />