@agent-native/core 0.120.1 → 0.120.3

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 (396) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/docs/content/extensions.mdx +23 -4
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +4 -0
  6. package/corpus/core/src/agent/run-manager.ts +36 -10
  7. package/corpus/core/src/agent/types.ts +2 -0
  8. package/corpus/core/src/cli/context-xray-local.ts +1 -1
  9. package/corpus/core/src/cli/index.ts +63 -18
  10. package/corpus/core/src/cli/skills.ts +5 -0
  11. package/corpus/core/src/cli/telemetry.ts +118 -1
  12. package/corpus/core/src/client/AgentPanel.tsx +10 -0
  13. package/corpus/core/src/client/CommandMenu.tsx +13 -1
  14. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +39 -4
  15. package/corpus/core/src/client/analytics.ts +13 -0
  16. package/corpus/core/src/client/error-capture.ts +6 -1
  17. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +28 -8
  18. package/corpus/core/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  19. package/corpus/core/src/client/navigation/index.ts +2 -0
  20. package/corpus/core/src/client/org/OrgSwitcher.tsx +14 -32
  21. package/corpus/core/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  22. package/corpus/core/src/client/settings/SettingsPanel.tsx +10 -0
  23. package/corpus/core/src/deploy/build.ts +3 -0
  24. package/corpus/core/src/extensions/theme.ts +1 -1
  25. package/corpus/core/src/localization/default-messages.ts +2 -0
  26. package/corpus/core/src/server/analytics.ts +154 -25
  27. package/corpus/core/src/server/core-routes-plugin.ts +10 -1
  28. package/corpus/core/src/server/framework-request-handler.ts +17 -26
  29. package/corpus/core/src/styles/agent-native.css +13 -0
  30. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  31. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +113 -51
  32. package/corpus/core/src/templates/chat/app/global.css +1 -1
  33. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  34. package/corpus/core/src/templates/chat/netlify.toml +1 -0
  35. package/corpus/core/src/templates/default/app/global.css +1 -1
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  37. package/corpus/core/src/tracking/error-capture.ts +151 -0
  38. package/corpus/core/src/tracking/index.ts +5 -0
  39. package/corpus/core/src/vite/client.ts +6 -0
  40. package/corpus/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +27 -43
  41. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -8
  42. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +2 -2
  43. package/corpus/templates/analytics/AGENTS.md +20 -8
  44. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +3 -0
  45. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +140 -46
  46. package/corpus/templates/analytics/actions/migrate-analytics-artifacts.ts +48 -0
  47. package/corpus/templates/analytics/actions/save-analysis.ts +2 -3
  48. package/corpus/templates/analytics/actions/view-screen.ts +1 -1
  49. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +1 -1
  50. package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +1 -4
  51. package/corpus/templates/analytics/app/components/dashboard/DashboardMetadata.tsx +77 -0
  52. package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
  53. package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +1 -1
  54. package/corpus/templates/analytics/app/components/dashboard/DateRangePicker.tsx +2 -2
  55. package/corpus/templates/analytics/app/components/dashboard/MetricCard.tsx +1 -1
  56. package/corpus/templates/analytics/app/components/dashboard/RecentActivity.tsx +1 -1
  57. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +1 -1
  58. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +1 -1
  59. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +41 -9
  60. package/corpus/templates/analytics/app/components/dashboard/StatsCard.tsx +1 -1
  61. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +1 -1
  62. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +2 -0
  63. package/corpus/templates/analytics/app/components/layout/Layout.tsx +28 -14
  64. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  65. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +255 -626
  66. package/corpus/templates/analytics/app/global.css +31 -1
  67. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -2
  68. package/corpus/templates/analytics/app/i18n-data.ts +80 -0
  69. package/corpus/templates/analytics/app/lib/dashboard-report-capture.ts +42 -0
  70. package/corpus/templates/analytics/app/pages/Agents.tsx +49 -2
  71. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +1 -1
  72. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ChartTypePicker.tsx +1 -1
  73. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/DateRangePicker.tsx +1 -1
  74. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventRow.tsx +1 -1
  75. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/SqlPreview.tsx +2 -2
  76. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +54 -11
  77. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +3 -3
  78. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +2 -2
  79. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  80. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +4 -54
  81. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +178 -69
  82. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +2 -2
  83. package/corpus/templates/analytics/app/root.tsx +28 -0
  84. package/corpus/templates/analytics/app/routes/analyses._index.tsx +3 -2
  85. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  86. package/corpus/templates/analytics/changelog/2026-07-23-analytics-now-groups-saved-analyses-and-embedded-extensions-.md +6 -0
  87. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-menus-show-who-created-and-last-updated-each-dashb.md +6 -0
  88. package/corpus/templates/analytics/changelog/2026-07-23-dashboard-usage-now-loads-reliably-for-organizations-with-ma.md +6 -0
  89. package/corpus/templates/analytics/changelog/2026-07-23-long-dashboard-building-requests-now-continue-without-stoppi.md +6 -0
  90. package/corpus/templates/analytics/changelog/2026-07-24-analytics-charts-now-explain-slow-loads-and-offer-a-direct-r.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-07-24-daily-dashboard-emails-now-load-signed-reports-more-reliably.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-07-24-manage-agent-is-clearly-marked-as-a-link-to-its-dedicated-pa.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  95. package/corpus/templates/analytics/netlify.toml +4 -0
  96. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -1
  97. package/corpus/templates/analytics/server/db/schema.ts +9 -0
  98. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +21 -0
  99. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -1
  100. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +1 -0
  101. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +11 -0
  102. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +54 -0
  103. package/corpus/templates/analytics/server/lib/dashboard-report.ts +235 -26
  104. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +4 -1
  105. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +1062 -0
  106. package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -52
  107. package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -1
  108. package/corpus/templates/analytics/server/plugins/db.ts +19 -0
  109. package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +1 -1
  110. package/corpus/templates/assets/actions/generate-image-batch.ts +36 -1
  111. package/corpus/templates/assets/actions/generate-image.ts +28 -1
  112. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +376 -0
  113. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +14 -4
  114. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  115. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +164 -82
  116. package/corpus/templates/assets/app/global.css +1 -1
  117. package/corpus/templates/assets/app/i18n-data.ts +10 -0
  118. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +99 -10
  119. package/corpus/templates/assets/app/routes/extensions._index.tsx +2 -2
  120. package/corpus/templates/assets/app/routes/library.tsx +21 -198
  121. package/corpus/templates/assets/changelog/2026-07-21-unified-asset-preview-click-any-asset-to-open-one-preview-wi.md +6 -0
  122. package/corpus/templates/assets/changelog/2026-07-23-assets-image-generation-now-recovers-after-a-completed-tool-.md +6 -0
  123. package/corpus/templates/assets/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  124. package/corpus/templates/assets/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  125. package/corpus/templates/assets/netlify.toml +1 -0
  126. package/corpus/templates/brain/app/components/layout/Layout.tsx +3 -0
  127. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +114 -51
  128. package/corpus/templates/brain/app/global.css +1 -1
  129. package/corpus/templates/brain/app/routes/extensions._index.tsx +2 -2
  130. package/corpus/templates/brain/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  131. package/corpus/templates/brain/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  132. package/corpus/templates/brain/netlify.toml +1 -0
  133. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +110 -57
  134. package/corpus/templates/calendar/app/global.css +1 -1
  135. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +2 -16
  136. package/corpus/templates/calendar/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  137. package/corpus/templates/calendar/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  138. package/corpus/templates/calendar/netlify.toml +1 -0
  139. package/corpus/templates/chat/app/components/layout/Layout.tsx +3 -0
  140. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +113 -51
  141. package/corpus/templates/chat/app/global.css +1 -1
  142. package/corpus/templates/chat/app/routes/extensions._index.tsx +2 -2
  143. package/corpus/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  144. package/corpus/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  145. package/corpus/templates/chat/netlify.toml +1 -0
  146. package/corpus/templates/clips/app/components/library/library-layout.tsx +117 -37
  147. package/corpus/templates/clips/app/components/library/organization-switcher.tsx +2 -2
  148. package/corpus/templates/clips/app/components/player/video-player.tsx +16 -1
  149. package/corpus/templates/clips/app/global.css +1 -1
  150. package/corpus/templates/clips/app/hooks/use-view-tracking.ts +55 -12
  151. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +2 -16
  152. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +6 -5
  153. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -7
  154. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -5
  155. package/corpus/templates/clips/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  156. package/corpus/templates/clips/changelog/2026-07-23-meeting-recording-meters-and-dismissed-prompts.md +6 -0
  157. package/corpus/templates/clips/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  158. package/corpus/templates/clips/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  159. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +1 -1
  160. package/corpus/templates/clips/desktop/src/lib/meeting-notification-dismissal.ts +2 -0
  161. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +1 -3
  162. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +86 -0
  163. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +24 -0
  164. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +5 -69
  165. package/corpus/templates/clips/desktop/src/styles.css +35 -3
  166. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +13 -44
  167. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +119 -17
  168. package/corpus/templates/clips/netlify.toml +1 -0
  169. package/corpus/templates/clips/server/plugins/db.ts +113 -68
  170. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +69 -49
  171. package/corpus/templates/content/app/global.css +1 -1
  172. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +2 -2
  173. package/corpus/templates/content/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  174. package/corpus/templates/content/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  175. package/corpus/templates/content/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  176. package/corpus/templates/content/netlify.toml +1 -0
  177. package/corpus/templates/design/app/components/layout/Sidebar.tsx +115 -42
  178. package/corpus/templates/design/app/entry.client.tsx +14 -1
  179. package/corpus/templates/design/app/global.css +1 -1
  180. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  181. package/corpus/templates/design/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  182. package/corpus/templates/design/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  183. package/corpus/templates/design/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  184. package/corpus/templates/design/netlify.toml +1 -0
  185. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -1
  186. package/corpus/templates/dispatch/app/global.css +1 -1
  187. package/corpus/templates/dispatch/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  188. package/corpus/templates/dispatch/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  189. package/corpus/templates/dispatch/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  190. package/corpus/templates/dispatch/netlify.toml +1 -0
  191. package/corpus/templates/forms/app/components/layout/Layout.tsx +3 -0
  192. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +113 -82
  193. package/corpus/templates/forms/app/global.css +1 -1
  194. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +2 -2
  195. package/corpus/templates/forms/changelog/2026-07-23-agent-settings-are-clearly-labeled-manage-agent-in-the-sideb.md +6 -0
  196. package/corpus/templates/forms/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  197. package/corpus/templates/forms/changelog/2026-07-24-forms-sidebar-actions-now-keep-search-language-feedback-and-.md +6 -0
  198. package/corpus/templates/forms/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  199. package/corpus/templates/forms/netlify.toml +1 -0
  200. package/corpus/templates/macros/app/components/DailyProgress.tsx +5 -5
  201. package/corpus/templates/macros/app/components/ExerciseCard.tsx +2 -2
  202. package/corpus/templates/macros/app/components/MealCard.tsx +2 -2
  203. package/corpus/templates/macros/app/components/WeightCard.tsx +2 -2
  204. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +92 -34
  205. package/corpus/templates/macros/app/components/ui/button.tsx +1 -2
  206. package/corpus/templates/macros/app/components/ui/card.tsx +1 -1
  207. package/corpus/templates/macros/app/global.css +1 -1
  208. package/corpus/templates/macros/app/routes/_index.tsx +1 -1
  209. package/corpus/templates/macros/app/routes/analytics.tsx +4 -7
  210. package/corpus/templates/macros/app/routes/extensions._index.tsx +2 -2
  211. package/corpus/templates/macros/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  212. package/corpus/templates/macros/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  213. package/corpus/templates/macros/netlify.toml +1 -0
  214. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +125 -46
  215. package/corpus/templates/mail/app/global.css +1 -1
  216. package/corpus/templates/mail/app/routes/extensions._index.tsx +2 -2
  217. package/corpus/templates/mail/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  218. package/corpus/templates/mail/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  219. package/corpus/templates/mail/netlify.toml +1 -0
  220. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -0
  221. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +128 -65
  222. package/corpus/templates/plan/app/global.css +1 -1
  223. package/corpus/templates/plan/app/routes/extensions._index.tsx +2 -2
  224. package/corpus/templates/plan/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  225. package/corpus/templates/plan/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  226. package/corpus/templates/plan/netlify.toml +1 -0
  227. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +116 -36
  228. package/corpus/templates/slides/app/global.css +1 -1
  229. package/corpus/templates/slides/app/routes/extensions._index.tsx +2 -2
  230. package/corpus/templates/slides/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  231. package/corpus/templates/slides/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  232. package/corpus/templates/slides/netlify.toml +1 -0
  233. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +37 -6
  234. package/corpus/templates/tasks/app/global.css +1 -1
  235. package/corpus/templates/tasks/app/routes/extensions._index.tsx +2 -2
  236. package/corpus/templates/tasks/app/routes/settings.tsx +31 -0
  237. package/corpus/templates/tasks/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  238. package/corpus/templates/tasks/netlify.toml +1 -0
  239. package/corpus/toolkit/CHANGELOG.md +8 -0
  240. package/corpus/toolkit/README.md +8 -1
  241. package/corpus/toolkit/agent-native.eject.json +1 -1
  242. package/corpus/toolkit/package.json +1 -1
  243. package/corpus/toolkit/src/app-shell/index.ts +1 -0
  244. package/corpus/toolkit/src/app-shell/sidebar-footer-actions.tsx +48 -0
  245. package/corpus/toolkit/src/chat-history.css +52 -1
  246. package/corpus/toolkit/src/design-system/default-adapter.tsx +1 -1
  247. package/corpus/toolkit/src/ui/button.tsx +1 -2
  248. package/corpus/toolkit/src/ui/card.tsx +1 -1
  249. package/corpus/toolkit/src/ui/table.tsx +4 -11
  250. package/dist/agent/production-agent.d.ts.map +1 -1
  251. package/dist/agent/production-agent.js +4 -0
  252. package/dist/agent/production-agent.js.map +1 -1
  253. package/dist/agent/run-manager.d.ts +9 -0
  254. package/dist/agent/run-manager.d.ts.map +1 -1
  255. package/dist/agent/run-manager.js +25 -10
  256. package/dist/agent/run-manager.js.map +1 -1
  257. package/dist/agent/types.d.ts +2 -2
  258. package/dist/agent/types.d.ts.map +1 -1
  259. package/dist/agent/types.js.map +1 -1
  260. package/dist/cli/context-xray-local.js +1 -1
  261. package/dist/cli/context-xray-local.js.map +1 -1
  262. package/dist/cli/index.js +56 -17
  263. package/dist/cli/index.js.map +1 -1
  264. package/dist/cli/skills.d.ts.map +1 -1
  265. package/dist/cli/skills.js +5 -0
  266. package/dist/cli/skills.js.map +1 -1
  267. package/dist/cli/telemetry.d.ts +7 -0
  268. package/dist/cli/telemetry.d.ts.map +1 -1
  269. package/dist/cli/telemetry.js +91 -1
  270. package/dist/cli/telemetry.js.map +1 -1
  271. package/dist/client/AgentPanel.d.ts +7 -1
  272. package/dist/client/AgentPanel.d.ts.map +1 -1
  273. package/dist/client/AgentPanel.js +2 -2
  274. package/dist/client/AgentPanel.js.map +1 -1
  275. package/dist/client/CommandMenu.d.ts +2 -0
  276. package/dist/client/CommandMenu.d.ts.map +1 -1
  277. package/dist/client/CommandMenu.js +12 -1
  278. package/dist/client/CommandMenu.js.map +1 -1
  279. package/dist/client/agent-page/AgentTabsPage.d.ts +4 -0
  280. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  281. package/dist/client/agent-page/AgentTabsPage.js +8 -4
  282. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  283. package/dist/client/analytics.d.ts.map +1 -1
  284. package/dist/client/analytics.js +7 -0
  285. package/dist/client/analytics.js.map +1 -1
  286. package/dist/client/error-capture.d.ts.map +1 -1
  287. package/dist/client/error-capture.js +8 -1
  288. package/dist/client/error-capture.js.map +1 -1
  289. package/dist/client/extensions/ExtensionsListPage.d.ts +5 -1
  290. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  291. package/dist/client/extensions/ExtensionsListPage.js +6 -4
  292. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  293. package/dist/client/extensions/agent-native-extension-runtime.js +1 -1
  294. package/dist/client/extensions/agent-native-extension-runtime.js.map +1 -1
  295. package/dist/client/navigation/index.d.ts +1 -1
  296. package/dist/client/navigation/index.d.ts.map +1 -1
  297. package/dist/client/navigation/index.js +1 -1
  298. package/dist/client/navigation/index.js.map +1 -1
  299. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  300. package/dist/client/org/OrgSwitcher.js +9 -13
  301. package/dist/client/org/OrgSwitcher.js.map +1 -1
  302. package/dist/client/settings/ExtensionsSettingsContent.d.ts +2 -0
  303. package/dist/client/settings/ExtensionsSettingsContent.d.ts.map +1 -0
  304. package/dist/client/settings/ExtensionsSettingsContent.js +10 -0
  305. package/dist/client/settings/ExtensionsSettingsContent.js.map +1 -0
  306. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  307. package/dist/client/settings/SettingsPanel.js +10 -1
  308. package/dist/client/settings/SettingsPanel.js.map +1 -1
  309. package/dist/deploy/build.js +1 -0
  310. package/dist/deploy/build.js.map +1 -1
  311. package/dist/extensions/theme.js +1 -1
  312. package/dist/extensions/theme.js.map +1 -1
  313. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  314. package/dist/localization/default-messages.d.ts +2 -0
  315. package/dist/localization/default-messages.d.ts.map +1 -1
  316. package/dist/localization/default-messages.js +2 -0
  317. package/dist/localization/default-messages.js.map +1 -1
  318. package/dist/observability/routes.d.ts +3 -3
  319. package/dist/progress/routes.d.ts +1 -1
  320. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  321. package/dist/provider-api/actions/provider-api.d.ts +9 -9
  322. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  323. package/dist/resources/handlers.d.ts +1 -1
  324. package/dist/server/analytics.d.ts +9 -4
  325. package/dist/server/analytics.d.ts.map +1 -1
  326. package/dist/server/analytics.js +111 -22
  327. package/dist/server/analytics.js.map +1 -1
  328. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  329. package/dist/server/core-routes-plugin.js +8 -1
  330. package/dist/server/core-routes-plugin.js.map +1 -1
  331. package/dist/server/framework-request-handler.d.ts.map +1 -1
  332. package/dist/server/framework-request-handler.js +17 -27
  333. package/dist/server/framework-request-handler.js.map +1 -1
  334. package/dist/server/realtime-token.d.ts +1 -1
  335. package/dist/styles/agent-native.css +13 -0
  336. package/dist/templates/chat/app/components/layout/Layout.tsx +3 -0
  337. package/dist/templates/chat/app/components/layout/Sidebar.tsx +113 -51
  338. package/dist/templates/chat/app/global.css +1 -1
  339. package/dist/templates/chat/app/routes/extensions._index.tsx +2 -2
  340. package/dist/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  341. package/dist/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  342. package/dist/templates/chat/netlify.toml +1 -0
  343. package/dist/templates/default/app/global.css +1 -1
  344. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  345. package/dist/tracking/error-capture.d.ts +14 -0
  346. package/dist/tracking/error-capture.d.ts.map +1 -0
  347. package/dist/tracking/error-capture.js +113 -0
  348. package/dist/tracking/error-capture.js.map +1 -0
  349. package/dist/tracking/index.d.ts +1 -0
  350. package/dist/tracking/index.d.ts.map +1 -1
  351. package/dist/tracking/index.js +1 -0
  352. package/dist/tracking/index.js.map +1 -1
  353. package/dist/vite/client.d.ts.map +1 -1
  354. package/dist/vite/client.js +2 -0
  355. package/dist/vite/client.js.map +1 -1
  356. package/docs/content/extensions.mdx +23 -4
  357. package/package.json +2 -2
  358. package/src/agent/production-agent.ts +4 -0
  359. package/src/agent/run-manager.ts +36 -10
  360. package/src/agent/types.ts +2 -0
  361. package/src/cli/context-xray-local.ts +1 -1
  362. package/src/cli/index.ts +63 -18
  363. package/src/cli/skills.ts +5 -0
  364. package/src/cli/telemetry.ts +118 -1
  365. package/src/client/AgentPanel.tsx +10 -0
  366. package/src/client/CommandMenu.tsx +13 -1
  367. package/src/client/agent-page/AgentTabsPage.tsx +39 -4
  368. package/src/client/analytics.ts +13 -0
  369. package/src/client/error-capture.ts +6 -1
  370. package/src/client/extensions/ExtensionsListPage.tsx +28 -8
  371. package/src/client/extensions/agent-native-extension-runtime.ts +1 -1
  372. package/src/client/navigation/index.ts +2 -0
  373. package/src/client/org/OrgSwitcher.tsx +14 -32
  374. package/src/client/settings/ExtensionsSettingsContent.tsx +24 -0
  375. package/src/client/settings/SettingsPanel.tsx +10 -0
  376. package/src/deploy/build.ts +3 -0
  377. package/src/extensions/theme.ts +1 -1
  378. package/src/localization/default-messages.ts +2 -0
  379. package/src/server/analytics.ts +154 -25
  380. package/src/server/core-routes-plugin.ts +10 -1
  381. package/src/server/framework-request-handler.ts +17 -26
  382. package/src/styles/agent-native.css +13 -0
  383. package/src/templates/chat/app/components/layout/Layout.tsx +3 -0
  384. package/src/templates/chat/app/components/layout/Sidebar.tsx +113 -51
  385. package/src/templates/chat/app/global.css +1 -1
  386. package/src/templates/chat/app/routes/extensions._index.tsx +2 -2
  387. package/src/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +6 -0
  388. package/src/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +6 -0
  389. package/src/templates/chat/netlify.toml +1 -0
  390. package/src/templates/default/app/global.css +1 -1
  391. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +16 -0
  392. package/src/tracking/error-capture.ts +151 -0
  393. package/src/tracking/index.ts +5 -0
  394. package/src/vite/client.ts +6 -0
  395. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +0 -165
  396. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +0 -175
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Manage agent is clearly marked as a link to its dedicated page
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Sidebar utility controls now follow a consistent footer order.
@@ -10,9 +10,13 @@ functions = "templates/analytics/.netlify/functions-internal"
10
10
 
