@agent-native/core 0.141.5 → 0.141.7

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 (235) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +3 -14
  3. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +20 -8
  4. package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +5 -5
  5. package/corpus/templates/analytics/AGENTS.md +2 -2
  6. package/corpus/templates/analytics/actions/compose-dashboard.ts +1 -1
  7. package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +15 -1
  8. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +135 -0
  9. package/corpus/templates/analytics/actions/navigate.ts +1 -1
  10. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +0 -1
  12. package/corpus/templates/analytics/actions/view-screen.ts +0 -20
  13. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  14. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +5 -5
  15. package/corpus/templates/analytics/app/i18n-data.ts +330 -90
  16. package/corpus/templates/analytics/app/pages/Agents.tsx +0 -3
  17. package/corpus/templates/analytics/app/pages/DataSources.tsx +66 -30
  18. package/corpus/templates/analytics/app/pages/Settings.tsx +22 -43
  19. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +23 -20
  20. package/corpus/templates/analytics/app/routes/extensions._index.tsx +1 -1
  21. package/corpus/templates/analytics/app/routes/settings.$.tsx +1 -0
  22. package/corpus/templates/analytics/app/routes/team.tsx +1 -1
  23. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-now-repair-automatically-after-.md +6 -0
  24. package/corpus/templates/analytics/changelog/2026-08-05-settings-no-longer-includes-an-unnecessary-about-section.md +6 -0
  25. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +1 -1
  26. package/corpus/templates/analytics/netlify.toml +4 -0
  27. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
  28. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery.sql +104 -0
  29. package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +342 -0
  30. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -2
  31. package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +8 -0
  32. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +11 -29
  33. package/corpus/templates/analytics/server/lib/dashboards-store.ts +55 -0
  34. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +1058 -0
  35. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +151 -46
  36. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +15 -9
  37. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +59 -17
  38. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -3
  39. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -4
  40. package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +61 -0
  41. package/corpus/templates/analytics/server/plugins/db.ts +58 -187
  42. package/corpus/templates/analytics/server/routes/api/analytics-rollups/backfill.post.ts +53 -0
  43. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +8 -6
  44. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +16 -1
  45. package/corpus/templates/assets/AGENTS.md +1 -0
  46. package/corpus/templates/assets/actions/_helpers.ts +12 -0
  47. package/corpus/templates/assets/actions/generate-image.ts +9 -3
  48. package/corpus/templates/assets/actions/import-style-from-url.ts +115 -0
  49. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
  50. package/corpus/templates/assets/app/routes/extensions._index.tsx +1 -1
  51. package/corpus/templates/assets/app/routes/settings.$.tsx +1 -0
  52. package/corpus/templates/assets/app/routes/settings.tsx +5 -0
  53. package/corpus/templates/assets/app/routes/team.tsx +1 -1
  54. package/corpus/templates/assets/changelog/2026-08-05-brand-kits-can-import-a-website-s-hydrated-visual-language-a.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-08-05-generated-images-now-hand-off-embeddable-previews-to-other-w.md +6 -0
  56. package/corpus/templates/assets/server/lib/generation.ts +58 -0
  57. package/corpus/templates/assets/shared/api.ts +12 -0
  58. package/corpus/templates/brain/app/routes/extensions._index.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/team.tsx +1 -1
  60. package/corpus/templates/brain/app/routes.ts +1 -1
  61. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  62. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +1 -1
  63. package/corpus/templates/calendar/app/routes/_app.settings.$.tsx +1 -0
  64. package/corpus/templates/calendar/app/routes/_app.team.tsx +1 -1
  65. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  66. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  67. package/corpus/templates/chat/app/root.tsx +1 -1
  68. package/corpus/templates/chat/app/routes/agent.tsx +7 -7
  69. package/corpus/templates/chat/app/routes/extensions._index.tsx +1 -1
  70. package/corpus/templates/chat/app/routes/settings.$.tsx +1 -0
  71. package/corpus/templates/chat/app/routes/team.tsx +1 -1
  72. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  73. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  74. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +1 -1
  75. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +1 -1
  76. package/corpus/templates/clips/app/routes/_app.settings.$.tsx +1 -0
  77. package/corpus/templates/clips/changelog/2026-08-05-clips-overlays-now-use-clean-surfaces-without-clipped-shadow.md +6 -0
  78. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -2
  79. package/corpus/templates/clips/desktop/src/styles.css +3 -54
  80. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -3
  81. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +1 -1
  82. package/corpus/templates/content/app/routes/_app.settings.$.tsx +1 -0
  83. package/corpus/templates/content/app/routes/_app.team.tsx +1 -1
  84. package/corpus/templates/content/e2e/global-setup.ts +1 -1
  85. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +6 -1
  86. package/corpus/templates/design/actions/analyze-brand-assets.ts +7 -7
  87. package/corpus/templates/design/actions/get-design-system.ts +3 -1
  88. package/corpus/templates/design/actions/import-from-url.ts +6 -5
  89. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +2 -2
  90. package/corpus/templates/design/app/routes/extensions._index.tsx +1 -1
  91. package/corpus/templates/design/app/routes/settings.$.tsx +1 -0
  92. package/corpus/templates/design/app/routes/team.tsx +1 -1
  93. package/corpus/templates/design/changelog/2026-08-05-missing-design-systems-now-return-a-clear-not-found-response.md +6 -0
  94. package/corpus/templates/design/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-de.md +6 -0
  95. package/corpus/templates/design/e2e/global-setup.ts +1 -1
  96. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +1 -1
  97. package/corpus/templates/dispatch/app/routes/settings.$.tsx +1 -0
  98. package/corpus/templates/dispatch/app/routes/team.tsx +1 -1
  99. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +1 -1
  100. package/corpus/templates/factory/app/routes/extensions._index.tsx +1 -1
  101. package/corpus/templates/factory/app/routes/settings.$.tsx +1 -0
  102. package/corpus/templates/factory/app/routes/team.tsx +1 -1
  103. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +1 -1
  104. package/corpus/templates/forms/app/routes/_app.settings.$.tsx +1 -0
  105. package/corpus/templates/forms/app/routes/_app.team.tsx +1 -1
  106. package/corpus/templates/forms/shared/navigation.ts +1 -1
  107. package/corpus/templates/macros/app/routes/extensions._index.tsx +1 -1
  108. package/corpus/templates/macros/app/routes/settings.$.tsx +1 -0
  109. package/corpus/templates/macros/app/routes/team.tsx +1 -1
  110. package/corpus/templates/mail/app/routes/extensions._index.tsx +1 -1
  111. package/corpus/templates/mail/app/routes/settings.$.tsx +1 -0
  112. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +1 -1
  113. package/corpus/templates/plan/app/routes/extensions._index.tsx +1 -1
  114. package/corpus/templates/plan/app/routes/settings.$.tsx +1 -0
  115. package/corpus/templates/plan/app/routes/team.tsx +1 -1
  116. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +3 -1
  117. package/corpus/templates/slides/actions/analyze-brand-assets.ts +7 -7
  118. package/corpus/templates/slides/actions/import-from-url.ts +6 -6
  119. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -2
  120. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
  121. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -0
  122. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  123. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +9 -3
  124. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -20
  125. package/corpus/templates/slides/app/global.css +6 -0
  126. package/corpus/templates/slides/app/i18n/en-US.ts +5 -5
  127. package/corpus/templates/slides/app/lib/create-deck-generation.ts +9 -5
  128. package/corpus/templates/slides/app/pages/DeckEditor.tsx +17 -0
  129. package/corpus/templates/slides/app/pages/Index.tsx +16 -5
  130. package/corpus/templates/slides/app/routes/extensions._index.tsx +1 -1
  131. package/corpus/templates/slides/app/routes/settings.$.tsx +1 -0
  132. package/corpus/templates/slides/app/routes/team.tsx +1 -1
  133. package/corpus/templates/slides/changelog/2026-08-05-failed-deck-creation-and-reference-uploads-now-recover-clean.md +6 -0
  134. package/corpus/templates/slides/changelog/2026-08-05-new-presentation-prompts-now-clearly-describe-presentation-g.md +6 -0
  135. package/corpus/templates/slides/changelog/2026-08-05-slides-recover-completed-generation-results-after-a-sync-hic.md +6 -0
  136. package/corpus/templates/slides/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-st.md +6 -0
  137. package/corpus/templates/tasks/app/components/layout/Header.tsx +3 -1
  138. package/corpus/templates/tasks/app/routes/extensions._index.tsx +1 -1
  139. package/corpus/templates/tasks/app/routes/settings.$.tsx +1 -0
  140. package/dist/agent/engine/ai-sdk-engine.js +49 -3
  141. package/dist/agent/production-agent.d.ts +1 -0
  142. package/dist/agent/production-agent.js +8 -0
  143. package/dist/agent/types.d.ts +7 -0
  144. package/dist/client/AssistantChat.js +1 -1
  145. package/dist/client/agent-chat-adapter.d.ts +1 -1
  146. package/dist/client/agent-chat-adapter.js +30 -3
  147. package/dist/client/agent-page/AgentWorkspaceContent.d.ts +1 -1
  148. package/dist/client/agent-page/AgentWorkspaceContent.js +13 -13
  149. package/dist/client/chat/run-recovery.d.ts +2 -1
  150. package/dist/client/chat/run-recovery.js +14 -3
  151. package/dist/client/embed-auth.js +6 -2
  152. package/dist/client/error-format.d.ts +1 -0
  153. package/dist/client/error-format.js +16 -2
  154. package/dist/client/integrations/IntegrationsPanel.js +40 -6
  155. package/dist/client/org/OrgSwitcher.js +2 -2
  156. package/dist/client/org/TeamPage.js +92 -72
  157. package/dist/client/require-session.d.ts +1 -1
  158. package/dist/client/require-session.js +3 -3
  159. package/dist/client/resources/use-mcp-servers.d.ts +5 -0
  160. package/dist/client/resources/use-mcp-servers.js +36 -0
  161. package/dist/client/settings/AccountSettingsCard.js +40 -35
  162. package/dist/client/settings/AutomationsSection.js +1 -1
  163. package/dist/client/settings/DemoModeSection.d.ts +3 -1
  164. package/dist/client/settings/DemoModeSection.js +4 -1
  165. package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
  166. package/dist/client/settings/SchedulingTimezoneField.d.ts +3 -1
  167. package/dist/client/settings/SchedulingTimezoneField.js +9 -5
  168. package/dist/client/settings/SettingsPanel.d.ts +4 -1
  169. package/dist/client/settings/SettingsPanel.js +21 -21
  170. package/dist/client/settings/SettingsTabsPage.d.ts +2 -2
  171. package/dist/client/settings/SettingsTabsPage.js +98 -45
  172. package/dist/client/settings/VoiceTranscriptionSection.js +6 -1
  173. package/dist/client/settings/useSettingsPanelController.js +47 -8
  174. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  175. package/dist/ingestion/website.d.ts +30 -0
  176. package/dist/localization/default-messages.d.ts +5 -0
  177. package/dist/localization/default-messages.js +5 -0
  178. package/dist/mcp-client/oauth-routes.js +1 -1
  179. package/dist/mcp-client/routes.d.ts +1 -0
  180. package/dist/mcp-client/routes.js +49 -0
  181. package/dist/navigation/index.js +16 -5
  182. package/dist/observability/routes.d.ts +3 -3
  183. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  184. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  185. package/dist/scripts/call-agent.js +11 -0
  186. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  187. package/dist/server/auth.js +9 -7
  188. package/dist/server/identity-sso.js +2 -1
  189. package/dist/server/onboarding-html.js +57 -34
  190. package/dist/server/realtime-token.d.ts +1 -1
  191. package/dist/server/transcribe-voice.d.ts +1 -1
  192. package/dist/server/workspace-provider-oauth.js +1 -1
  193. package/dist/setup-connections/onboarding.js +1 -1
  194. package/dist/shared/index.d.ts +1 -1
  195. package/dist/shared/index.js +1 -1
  196. package/dist/shared/sign-in-journey.d.ts +5 -2
  197. package/dist/shared/sign-in-journey.js +10 -4
  198. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  199. package/dist/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  200. package/dist/templates/chat/app/root.tsx +1 -1
  201. package/dist/templates/chat/app/routes/agent.tsx +7 -7
  202. package/dist/templates/chat/app/routes/extensions._index.tsx +1 -1
  203. package/dist/templates/chat/app/routes/settings.$.tsx +1 -0
  204. package/dist/templates/chat/app/routes/team.tsx +1 -1
  205. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  206. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  207. package/docs/content/authentication.mdx +4 -5
  208. package/docs/content/template-analytics-developers.mdx +1 -1
  209. package/docs/content/template-calendar-developers.mdx +1 -1
  210. package/docs/content/template-chat-developers.mdx +2 -2
  211. package/docs/content/template-content-developers.mdx +1 -1
  212. package/docs/content/template-mail-developers.mdx +1 -1
  213. package/docs/content/template-plan-developers.mdx +1 -1
  214. package/docs/content/toolkit-command-navigation.mdx +6 -6
  215. package/docs/content/toolkit-org-team.mdx +1 -1
  216. package/docs/content/toolkit-settings.mdx +8 -8
  217. package/docs/content/toolkit-setup-connections.mdx +1 -1
  218. package/package.json +1 -1
  219. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  220. package/src/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  221. package/src/templates/chat/app/root.tsx +1 -1
  222. package/src/templates/chat/app/routes/agent.tsx +7 -7
  223. package/src/templates/chat/app/routes/extensions._index.tsx +1 -1
  224. package/src/templates/chat/app/routes/settings.$.tsx +1 -0
  225. package/src/templates/chat/app/routes/team.tsx +1 -1
  226. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  227. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  228. package/corpus/templates/analytics/.agents/skills/dashboard-management/references/template-catalog-and-demo.md +0 -62
  229. package/corpus/templates/analytics/actions/install-dashboard-template.ts +0 -369
  230. package/corpus/templates/analytics/actions/list-dashboard-templates.ts +0 -47
  231. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +0 -34
  232. package/corpus/templates/analytics/app/routes/catalog.tsx +0 -240
  233. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +0 -6
  234. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +0 -6
  235. package/src/templates/default/actions/navigate.spec.ts +0 -53