11
11
  [build.environment]
12
12
  GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
13
+ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
13
14
  NITRO_PRESET = "netlify"
14
15
  NPM_CONFIG_PRODUCTION = "false"
15
16
  AGENT_PROD_CODE_EXECUTION = "sandboxed"
17
+ # Analytics dashboard creation can span multiple model/tool rounds; keep those
18
+ # turns on the durable background worker instead of the short foreground wall.
19
+ AGENT_CHAT_DURABLE_BACKGROUND = "true"
16
20
 
17
21
  # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
18
22
  # Hosted agent chat uses a soft timeout before this function timeout so the
@@ -75,7 +75,7 @@
75
75
  "chartType": "area",
76
76
  "source": "first-party",
77
77
  "width": 2,
78
- "sql": "WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY event_date) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT event_date AS date, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'signup' AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template",
78
+ "sql": "WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT event_date AS date, COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown') AS template FROM analytics_events WHERE event_name = 'signup' AND (event_date <= to_char(CURRENT_DATE, 'YYYY-MM-DD') AND ('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '7 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '30d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '30 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '90d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '90 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '180d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '180 days', 'YYYY-MM-DD')) OR ('{{timeRange}}' = '365d' AND event_date >= to_char(CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))) AND ('{{emailFilter}}' IN ('', 'all') OR ('{{emailFilter}}' = 'exclude_builder' AND lower(coalesce(user_id, '')) NOT LIKE '%@builder.io') OR ('{{emailFilter}}' = 'only_builder' AND lower(coalesce(user_id, '')) LIKE '%@builder.io'))), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template",
79
79
  "config": {
80
80
  "xKey": "date",
81
81
  "yKey": "count",
@@ -67,6 +67,10 @@ export const dashboardRevisions = table(
67
67
  t.dashboardId,
68
68
  t.createdAt,
69
69
  ),
70
+ orgDashboardIdx: index("dashboard_revisions_org_dashboard_idx").on(
71
+ t.orgId,
72
+ t.dashboardId,
73
+ ),
70
74
  }),
71
75
  );
72
76
 
@@ -109,6 +113,11 @@ export const dashboardReportSubscriptions = table(
109
113
  enum: ["success", "error", "running"],
110
114
  }),
111
115
  lastError: text("last_error"),
116
+ lastCaptureAt: text("last_capture_at"),
117
+ lastCaptureMode: text("last_capture_mode", {
118
+ enum: ["full", "partial", "none"],
119
+ }),
120
+ lastCaptureError: text("last_capture_error"),
112
121
  createdAt: text("created_at").notNull().default(now()),
113
122
  updatedAt: text("updated_at").notNull().default(now()),
114
123
  ownerEmail: text("owner_email").notNull().default("local@localhost"),
@@ -5,6 +5,7 @@ import {
5
5
  claimDueDashboardReportSubscriptions,
6
6
  dashboardReportRetryAt,
7
7
  markDashboardReportResult,
8
+ recordDashboardReportCaptureOutcome,
8
9
  } from "../lib/dashboard-report-subscriptions";
9
10
 
10
11
  let running = false;
@@ -26,6 +27,24 @@ async function persistDashboardReportResult(
26
27
  }
27
28
  }
28
29
 
30
+ async function persistDashboardReportCaptureOutcome(
31
+ ...args: Parameters<typeof recordDashboardReportCaptureOutcome>
32
+ ): Promise<void> {
33
+ try {
34
+ const persisted = await recordDashboardReportCaptureOutcome(...args);
35
+ if (!persisted) {
36
+ console.warn(
37
+ `[dashboard-report] Capture checkpoint was superseded for subscription ${args[0].id}`,
38
+ );
39
+ }
40
+ } catch (err) {
41
+ console.error(
42
+ `[dashboard-report] Failed to persist capture checkpoint for subscription ${args[0].id}:`,
43
+ err instanceof Error ? err.message : String(err),
44
+ );
45
+ }
46
+ }
47
+
29
48
  function maxReportsPerSweep(): number {
30
49
  if (process.env.NETLIFY === "true") return 1;
31
50
  const raw = process.env.DASHBOARD_REPORT_SWEEP_LIMIT?.trim();
@@ -71,6 +90,8 @@ export async function runDashboardReportsOnce(): Promise<{
71
90
  () =>
72
91
  sendDashboardReportSubscription(sub, {
73
92
  skipEmailWithoutScreenshot: retryAt !== null,
93
+ onCaptureOutcome: (outcome) =>
94
+ persistDashboardReportCaptureOutcome(sub, outcome),
74
95
  ...(deliveryDeadlineAt ? { deadlineAt: deliveryDeadlineAt } : {}),
75
96
  }),
76
97
  );
@@ -15,7 +15,6 @@ export const INITIAL_TOOL_NAMES = [
15
15
  "list-session-recordings",
16
16
  "list-analyses",
17
17
  "get-analysis",
18
- "save-analysis",
19
18
  // Dashboard/extension INSPECTION stays on the initial surface so a
20
19
  // template-clone request can resolve and inspect the source on the first
21
20
  // turn. The MUTATING writers (update-dashboard, mutate-dashboard,
@@ -105,6 +105,7 @@ export function buildDashboardAgentContext(
105
105
  hiddenBy: dashboard.hiddenBy,
106
106
  createdAt: dashboard.createdAt,
107
107
  updatedAt: dashboard.updatedAt,
108
+ updatedBy: dashboard.updatedBy,
108
109
  url: `/dashboards/${dashboard.id}`,
109
110
  };
110
111
  return options.includeConfig === false ? base : { ...base, ...config };
@@ -1,4 +1,7 @@
1
1
  import {
2
+ LEGACY_SIGNUPS_OVER_TIME_SQL,
3
+ LEGACY_SEED_SIGNUPS_OVER_TIME_SQL,
4
+ SIGNUPS_OVER_TIME_SQL,
2
5
  type ExactFirstPartyPanelReplacement,
3
6
  repairFirstPartyObservedRetentionPanels,
4
7
  } from "./first-party-metric-catalog";
@@ -12,6 +15,14 @@ const NEW_VS_RECURRING_USERS_DESCRIPTION =
12
15
 
13
16
  const CANONICAL_CUSTOM_PANEL_REPLACEMENTS: readonly ExactFirstPartyPanelReplacement[] =
14
17
  [
18
+ {
19
+ id: "signups-over-time",
20
+ legacySql: [
21
+ LEGACY_SEED_SIGNUPS_OVER_TIME_SQL,
22
+ LEGACY_SIGNUPS_OVER_TIME_SQL,
23
+ ],
24
+ sql: SIGNUPS_OVER_TIME_SQL,
25
+ },
15
26
  {
16
27
  id: "new-vs-recurring-users",
17
28
  legacySql: [LEGACY_NEW_VS_RECURRING_USERS_SQL],
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  import { recordChange } from "@agent-native/core/server";
4
+ import { EMBED_TOKEN_QUERY_PARAM } from "@agent-native/core/shared";
4
5
  import { and, asc, eq, isNull, lte, or, sql } from "drizzle-orm";
5
6
 
6
7
  import { getDb, schema } from "../db/index.js";
@@ -34,12 +35,22 @@ export interface DashboardReportSubscription {
34
35
  lastRunAt: string | null;
35
36
  lastStatus: "success" | "error" | "running" | null;
36
37
  lastError: string | null;
38
+ lastCaptureAt: string | null;
39
+ lastCaptureMode: DashboardReportCaptureMode | null;
40
+ lastCaptureError: string | null;
37
41
  createdAt: string;
38
42
  updatedAt: string;
39
43
  ownerEmail: string;
40
44
  orgId: string | null;
41
45
  }
42
46
 
47
+ export type DashboardReportCaptureMode = "full" | "partial" | "none";
48
+
49
+ export interface DashboardReportCaptureOutcome {
50
+ mode: DashboardReportCaptureMode;
51
+ error?: string;
52
+ }
53
+
43
54
  export interface AccessCtx {
44
55
  email: string;
45
56
  orgId: string | null;
@@ -89,7 +100,15 @@ function nowIso(): string {
89
100
  const DASHBOARD_REPORT_ERROR_MAX_LENGTH = 2_000;
90
101
  const DASHBOARD_REPORT_ERROR_OMISSION = "\n… [truncated] …\n";
91
102
 
103
+ export function redactDashboardReportDiagnostic(value: string): string {
104
+ return value.replace(
105
+ new RegExp(`(${EMBED_TOKEN_QUERY_PARAM}=)[^&\\s]+`, "gi"),
106
+ "$1[REDACTED]",
107
+ );
108
+ }
109
+
92
110
  export function truncateDashboardReportError(error: string): string {
111
+ error = redactDashboardReportDiagnostic(error);
93
112
  if (error.length <= DASHBOARD_REPORT_ERROR_MAX_LENGTH) return error;
94
113
 
95
114
  const retainedLength =
@@ -307,6 +326,9 @@ function rowToSubscription(row: any): DashboardReportSubscription {
307
326
  lastRunAt: row.lastRunAt ?? null,
308
327
  lastStatus: row.lastStatus ?? null,
309
328
  lastError: row.lastError ?? null,
329
+ lastCaptureAt: row.lastCaptureAt ?? null,
330
+ lastCaptureMode: row.lastCaptureMode ?? null,
331
+ lastCaptureError: row.lastCaptureError ?? null,
310
332
  createdAt: row.createdAt,
311
333
  updatedAt: row.updatedAt,
312
334
  ownerEmail: row.ownerEmail,
@@ -578,3 +600,35 @@ export async function markDashboardReportResult(
578
600
  })
579
601
  .where(eq(schema.dashboardReportSubscriptions.id, sub.id));
580
602
  }
603
+
604
+ /**
605
+ * Persists the capture result before email delivery so a serverless cutoff
606
+ * cannot erase the browser diagnostics after a fallback message is accepted.
607
+ */
608
+ export async function recordDashboardReportCaptureOutcome(
609
+ sub: DashboardReportSubscription,
610
+ outcome: DashboardReportCaptureOutcome,
611
+ ): Promise<boolean> {
612
+ if (!sub.lastRunAt) return false;
613
+
614
+ const capturedAt = nowIso();
615
+ const db = getDb() as any;
616
+ const rows = await db
617
+ .update(schema.dashboardReportSubscriptions)
618
+ .set({
619
+ lastCaptureAt: capturedAt,
620
+ lastCaptureMode: outcome.mode,
621
+ lastCaptureError: outcome.error
622
+ ? truncateDashboardReportError(outcome.error)
623
+ : null,
624
+ updatedAt: capturedAt,
625
+ })
626
+ .where(
627
+ and(
628
+ eq(schema.dashboardReportSubscriptions.id, sub.id),
629
+ eq(schema.dashboardReportSubscriptions.lastRunAt, sub.lastRunAt),
630
+ ),
631
+ )
632
+ .returning();
633
+ return Boolean(rows[0]);
634
+ }
@@ -29,6 +29,7 @@ import {
29
29
  getReportDashboard,
30
30
  normalizeDashboardReportRecipients,
31
31
  type AccessCtx,
32
+ type DashboardReportCaptureOutcome,
32
33
  type DashboardReportSubscription,
33
34
  } from "./dashboard-report-subscriptions";
34
35
 
@@ -437,6 +438,7 @@ async function waitForDashboardReportReady(
437
438
  timeout: number,
438
439
  consoleErrors: string[] = [],
439
440
  failedRequests: string[] = [],
441
+ expectedPanelIds: string[] = [],
440
442
  ): Promise<boolean> {
441
443
  try {
442
444
  await page.waitForFunction(
@@ -460,22 +462,34 @@ async function waitForDashboardReportReady(
460
462
  const detail = await page
461
463
  .evaluate(`(() => {
462
464
  const root = document.querySelector("[data-dashboard-report-capture]");
465
+ const expectedPanelIds = ${JSON.stringify(expectedPanelIds.slice(0, REPORT_PANEL_STATE_DIAGNOSTICS_LIMIT))};
466
+ const cleanText = (value, limit = ${REPORT_PANEL_STATE_TEXT_LIMIT}) =>
467
+ String(value ?? "").replace(/\\s+/g, " ").trim().slice(0, limit);
468
+ const panelStates = expectedPanelIds.map((id) => {
469
+ const panel = Array.from(
470
+ root?.querySelectorAll("[data-dashboard-report-panel-id]") ?? [],
471
+ ).find((node) =>
472
+ node.getAttribute("data-dashboard-report-panel-id") === id,
473
+ );
474
+ if (!panel) return { id: cleanText(id, 96), state: "missing" };
475
+ const errorNode = panel.querySelector(
476
+ "[data-dashboard-report-panel-error='true'], .text-red-400",
477
+ );
478
+ const error = cleanText(errorNode?.textContent);
479
+ return {
480
+ id: cleanText(id, 96),
481
+ title: cleanText(panel.getAttribute("data-dashboard-report-panel-title"), 120),
482
+ state: panel.querySelector("[data-dashboard-report-loading='true']")
483
+ ? "loading"
484
+ : error
485
+ ? "errored"
486
+ : "ready",
487
+ ...(error ? { error } : {}),
488
+ };
489
+ });
463
490
  return {
464
491
  ready: root?.getAttribute("data-dashboard-report-ready") ?? null,
465
- loadingCount: root?.querySelectorAll("[data-dashboard-report-loading='true']").length ?? null,
466
- loadingPanels: Array.from(
467
- root?.querySelectorAll("[data-dashboard-report-loading='true']") ?? [],
468
- ).reduce((panels, loadingNode) => {
469
- const panel = loadingNode.closest("[data-dashboard-report-panel-id]");
470
- const id = panel?.getAttribute("data-dashboard-report-panel-id");
471
- if (!id || panels.some((entry) => entry.id === id)) return panels;
472
- panels.push({
473
- id,
474
- title: panel?.getAttribute("data-dashboard-report-panel-title") ?? "",
475
- });
476
- return panels;
477
- }, []),
478
- text: document.body?.innerText?.slice(0, 1000) ?? "",
492
+ panelStates,
479
493
  url: location.href,
480
494
  };
481
495
  })()`)
@@ -596,6 +610,93 @@ async function runBoundedBrowserCleanup(
596
610
  const DIAGNOSTICS_PROBE_TIMEOUT_MS = 2_000;
597
611
  const DIAGNOSTICS_MAX_LENGTH = 700;
598
612
  const DIAGNOSTICS_COLLECTOR_LIMIT = 5;
613
+ const REPORT_PANEL_STATE_DIAGNOSTICS_LIMIT = REPORT_PANEL_CHUNK_SIZE;
614
+ const REPORT_PANEL_STATE_TEXT_LIMIT = 160;
615
+ const DIAGNOSTICS_URL_LIMIT = 120;
616
+ const DIAGNOSTICS_TEXT_SNAPSHOT_LIMIT = 180;
617
+ const DIAGNOSTICS_REQUEST_TRACKING_LIMIT = 50;
618
+ const DIAGNOSTICS_IN_FLIGHT_SERIALIZATION_LIMIT = 3;
619
+
620
+ type InFlightReportRequest = {
621
+ method: string;
622
+ url: string;
623
+ startedAt: number;
624
+ };
625
+
626
+ type RecentReportResource = {
627
+ method: string;
628
+ url: string;
629
+ status?: number;
630
+ ageMs?: number;
631
+ outcome: "response" | "finished" | "failed";
632
+ };
633
+
634
+ function redactDiagnosticUrl(value: unknown): string {
635
+ return String(value ?? "")
636
+ .replace(
637
+ /([?&](?:__an_embed_token|token|access_token|authorization|sql|query)=)[^&\s]*/gi,
638
+ "$1[REDACTED]",
639
+ )
640
+ .slice(0, DIAGNOSTICS_URL_LIMIT);
641
+ }
642
+
643
+ function sanitizeDiagnosticText(
644
+ value: unknown,
645
+ limit = DIAGNOSTICS_TEXT_SNAPSHOT_LIMIT,
646
+ ): string {
647
+ return errorMessage(String(value ?? ""))
648
+ .replace(/\b(?:bearer\s+)[a-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
649
+ .replace(
650
+ /\b((?:access[_-]?token|auth(?:orization)?|session|cookie)\s*[:=]\s*)[^\s,;"'<>]+/gi,
651
+ "$1[REDACTED]",
652
+ )
653
+ .replace(/\s+/g, " ")
654
+ .trim()
655
+ .slice(0, limit);
656
+ }
657
+
658
+ function requestDiagnostic(req: any): InFlightReportRequest {
659
+ return {
660
+ method: sanitizeDiagnosticText(req.method?.(), 16) || "GET",
661
+ url: redactDiagnosticUrl(req.url?.()),
662
+ startedAt: Date.now(),
663
+ };
664
+ }
665
+
666
+ function requestDiagnosticPriority(url: string): number {
667
+ return /\/(?:_agent-native\/actions|auth|session)(?:\/|[?]|$)|get-sql-dashboard/i.test(
668
+ url,
669
+ )
670
+ ? 1
671
+ : 0;
672
+ }
673
+
674
+ function retainInFlightRequest(
675
+ requests: Map<any, InFlightReportRequest>,
676
+ req: any,
677
+ ) {
678
+ if (
679
+ !requests.has(req) &&
680
+ requests.size >= DIAGNOSTICS_REQUEST_TRACKING_LIMIT
681
+ ) {
682
+ const discard = [...requests.entries()].sort(([, left], [, right]) => {
683
+ const priority =
684
+ requestDiagnosticPriority(left.url) -
685
+ requestDiagnosticPriority(right.url);
686
+ return priority || left.startedAt - right.startedAt;
687
+ })[0];
688
+ if (discard) requests.delete(discard[0]);
689
+ }
690
+ requests.set(req, requestDiagnostic(req));
691
+ }
692
+
693
+ function pushRecentResource(
694
+ resources: RecentReportResource[],
695
+ resource: RecentReportResource,
696
+ ) {
697
+ if (resources.length >= DIAGNOSTICS_COLLECTOR_LIMIT) resources.shift();
698
+ resources.push(resource);
699
+ }
599
700
 
600
701
  // netlify.toml's memory = "2gb" is plan-gated and can be silently ignored, so
601
702
  // capture the actual lambda memory ceiling alongside current RSS.
@@ -614,8 +715,24 @@ async function collectPageDiagnostics(
614
715
  page: any,
615
716
  consoleErrors: string[],
616
717
  failedRequests: string[],
718
+ inFlightRequests: Map<any, InFlightReportRequest>,
719
+ recentResources: RecentReportResource[],
617
720
  ): Promise<string> {
618
721
  const memory = memoryDiagnostics();
722
+ const pendingRequests = () =>
723
+ [...inFlightRequests.values()]
724
+ .sort((left, right) => {
725
+ const priority =
726
+ requestDiagnosticPriority(right.url) -
727
+ requestDiagnosticPriority(left.url);
728
+ return priority || right.startedAt - left.startedAt;
729
+ })
730
+ .slice(0, DIAGNOSTICS_IN_FLIGHT_SERIALIZATION_LIMIT)
731
+ .map((request) => ({
732
+ method: request.method,
733
+ url: request.url,
734
+ ageMs: Math.max(0, Date.now() - request.startedAt),
735
+ }));
619
736
  try {
620
737
  let responsive = true;
621
738
  let probeTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -638,7 +755,9 @@ async function collectPageDiagnostics(
638
755
  if (!responsive) {
639
756
  return `${memory} page unresponsive (renderer hung or crashed); consoleErrors=${JSON.stringify(
640
757
  consoleErrors,
641
- )} failedRequests=${JSON.stringify(failedRequests)}`.slice(
758
+ )} failedRequests=${JSON.stringify(failedRequests)} inFlightRequests=${JSON.stringify(
759
+ pendingRequests(),
760
+ )} recentResources=${JSON.stringify(recentResources)}`.slice(
642
761
  0,
643
762
  DIAGNOSTICS_MAX_LENGTH,
644
763
  );
@@ -651,8 +770,7 @@ async function collectPageDiagnostics(
651
770
  // page may already be closed; leave url empty
652
771
  }
653
772
 
654
- let title = "";
655
- let bodyText = "";
773
+ let pageState: Record<string, unknown> = {};
656
774
  let detailsTimeout: ReturnType<typeof setTimeout> | undefined;
657
775
  try {
658
776
  // page.evaluate ignores setDefaultTimeout, so race it like the probe —
@@ -662,7 +780,21 @@ async function collectPageDiagnostics(
662
780
  page.evaluate(
663
781
  `(() => ({
664
782
  title: document.title,
665
- bodyText: document.body?.innerText?.slice(0, 240) ?? "",
783
+ readyState: document.readyState,
784
+ capture: (() => {
785
+ const root = document.querySelector("[data-dashboard-report-capture]");
786
+ return {
787
+ present: Boolean(root),
788
+ ready: root?.getAttribute("data-dashboard-report-ready") ?? null,
789
+ phase: root?.getAttribute("data-dashboard-report-phase") ?? root?.dataset?.phase ?? null,
790
+ fetchState: root?.getAttribute("data-dashboard-report-fetch-state") ?? root?.dataset?.fetchState ?? null,
791
+ error: root?.getAttribute("data-dashboard-report-error") ?? root?.dataset?.error ?? null,
792
+ childCount: root?.children.length ?? 0,
793
+ htmlLength: root?.innerHTML.length ?? 0,
794
+ textLength: root?.textContent.length ?? 0,
795
+ };
796
+ })(),
797
+ bodyTextSnapshot: document.body?.textContent?.replace(/\\s+/g, " ").trim().slice(0, ${DIAGNOSTICS_TEXT_SNAPSHOT_LIMIT}) ?? "",
666
798
  }))()`,
667
799
  ),
668
800
  ),
@@ -673,18 +805,46 @@ async function collectPageDiagnostics(
673
805
  );
674
806
  }),
675
807
  ]);
676
- title = (details as any)?.title ?? "";
677
- bodyText = (details as any)?.bodyText ?? "";
808
+ const raw = details as any;
809
+ pageState = {
810
+ title: sanitizeDiagnosticText(raw?.title),
811
+ readyState: sanitizeDiagnosticText(raw?.readyState, 32),
812
+ capture: raw?.capture
813
+ ? {
814
+ present: Boolean(raw.capture.present),
815
+ ready: sanitizeDiagnosticText(raw.capture.ready, 32) || null,
816
+ phase: sanitizeDiagnosticText(raw.capture.phase, 80) || null,
817
+ fetchState:
818
+ sanitizeDiagnosticText(raw.capture.fetchState, 80) || null,
819
+ error: sanitizeDiagnosticText(raw.capture.error) || null,
820
+ childCount: Number.isFinite(raw.capture.childCount)
821
+ ? raw.capture.childCount
822
+ : 0,
823
+ htmlLength: Number.isFinite(raw.capture.htmlLength)
824
+ ? raw.capture.htmlLength
825
+ : 0,
826
+ textLength: Number.isFinite(raw.capture.textLength)
827
+ ? raw.capture.textLength
828
+ : 0,
829
+ }
830
+ : { present: false },
831
+ bodyTextSnapshot: sanitizeDiagnosticText(raw?.bodyTextSnapshot),
832
+ };
678
833
  } catch {
679
834
  // best effort; leave title/bodyText empty
680
835
  } finally {
681
836
  if (detailsTimeout) clearTimeout(detailsTimeout);
682
837
  }
683
838
 
839
+ const { capture, readyState, title, bodyTextSnapshot } = pageState;
684
840
  return `${memory} page state: ${JSON.stringify({
685
- url,
841
+ capture,
842
+ readyState,
843
+ inFlightRequests: pendingRequests(),
844
+ recentResources,
845
+ url: redactDiagnosticUrl(url),
686
846
  title,
687
- bodyText,
847
+ bodyTextSnapshot,
688
848
  consoleErrors,
689
849
  failedRequests,
690
850
  })}`.slice(0, DIAGNOSTICS_MAX_LENGTH);
@@ -743,30 +903,64 @@ async function captureDashboardChunk(
743
903
  let captureStage = "pre-seeding the embed session";
744
904
  const consoleErrors: string[] = [];
745
905
  const failedRequests: string[] = [];
906
+ const inFlightRequests = new Map<any, InFlightReportRequest>();
907
+ const recentResources: RecentReportResource[] = [];
746
908
  page.on("console", (msg: any) => {
747
909
  if (
748
910
  msg.type() !== "error" ||
749
911
  consoleErrors.length >= DIAGNOSTICS_COLLECTOR_LIMIT
750
912
  )
751
913
  return;
752
- consoleErrors.push(msg.text().slice(0, 160));
914
+ consoleErrors.push(sanitizeDiagnosticText(msg.text()));
915
+ });
916
+ page.on("request", (req: any) => {
917
+ retainInFlightRequest(inFlightRequests, req);
753
918
  });
754
919
  page.on("requestfailed", (req: any) => {
920
+ const request = inFlightRequests.get(req) ?? requestDiagnostic(req);
921
+ inFlightRequests.delete(req);
922
+ pushRecentResource(recentResources, {
923
+ method: request.method,
924
+ url: request.url,
925
+ ageMs: Math.max(0, Date.now() - request.startedAt),
926
+ outcome: "failed",
927
+ });
755
928
  if (failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT) return;
756
929
  failedRequests.push(
757
- `${req.method()} ${req.url().slice(0, 120)}: ${req.failure()?.errorText ?? "failed"}`,
930
+ `${request.method} ${request.url}: ${sanitizeDiagnosticText(req.failure()?.errorText ?? "failed")}`,
758
931
  );
759
932
  });
760
933
  page.on("response", (res: any) => {
934
+ const req = res.request();
935
+ const request = inFlightRequests.get(req) ?? requestDiagnostic(req);
936
+ inFlightRequests.delete(req);
937
+ pushRecentResource(recentResources, {
938
+ method: request.method,
939
+ url: request.url,
940
+ status: res.status(),
941
+ ageMs: Math.max(0, Date.now() - request.startedAt),
942
+ outcome: "response",
943
+ });
761
944
  if (
762
945
  res.status() < 400 ||
763
946
  failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT
764
947
  )
765
948
  return;
766
949
  failedRequests.push(
767
- `${res.request().method()} ${res.url().slice(0, 120)}: HTTP ${res.status()}`,
950
+ `${request.method} ${request.url}: HTTP ${res.status()}`,
768
951
  );
769
952
  });
953
+ page.on("requestfinished", (req: any) => {
954
+ const request = inFlightRequests.get(req);
955
+ if (!request) return;
956
+ inFlightRequests.delete(req);
957
+ pushRecentResource(recentResources, {
958
+ method: request.method,
959
+ url: request.url,
960
+ ageMs: Math.max(0, Date.now() - request.startedAt),
961
+ outcome: "finished",
962
+ });
963
+ });
770
964
  try {
771
965
  try {
772
966
  await page.context().addCookies([
@@ -801,7 +995,13 @@ async function captureDashboardChunk(
801
995
  });
802
996
  } catch (err) {
803
997
  const diagnostics = errorMessage(
804
- await collectPageDiagnostics(page, consoleErrors, failedRequests),
998
+ await collectPageDiagnostics(
999
+ page,
1000
+ consoleErrors,
1001
+ failedRequests,
1002
+ inFlightRequests,
1003
+ recentResources,
1004
+ ),
805
1005
  );
806
1006
  throw new Error(`${errorMessage(err)}; ${diagnostics}`);
807
1007
  }
@@ -811,6 +1011,7 @@ async function captureDashboardChunk(
811
1011
  boundedStageTimeout(attempt.readyTimeout ?? timeout, deadlineAt),
812
1012
  consoleErrors,
813
1013
  failedRequests,
1014
+ expectedPanelIds,
814
1015
  );
815
1016
  captureStage = "validating the report chunk panels";
816
1017
  await assertDashboardReportPanelWindow(page, expectedPanelIds);
@@ -825,6 +1026,7 @@ async function captureDashboardChunk(
825
1026
  ),
826
1027
  consoleErrors,
827
1028
  failedRequests,
1029
+ expectedPanelIds,
828
1030
  );
829
1031
  captureStage = "revalidating the report chunk panels";
830
1032
  await assertDashboardReportPanelWindow(page, expectedPanelIds);
@@ -1138,6 +1340,9 @@ export async function sendDashboardReportSubscription(
1138
1340
  requireScreenshot?: boolean;
1139
1341
  skipEmailWithoutScreenshot?: boolean;
1140
1342
  deadlineAt?: number;
1343
+ onCaptureOutcome?: (
1344
+ outcome: DashboardReportCaptureOutcome,
1345
+ ) => Promise<void>;
1141
1346
  } = {},
1142
1347
  ): Promise<{
1143
1348
  dashboardUrl: string;
@@ -1169,6 +1374,10 @@ export async function sendDashboardReportSubscription(
1169
1374
  !options.skipEmailWithoutScreenshot && !options.requireScreenshot,
1170
1375
  captureTimeoutMs,
1171
1376
  );
1377
+ await options.onCaptureOutcome?.({
1378
+ mode: capture.mode,
1379
+ ...(capture.error ? { error: capture.error } : {}),
1380
+ });
1172
1381
  if (capture.mode !== "full" && options.requireScreenshot) {
1173
1382
  throw new Error(
1174
1383
  capture.error
@@ -221,7 +221,10 @@ export function usesFirstPartyDashboardFilters(sql: string): boolean {
221
221
  }
222
222
 
223
223
  const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}`;
224
- const SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
224
+ const LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER = `('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(7)}) OR ('{{timeRange}}' = '30d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(30)}) OR ('{{timeRange}}' = '90d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(90)}) OR ('{{timeRange}}' = '180d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(180)}) OR ('{{timeRange}}' = '365d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(365)}))`;
225
+ export const LEGACY_SEED_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
226
+ export const LEGACY_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
227
+ export const SIGNUPS_OVER_TIME_SQL = `WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
225
228
  export const LEGACY_RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= ${rollingWindowStartSql()} AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= ${RETENTION_MIN_COHORT_SIZE} ORDER BY cs.date, p.period`;
226
229
  export const LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
227
230
  export const LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;