@@ -4,6 +4,10 @@ import { and, gte, inArray, lte } from "drizzle-orm";
4
4
 
5
5
  import { getDb, schema } from "../db/index.js";
6
6
  import { hasCredential } from "./credentials.js";
7
+ import {
8
+ getFirstPartyAnalyticsBackend,
9
+ getFirstPartyAnalyticsBigQueryMetrics,
10
+ } from "./first-party-analytics-backend.js";
7
11
  import type { AnalyticsScope } from "./first-party-analytics.js";
8
12
 
9
13
  export const FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS = {
@@ -13,11 +17,33 @@ export const FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS = {
13
17
  recommendMaxQueryMs: 30_000,
14
18
  } as const;
15
19
 
16
- const BIGQUERY_SETUP_LINK = buildDeepLink({
17
- app: "analytics",
18
- view: "data-sources",
19
- to: "/data-sources?source=bigquery",
20
- });
20
+ const EXTERNAL_BACKEND_DEFINITIONS = [
21
+ {
22
+ id: "bigquery",
23
+ label: "BigQuery",
24
+ role: "warehouse",
25
+ requiredKeys: [
26
+ "GOOGLE_APPLICATION_CREDENTIALS_JSON",
27
+ "BIGQUERY_PROJECT_ID",
28
+ ],
29
+ setupLink: buildDeepLink({
30
+ app: "analytics",
31
+ view: "data-sources",
32
+ to: "/data-sources?source=bigquery",
33
+ }),
34
+ },
35
+ {
36
+ id: "amplitude",
37
+ label: "Amplitude",
38
+ role: "product-analytics",
39
+ requiredKeys: ["AMPLITUDE_API_KEY", "AMPLITUDE_SECRET_KEY"],
40
+ setupLink: buildDeepLink({
41
+ app: "analytics",
42
+ view: "data-sources",
43
+ to: "/data-sources?source=amplitude",
44
+ }),
45
+ },
46
+ ] as const;
21
47
 
22
48
  export type FirstPartyAnalyticsQueryClass =
23
49
  | "raw-events"
@@ -39,9 +65,29 @@ export type FirstPartyAnalyticsRecommendation =
39
65
  | "connect_bigquery"
40
66
  | "use_bigquery";
41
67
 
68
+ export type FirstPartyAnalyticsExternalBackendRecommendation =
69
+ | "none"
70
+ | "connect"
71
+ | "use"
72
+ | "unknown";
73
+
74
+ export type FirstPartyAnalyticsBackendId =
75
+ (typeof EXTERNAL_BACKEND_DEFINITIONS)[number]["id"];
76
+
77
+ export interface FirstPartyAnalyticsBackendStatus {
78
+ id: FirstPartyAnalyticsBackendId;
79
+ label: string;
80
+ role: (typeof EXTERNAL_BACKEND_DEFINITIONS)[number]["role"];
81
+ configured: boolean | null;
82
+ missingRequiredKeys: string[];
83
+ setupLink: string;
84
+ }
85
+
42
86
  export interface FirstPartyAnalyticsHealth {
43
87
  status: FirstPartyAnalyticsHealthStatus;
44
88
  recommendation: FirstPartyAnalyticsRecommendation;
89
+ externalBackendRecommendation: FirstPartyAnalyticsExternalBackendRecommendation;
90
+ externalBackends: FirstPartyAnalyticsBackendStatus[];
45
91
  reasons: Array<"event_volume" | "slow_queries" | "query_timeout">;
46
92
  observedAt: string;
47
93
  metrics: {
@@ -56,11 +102,8 @@ export interface FirstPartyAnalyticsHealth {
56
102
  maxQueryDurationMs24h: number;
57
103
  };
58
104
  thresholds: typeof FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS;
59
- bigQuery: {
60
- configured: boolean | null;
61
- missingRequiredKeys: string[];
62
- setupLink: string;
63
- };
105
+ /** Kept for clients that still read the original BigQuery-only field. */
106
+ bigQuery: FirstPartyAnalyticsBackendStatus;
64
107
  }
65
108
 
66
109
  interface QueryPressureEvent {
@@ -199,18 +242,13 @@ function dateSpanDays(
199
242
  return Math.floor((last - first) / 86_400_000) + 1;
200
243
  }
201
244
 
202
- async function bigQueryStatus(scope: AnalyticsScope): Promise<{
203
- configured: boolean | null;
204
- missingRequiredKeys: string[];
205
- setupLink: string;
206
- }> {
207
- const requiredKeys = [
208
- "GOOGLE_APPLICATION_CREDENTIALS_JSON",
209
- "BIGQUERY_PROJECT_ID",
210
- ];
245
+ async function externalBackendStatus(
246
+ scope: AnalyticsScope,
247
+ definition: (typeof EXTERNAL_BACKEND_DEFINITIONS)[number],
248
+ ): Promise<FirstPartyAnalyticsBackendStatus> {
211
249
  try {
212
250
  const configured = await Promise.all(
213
- requiredKeys.map((key) =>
251
+ definition.requiredKeys.map((key) =>
214
252
  hasCredential(key, {
215
253
  userEmail: scope.userEmail,
216
254
  orgId: scope.orgId,
@@ -218,22 +256,41 @@ async function bigQueryStatus(scope: AnalyticsScope): Promise<{
218
256
  ),
219
257
  );
220
258
  return {
259
+ id: definition.id,
260
+ label: definition.label,
261
+ role: definition.role,
221
262
  configured: configured.every(Boolean),
222
- missingRequiredKeys: requiredKeys.filter(
263
+ missingRequiredKeys: definition.requiredKeys.filter(
223
264
  (_, index) => !configured[index],
224
265
  ),
225
- setupLink: BIGQUERY_SETUP_LINK,
266
+ setupLink: definition.setupLink,
226
267
  };
227
268
  } catch (error) {
228
- console.warn("[first-party-analytics] BigQuery status unavailable:", error);
269
+ console.warn(
270
+ `[first-party-analytics] ${definition.label} status unavailable:`,
271
+ error,
272
+ );
229
273
  return {
274
+ id: definition.id,
275
+ label: definition.label,
276
+ role: definition.role,
230
277
  configured: null,
231
278
  missingRequiredKeys: [],
232
- setupLink: BIGQUERY_SETUP_LINK,
279
+ setupLink: definition.setupLink,
233
280
  };
234
281
  }
235
282
  }
236
283
 
284
+ async function externalBackendStatuses(
285
+ scope: AnalyticsScope,
286
+ ): Promise<FirstPartyAnalyticsBackendStatus[]> {
287
+ return Promise.all(
288
+ EXTERNAL_BACKEND_DEFINITIONS.map((definition) =>
289
+ externalBackendStatus(scope, definition),
290
+ ),
291
+ );
292
+ }
293
+
237
294
  function recommendationFor(
238
295
  status: FirstPartyAnalyticsHealthStatus,
239
296
  configured: boolean | null,
@@ -242,6 +299,18 @@ function recommendationFor(
242
299
  return configured === true ? "use_bigquery" : "connect_bigquery";
243
300
  }
244
301
 
302
+ function externalBackendRecommendationFor(
303
+ status: FirstPartyAnalyticsHealthStatus,
304
+ backends: FirstPartyAnalyticsBackendStatus[],
305
+ ): FirstPartyAnalyticsExternalBackendRecommendation {
306
+ if (status !== "recommend_bigquery") return "none";
307
+ if (backends.some((backend) => backend.configured === true)) return "use";
308
+ if (backends.every((backend) => backend.configured === false)) {
309
+ return "connect";
310
+ }
311
+ return "unknown";
312
+ }
313
+
245
314
  export async function getFirstPartyAnalyticsHealth(
246
315
  scope: AnalyticsScope,
247
316
  ): Promise<FirstPartyAnalyticsHealth> {
@@ -255,19 +324,30 @@ export async function getFirstPartyAnalyticsHealth(
255
324
  const rollups = schema.analyticsEventDailyRollups;
256
325
  const pressure = schema.analyticsQueryPressureDaily;
257
326
  const db = getDb() as any;
327
+ const backend = await getFirstPartyAnalyticsBackend(scope);
258
328
 
259
- const [rollupRows, pressureRows, bigQuery] = await Promise.all([
260
- db
261
- .select({
262
- eventCount: sql<number>`coalesce(sum(${rollups.eventCount}), 0)`,
263
- dailyRollupRows: sql<number>`count(*)`,
264
- firstEventDate: sql<string | null>`min(${rollups.eventDate})`,
265
- lastEventDate: sql<string | null>`max(${rollups.eventDate})`,
266
- })
267
- .from(rollups)
268
- .where(
269
- and(inArray(rollups.tenantKey, keys), lte(rollups.eventDate, today)),
270
- ),
329
+ const rollupRowsPromise =
330
+ backend.sink === "bigquery"
331
+ ? getFirstPartyAnalyticsBigQueryMetrics(scope, backend.table).then(
332
+ (metrics) => [metrics],
333
+ )
334
+ : db
335
+ .select({
336
+ eventCount: sql<number>`coalesce(sum(${rollups.eventCount}), 0)`,
337
+ dailyRollupRows: sql<number>`count(*)`,
338
+ firstEventDate: sql<string | null>`min(${rollups.eventDate})`,
339
+ lastEventDate: sql<string | null>`max(${rollups.eventDate})`,
340
+ })
341
+ .from(rollups)
342
+ .where(
343
+ and(
344
+ inArray(rollups.tenantKey, keys),
345
+ lte(rollups.eventDate, today),
346
+ ),
347
+ );
348
+
349
+ const [rollupRows, pressureRows, externalBackends] = await Promise.all([
350
+ rollupRowsPromise,
271
351
  db
272
352
  .select({
273
353
  queryClass: pressure.queryClass,
@@ -284,9 +364,14 @@ export async function getFirstPartyAnalyticsHealth(
284
364
  lte(pressure.lastSeenAt, nowTimestamp),
285
365
  ),
286
366
  ),
287
- bigQueryStatus(scope),
367
+ externalBackendStatuses(scope),
288
368
  ]);
289
369
 
370
+ const bigQuery = externalBackends.find(
371
+ (backend) => backend.id === "bigquery",
372
+ );
373
+ if (!bigQuery) throw new Error("BigQuery backend status is missing");
374
+
290
375
  const rollup = (rollupRows[0] ?? {}) as Record<string, unknown>;
291
376
  const eventCount = finiteNumber(rollup.eventCount);
292
377
  const dailyRollupRows = finiteNumber(rollup.dailyRollupRows);
@@ -339,15 +424,24 @@ export async function getFirstPartyAnalyticsHealth(
339
424
  FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS.recommendEventCount / 4 ||
340
425
  slowQueryCount24h > 0;
341
426
  const status: FirstPartyAnalyticsHealthStatus =
342
- reasons.length > 0
343
- ? "recommend_bigquery"
344
- : hasMonitorSignal
427
+ backend.sink === "bigquery"
428
+ ? slowQueryCount24h > 0 || timeoutCount24h > 0
345
429
  ? "monitor"
346
- : "healthy";
430
+ : "healthy"
431
+ : reasons.length > 0
432
+ ? "recommend_bigquery"
433
+ : hasMonitorSignal
434
+ ? "monitor"
435
+ : "healthy";
347
436
 
348
437
  return {
349
438
  status,
350
439
  recommendation: recommendationFor(status, bigQuery.configured),
440
+ externalBackendRecommendation: externalBackendRecommendationFor(
441
+ status,
442
+ externalBackends,
443
+ ),
444
+ externalBackends,
351
445
  reasons,
352
446
  observedAt: nowIso(),
353
447
  metrics: {
@@ -367,9 +461,24 @@ export async function getFirstPartyAnalyticsHealth(
367
461
  }
368
462
 
369
463
  export function unavailableFirstPartyAnalyticsHealth(): FirstPartyAnalyticsHealth {
464
+ const externalBackends = EXTERNAL_BACKEND_DEFINITIONS.map((definition) => ({
465
+ id: definition.id,
466
+ label: definition.label,
467
+ role: definition.role,
468
+ configured: null,
469
+ missingRequiredKeys: [],
470
+ setupLink: definition.setupLink,
471
+ }));
472
+ const bigQuery = externalBackends.find(
473
+ (backend) => backend.id === "bigquery",
474
+ );
475
+ if (!bigQuery) throw new Error("BigQuery backend status is missing");
476
+
370
477
  return {
371
478
  status: "unavailable",
372
479
  recommendation: "none",
480
+ externalBackendRecommendation: "unknown",
481
+ externalBackends,
373
482
  reasons: [],
374
483
  observedAt: nowIso(),
375
484
  metrics: {
@@ -384,10 +493,6 @@ export function unavailableFirstPartyAnalyticsHealth(): FirstPartyAnalyticsHealt
384
493
  maxQueryDurationMs24h: 0,
385
494
  },
386
495
  thresholds: FIRST_PARTY_ANALYTICS_PRESSURE_THRESHOLDS,
387
- bigQuery: {
388
- configured: null,
389
- missingRequiredKeys: [],
390
- setupLink: BIGQUERY_SETUP_LINK,
391
- },
496
+ bigQuery,
392
497
  };
393
498
  }
@@ -1,6 +1,11 @@
1
+ import { isPostgres } from "@agent-native/core/db";
1
2
  import { sql } from "@agent-native/core/db/schema";
2
3
 
3
4
  import { getDb, schema } from "../db/index.js";
5
+ import {
6
+ FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY,
7
+ FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
8
+ } from "./analytics-rollup-lock.js";
4
9
 
5
10
  /**
6
11
  * The normalized subset emitted by first-party analytics ingest. Keeping this
@@ -23,15 +28,6 @@ export interface FirstPartyAnalyticsRollupResult {
23
28
  userDayCount: number;
24
29
  }
25
30
 
26
- /**
27
- * Shared with the historical backfill so live ingest and migration work hold
28
- * the same transaction-scoped Postgres lock while they update rollups.
29
- */
30
- export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY =
31
- "agent-native:analytics-rollup-backfill";
32
- export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL =
33
- "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))";
34
-
35
31
  interface DailyRollupRow {
36
32
  id: string;
37
33
  tenantKey: string;
@@ -150,6 +146,16 @@ export async function upsertFirstPartyAnalyticsRollups(
150
146
  }
151
147
 
152
148
  const writeRollups = async (tx: any) => {
149
+ if (isPostgres()) {
150
+ // The historical backfill takes this lock before its raw-event
151
+ // snapshot. Holding it through ingest's rollup increment prevents the
152
+ // backfill's monotonic upsert from losing a concurrently committed
153
+ // event.
154
+ await tx.execute({
155
+ sql: FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
156
+ args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
157
+ });
158
+ }
153
159
  const dailyRows = [...dailyRollups.values()];
154
160
  await tx
155
161
  .insert(schema.analyticsEventDailyRollups)
@@ -1,4 +1,5 @@
1
- import { getDbExec, isPostgres } from "@agent-native/core/db";
1
+ import { getDbExec } from "@agent-native/core/db";
2
+ import { runWithRequestContext } from "@agent-native/core/server";
2
3
  import { and, eq, isNull, or } from "drizzle-orm";
3
4
 
4
5
  import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
@@ -8,6 +9,12 @@ import {
8
9
  ingestAnalyticsExceptionEvents,
9
10
  type DerivedExceptionFields,
10
11
  } from "./error-capture.js";
12
+ import {
13
+ getFirstPartyAnalyticsBackend,
14
+ getFirstPartyAnalyticsTable,
15
+ insertFirstPartyAnalyticsRows,
16
+ queryFirstPartyAnalyticsInBigQuery,
17
+ } from "./first-party-analytics-backend.js";
11
18
  import {
12
19
  firstPartyCacheKey,
13
20
  withFirstPartyCache,
@@ -17,11 +24,7 @@ import {
17
24
  queryOutcomeFromError,
18
25
  recordFirstPartyAnalyticsQueryPressure,
19
26
  } from "./first-party-analytics-health.js";
20
- import {
21
- FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY,
22
- FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
23
- upsertFirstPartyAnalyticsRollups,
24
- } from "./first-party-analytics-rollups.js";
27
+ import { upsertFirstPartyAnalyticsRollups } from "./first-party-analytics-rollups.js";
25
28
 
26
29
  export interface AnalyticsScope {
27
30
  userEmail: string;
@@ -508,18 +511,33 @@ export async function recordAnalyticsEvents(
508
511
  };
509
512
  });
510
513
 
511
- if (rows.length) {
514
+ const backend = await getFirstPartyAnalyticsBackend({
515
+ userEmail: key.ownerEmail,
516
+ orgId: key.orgId ?? null,
517
+ });
518
+
519
+ if (rows.length && (backend.sink === "dual" || backend.sink === "bigquery")) {
520
+ try {
521
+ await runWithRequestContext(
522
+ {
523
+ userEmail: key.ownerEmail,
524
+ orgId: key.orgId ?? undefined,
525
+ },
526
+ () => insertFirstPartyAnalyticsRows(rows, backend.table),
527
+ );
528
+ } catch (error) {
529
+ if (backend.sink === "bigquery") throw error;
530
+ // Dual-write mode keeps Postgres as the recoverable source until the
531
+ // backfill has completed. A BigQuery outage must not lose live events.
532
+ console.error(
533
+ "[first-party-analytics] BigQuery dual-write failed; retaining Postgres event:",
534
+ error,
535
+ );
536
+ }
537
+ }
538
+
539
+ if (rows.length && backend.sink !== "bigquery") {
512
540
  await db.transaction(async (tx: any) => {
513
- if (isPostgres()) {
514
- // The historical backfill takes this lock before its raw-event
515
- // snapshot. Holding it through ingest's rollup increment prevents the
516
- // backfill's monotonic upsert from losing a concurrently committed
517
- // event.
518
- await tx.execute({
519
- sql: FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
520
- args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
521
- });
522
- }
523
541
  await tx.insert(schema.analyticsEvents).values(rows);
524
542
  await tx
525
543
  .update(schema.analyticsPublicKeys)
@@ -528,6 +546,12 @@ export async function recordAnalyticsEvents(
528
546
  await upsertFirstPartyAnalyticsRollups(rows, tx);
529
547
  });
530
548
  }
549
+ if (rows.length && backend.sink === "bigquery") {
550
+ await db
551
+ .update(schema.analyticsPublicKeys)
552
+ .set({ lastUsedAt: receivedAt })
553
+ .where(eq(schema.analyticsPublicKeys.id, key.id));
554
+ }
531
555
 
532
556
  // Fork captured exceptions into the dedicated error-capture tables. This is
533
557
  // best-effort: a malformed `$exception` payload must never reject the whole
@@ -984,6 +1008,24 @@ export async function queryFirstPartyAnalytics(
984
1008
  options: AnalyticsQueryOptions = {},
985
1009
  ): Promise<AnalyticsQueryResult> {
986
1010
  validateFirstPartyAnalyticsSql(sql);
1011
+ const backend = await getFirstPartyAnalyticsBackend(scope);
1012
+ if (backend.sink === "bigquery") {
1013
+ const usesSessionRecordings = /\bsession_recordings\b/i.test(sql);
1014
+ const usesEventTables =
1015
+ /\banalytics_events\b|\banalytics_event_daily_rollups\b|\banalytics_user_days\b/i.test(
1016
+ sql,
1017
+ );
1018
+ if (usesSessionRecordings && usesEventTables) {
1019
+ throw new Error(
1020
+ "Cross-backend joins are not supported; query first-party event tables in BigQuery and session_recordings in the Analytics SQL store separately.",
1021
+ );
1022
+ }
1023
+ if (!usesSessionRecordings) {
1024
+ const table = await getFirstPartyAnalyticsTable(backend.table);
1025
+ const scoped = scopedAnalyticsSql(sql, scope);
1026
+ return queryFirstPartyAnalyticsInBigQuery(scoped.sql, scoped.args, table);
1027
+ }
1028
+ }
987
1029
  const scoped = scopedAnalyticsSql(sql, scope);
988
1030
  const wrappedSql = `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`;
989
1031
  const timeoutMs = Math.max(
@@ -65,7 +65,7 @@ export const CORPUS_REDUCTION_ACTIONS = new Set(["run-code"]);
65
65
  // hasDataQueryAttempt, but they should stop the guard from steering a
66
66
  // template-clone turn into "connect a missing source". Deliberately limited
67
67
  // to read/inspection actions: update-dashboard/mutate-dashboard/
68
- // compose-dashboard/install-dashboard-template/create-extension/
68
+ // compose-dashboard/create-extension/
69
69
  // update-extension can all author brand-new SQL or extension content, so
70
70
  // calling one of those alone is not proof the turn actually inspected a
71
71
  // template rather than inventing it from scratch. If the tool run also
@@ -74,7 +74,6 @@ export const CORPUS_REDUCTION_ACTIONS = new Set(["run-code"]);
74
74
  export const DASHBOARD_CONSTRUCTION_ACTIONS = new Set([
75
75
  "get-sql-dashboard",
76
76
  "list-sql-dashboards",
77
- "list-dashboard-templates",
78
77
  "list-extensions",
79
78
  "get-extension",
80
79
  ]);
@@ -88,7 +87,6 @@ export const DASHBOARD_MUTATION_ACTIONS = new Set([
88
87
  "mutate-dashboard",
89
88
  "update-dashboard",
90
89
  "compose-dashboard",
91
- "install-dashboard-template",
92
90
  "create-extension",
93
91
  "update-extension",
94
92
  ]);
@@ -53,7 +53,6 @@ function hasSuccessfulDashboardSave(
53
53
  "update-dashboard",
54
54
  "mutate-dashboard",
55
55
  "compose-dashboard",
56
- "install-dashboard-template",
57
56
  ]);
58
57
  return (toolResults ?? []).some((result) => {
59
58
  if (result.isError) return false;
@@ -737,9 +736,8 @@ export function realDataFinalGuard(
737
736
  'This is a dashboard construction/template-clone request. Resolve the named template\'s id (use `list-sql-dashboards` if you only have a title) and call `get-sql-dashboard` with `includeConfig: true` first. If its panels are `chartType: "extension"`, use `get-extension` then `create-extension` to clone/adapt it, then `update-dashboard` to save the new dashboard. Do not invent SQL panels for an extension-backed template. Ask one clarifying filter question if needed. Only run a data-source query before presenting numbers or authoring invented SQL.',
738
737
  fallbackMessage:
739
738
  "I need to inspect the template dashboard (and its extension, if it uses one) before creating the new one. Tell me the template dashboard name, or confirm the org/account filter, and I'll clone it without inventing metrics.",
740
- // list-sql-dashboards/list-dashboard-templates are on the initial
741
- // surface, but expand anyway so a corrective retry can always reach
742
- // the lookup/inspection tools this message asks for.
739
+ // Expand the tool surface so a corrective retry can always reach the
740
+ // lookup/inspection tools this message asks for.
743
741
  expandToolSurface: true,
744
742
  };
745
743
  }
@@ -0,0 +1,61 @@
1
+ import { runAnalyticsRollupBackfillOnce } from "../jobs/analytics-rollup-backfill";
2
+
3
+ const DEFAULT_INTERVAL_MS = 15 * 60 * 1000;
4
+ let skippingLogged = false;
5
+
6
+ declare global {
7
+ var __AGENT_NATIVE_ANALYTICS_ROLLUP_BACKFILL_SCHEDULED_RUNTIME__:
8
+ | boolean
9
+ | undefined;
10
+ }
11
+
12
+ function platformSchedulerOwnsBackfill(): boolean {
13
+ return (
14
+ process.env.NETLIFY === "true" ||
15
+ Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
16
+ globalThis.__AGENT_NATIVE_ANALYTICS_ROLLUP_BACKFILL_SCHEDULED_RUNTIME__ ===
17
+ true
18
+ );
19
+ }
20
+
21
+ function intervalMs(): number {
22
+ const raw = process.env.ANALYTICS_ROLLUP_BACKFILL_INTERVAL_MS?.trim();
23
+ if (!raw) return DEFAULT_INTERVAL_MS;
24
+ const parsed = Number.parseInt(raw, 10);
25
+ return Number.isFinite(parsed) && parsed >= 10_000
26
+ ? parsed
27
+ : DEFAULT_INTERVAL_MS;
28
+ }
29
+
30
+ export default function registerAnalyticsRollupBackfillJobs(): void {
31
+ const isProd = process.env.NODE_ENV === "production";
32
+ const flag =
33
+ process.env.ANALYTICS_ROLLUP_BACKFILL_JOBS ??
34
+ process.env.RUN_BACKGROUND_JOBS;
35
+ const enabled =
36
+ !platformSchedulerOwnsBackfill() &&
37
+ (flag === "1" || (isProd && flag !== "0"));
38
+
39
+ if (!enabled) {
40
+ if (!skippingLogged) {
41
+ console.log(
42
+ platformSchedulerOwnsBackfill()
43
+ ? "[analytics-rollups] Skipping in-process cron because the platform scheduler owns historical rollup backfills."
44
+ : "[analytics-rollups] Skipping historical rollup backfill (set ANALYTICS_ROLLUP_BACKFILL_JOBS=1 or RUN_BACKGROUND_JOBS=1 to enable in dev; on by default in production)",
45
+ );
46
+ skippingLogged = true;
47
+ }
48
+ return;
49
+ }
50
+
51
+ const ms = intervalMs();
52
+ setInterval(() => {
53
+ runAnalyticsRollupBackfillOnce().catch((err) =>
54
+ console.error("[analytics-rollups] historical backfill failed:", err),
55
+ );
56
+ }, ms);
57
+
58
+ console.log(
59
+ `[analytics-rollups] Historical rollup backfill scheduled every ${ms / 1000}s.`,
60
+ );
61
+ }