@agent-native/core 0.141.6 → 0.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. package/agent-native.eject.json +9 -0
  2. package/corpus/README.md +1 -1
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +3 -14
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +11 -6
  5. package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +5 -5
  6. package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +19 -0
  7. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +296 -0
  8. package/corpus/templates/analytics/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  9. package/corpus/templates/analytics/.agents/skills/turn-into-skill/SKILL.md +112 -0
  10. package/corpus/templates/analytics/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  11. package/corpus/templates/analytics/AGENTS.md +37 -40
  12. package/corpus/templates/analytics/actions/compose-dashboard.ts +1 -1
  13. package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +1 -1
  14. package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +17 -22
  15. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +135 -0
  16. package/corpus/templates/analytics/actions/navigate.ts +5 -6
  17. package/corpus/templates/analytics/actions/provider-api-register.ts +50 -0
  18. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +46 -13
  19. package/corpus/templates/analytics/actions/test-custom-api-connection.ts +106 -0
  20. package/corpus/templates/analytics/actions/update-dashboard.ts +0 -1
  21. package/corpus/templates/analytics/actions/view-screen.ts +0 -20
  22. package/corpus/templates/analytics/app/components/CustomApiCard.tsx +430 -0
  23. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  24. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +5 -5
  25. package/corpus/templates/analytics/app/i18n-data.ts +682 -90
  26. package/corpus/templates/analytics/app/lib/native-mcp-exclusions.ts +6 -0
  27. package/corpus/templates/analytics/app/lib/sql-query.ts +33 -6
  28. package/corpus/templates/analytics/app/pages/Agents.tsx +0 -3
  29. package/corpus/templates/analytics/app/pages/DataSources.tsx +4 -0
  30. package/corpus/templates/analytics/app/pages/Settings.tsx +22 -43
  31. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +23 -20
  32. package/corpus/templates/analytics/app/routes/extensions._index.tsx +1 -1
  33. package/corpus/templates/analytics/app/routes/settings.$.tsx +1 -0
  34. package/corpus/templates/analytics/app/routes/team.tsx +1 -1
  35. package/corpus/templates/analytics/changelog/2026-08-05-settings-no-longer-includes-an-unnecessary-about-section.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-load-reliably-during-background-data-sw.md +6 -0
  37. package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-remain-responsive-while-historical-data.md +6 -0
  38. package/corpus/templates/analytics/changelog/2026-08-06-analytics-query-results-now-render-as-a-table-with-csv-downl.md +6 -0
  39. package/corpus/templates/analytics/changelog/2026-08-06-connect-public-https-custom-apis-and-hand-tested-endpoints-t.md +6 -0
  40. package/corpus/templates/analytics/changelog/2026-08-06-durable-agent-worker-memory.md +6 -0
  41. package/corpus/templates/analytics/changelog/2026-08-06-live-dashboard-panels-use-the-shared-query-timeout.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-08-06-on-demand-billing-is-available-as-a-native-dashboard-with-ta.md +6 -0
  43. package/corpus/templates/analytics/changelog/2026-08-06-visible-dashboard-names-are-unique.md +6 -0
  44. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +1 -1
  45. package/corpus/templates/analytics/netlify.toml +7 -0
  46. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +4 -0
  47. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery.sql +104 -0
  48. package/corpus/templates/analytics/server/db/index.ts +27 -0
  49. package/corpus/templates/analytics/server/db/schema.ts +10 -0
  50. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -2
  51. package/corpus/templates/analytics/server/lib/dashboards-store.ts +227 -60
  52. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +1147 -0
  53. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +34 -15
  54. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -4
  55. package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +227 -1
  56. package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -3
  57. package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -4
  58. package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +1 -0
  59. package/corpus/templates/analytics/server/plugins/db.ts +52 -18
  60. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +36 -8
  61. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +16 -1
  62. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +16 -1
  63. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +296 -0
  64. package/corpus/templates/assets/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  65. package/corpus/templates/assets/.agents/skills/turn-into-skill/SKILL.md +112 -0
  66. package/corpus/templates/assets/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  67. package/corpus/templates/assets/AGENTS.md +1 -0
  68. package/corpus/templates/assets/actions/_helpers.ts +12 -0
  69. package/corpus/templates/assets/actions/generate-image.ts +9 -3
  70. package/corpus/templates/assets/actions/import-style-from-url.ts +115 -0
  71. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
  72. package/corpus/templates/assets/app/routes/extensions._index.tsx +1 -1
  73. package/corpus/templates/assets/app/routes/settings.$.tsx +1 -0
  74. package/corpus/templates/assets/app/routes/settings.tsx +5 -0
  75. package/corpus/templates/assets/app/routes/team.tsx +1 -1
  76. package/corpus/templates/assets/changelog/2026-08-05-brand-kits-can-import-a-website-s-hydrated-visual-language-a.md +6 -0
  77. package/corpus/templates/assets/changelog/2026-08-05-generated-images-now-hand-off-embeddable-previews-to-other-w.md +6 -0
  78. package/corpus/templates/assets/server/lib/generation.ts +58 -0
  79. package/corpus/templates/assets/shared/api.ts +12 -0
  80. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +296 -0
  81. package/corpus/templates/brain/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  82. package/corpus/templates/brain/.agents/skills/turn-into-skill/SKILL.md +112 -0
  83. package/corpus/templates/brain/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  84. package/corpus/templates/brain/actions/list-connection-providers.ts +24 -3
  85. package/corpus/templates/brain/app/lib/dispatch-links.ts +44 -0
  86. package/corpus/templates/brain/app/routes/extensions._index.tsx +1 -1
  87. package/corpus/templates/brain/app/routes/sources.tsx +15 -7
  88. package/corpus/templates/brain/app/routes/team.tsx +1 -1
  89. package/corpus/templates/brain/app/routes.ts +1 -1
  90. package/corpus/templates/brain/changelog/2026-08-06-brain-provider-setup-links-open-dispatch-correctly-in-standa.md +6 -0
  91. package/corpus/templates/brain/server/plugins/agent-chat.ts +1 -0
  92. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +26 -0
  93. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +296 -0
  94. package/corpus/templates/calendar/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  95. package/corpus/templates/calendar/.agents/skills/turn-into-skill/SKILL.md +112 -0
  96. package/corpus/templates/calendar/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  97. package/corpus/templates/calendar/AGENTS.md +11 -32
  98. package/corpus/templates/calendar/actions/update-event.ts +107 -1
  99. package/corpus/templates/calendar/app/components/calendar/EventCalendarSelect.tsx +83 -0
  100. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +57 -0
  101. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +59 -73
  102. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  103. package/corpus/templates/calendar/app/hooks/use-events.ts +4 -1
  104. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  105. package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +1 -1
  106. package/corpus/templates/calendar/app/routes/_app.settings.$.tsx +1 -0
  107. package/corpus/templates/calendar/app/routes/_app.team.tsx +1 -1
  108. package/corpus/templates/calendar/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  109. package/corpus/templates/calendar/changelog/2026-08-06-existing-events-can-move-between-connected-google-account-ca.md +6 -0
  110. package/corpus/templates/calendar/server/lib/google-calendar.ts +101 -0
  111. package/corpus/templates/calendar/server/plugins/agent-chat.ts +7 -1
  112. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  113. package/corpus/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  114. package/corpus/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  115. package/corpus/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  116. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  117. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  118. package/corpus/templates/chat/app/root.tsx +1 -1
  119. package/corpus/templates/chat/app/routes/agent.tsx +7 -7
  120. package/corpus/templates/chat/app/routes/extensions._index.tsx +1 -1
  121. package/corpus/templates/chat/app/routes/settings.$.tsx +1 -0
  122. package/corpus/templates/chat/app/routes/team.tsx +1 -1
  123. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +296 -0
  124. package/corpus/templates/clips/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  125. package/corpus/templates/clips/.agents/skills/turn-into-skill/SKILL.md +112 -0
  126. package/corpus/templates/clips/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  127. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +9 -24
  128. package/corpus/templates/clips/app/components/editor/timeline-geometry.ts +15 -0
  129. package/corpus/templates/clips/app/components/editor/waveform.tsx +4 -17
  130. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  131. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +14 -3
  132. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  133. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +1 -1
  134. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +1 -1
  135. package/corpus/templates/clips/app/routes/_app.settings.$.tsx +1 -0
  136. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +28 -38
  137. package/corpus/templates/clips/app/routes/share.$shareId.tsx +21 -27
  138. package/corpus/templates/clips/changelog/2026-08-06-clips-recovers-from-stalled-recording-setup-without-requirin.md +6 -0
  139. package/corpus/templates/clips/changelog/2026-08-06-clips-starts-faster-after-a-deploy-or-idle-period-startup-no.md +6 -0
  140. package/corpus/templates/clips/changelog/2026-08-06-deleting-a-recording-returns-cleanly-to-the-library-and-fit-.md +6 -0
  141. package/corpus/templates/clips/changelog/2026-08-06-hide-clip-insights-from-viewers-without-analytics-access.md +6 -0
  142. package/corpus/templates/clips/desktop/src/app.tsx +48 -20
  143. package/corpus/templates/clips/desktop/src/lib/recorder.ts +275 -74
  144. package/corpus/templates/clips/desktop/src/lib/recording-start-guard.ts +84 -0
  145. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +14 -4
  146. package/corpus/templates/clips/server/plugins/db.ts +214 -26
  147. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +296 -0
  148. package/corpus/templates/content/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  149. package/corpus/templates/content/.agents/skills/turn-into-skill/SKILL.md +112 -0
  150. package/corpus/templates/content/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  151. package/corpus/templates/content/app/routes/_app.extensions._index.tsx +1 -1
  152. package/corpus/templates/content/app/routes/_app.settings.$.tsx +1 -0
  153. package/corpus/templates/content/app/routes/_app.team.tsx +1 -1
  154. package/corpus/templates/content/e2e/global-setup.ts +1 -1
  155. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +296 -0
  156. package/corpus/templates/crm/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  157. package/corpus/templates/crm/.agents/skills/turn-into-skill/SKILL.md +112 -0
  158. package/corpus/templates/crm/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  159. package/corpus/templates/crm/changelog/2026-08-06-crm-starts-faster-after-a-deploy-or-idle-period-one-time-dat.md +6 -0
  160. package/corpus/templates/crm/server/plugins/db.ts +11 -8
  161. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +36 -1
  162. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +6 -1
  163. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +296 -0
  164. package/corpus/templates/design/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  165. package/corpus/templates/design/.agents/skills/turn-into-skill/SKILL.md +112 -0
  166. package/corpus/templates/design/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  167. package/corpus/templates/design/actions/analyze-brand-assets.ts +7 -7
  168. package/corpus/templates/design/actions/import-from-url.ts +6 -5
  169. package/corpus/templates/design/app/components/design/LayersPanel.tsx +27 -18
  170. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +2 -2
  171. package/corpus/templates/design/app/pages/Index.tsx +6 -2
  172. package/corpus/templates/design/app/routes/extensions._index.tsx +1 -1
  173. package/corpus/templates/design/app/routes/settings.$.tsx +1 -0
  174. package/corpus/templates/design/app/routes/team.tsx +1 -1
  175. package/corpus/templates/design/changelog/2026-08-05-deleting-a-design-from-the-home-page-no-longer-leaves-the-ap.md +6 -0
  176. package/corpus/templates/design/changelog/2026-08-05-renaming-a-layer-now-focuses-its-name-field-automatically.md +6 -0
  177. package/corpus/templates/design/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-de.md +6 -0
  178. package/corpus/templates/design/e2e/global-setup.ts +1 -1
  179. package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
  180. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +296 -0
  181. package/corpus/templates/dispatch/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  182. package/corpus/templates/dispatch/.agents/skills/turn-into-skill/SKILL.md +112 -0
  183. package/corpus/templates/dispatch/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  184. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +1 -1
  185. package/corpus/templates/dispatch/app/routes/settings.$.tsx +1 -0
  186. package/corpus/templates/dispatch/app/routes/team.tsx +1 -1
  187. package/corpus/templates/dispatch/changelog/2026-08-06-ready-workspace-apps-stay-one-click-away-in-a-compact-dispat.md +6 -0
  188. package/corpus/templates/dispatch/changelog/2026-08-06-shared-vault-values-and-management-are-now-restricted-to-wor.md +6 -0
  189. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +296 -0
  190. package/corpus/templates/factory/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  191. package/corpus/templates/factory/.agents/skills/turn-into-skill/SKILL.md +112 -0
  192. package/corpus/templates/factory/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  193. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +1 -1
  194. package/corpus/templates/factory/app/routes/extensions._index.tsx +1 -1
  195. package/corpus/templates/factory/app/routes/settings.$.tsx +1 -0
  196. package/corpus/templates/factory/app/routes/team.tsx +1 -1
  197. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +296 -0
  198. package/corpus/templates/forms/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  199. package/corpus/templates/forms/.agents/skills/turn-into-skill/SKILL.md +112 -0
  200. package/corpus/templates/forms/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  201. package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +1 -1
  202. package/corpus/templates/forms/app/routes/_app.settings.$.tsx +1 -0
  203. package/corpus/templates/forms/app/routes/_app.team.tsx +1 -1
  204. package/corpus/templates/forms/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  205. package/corpus/templates/forms/server/plugins/agent-chat.ts +4 -0
  206. package/corpus/templates/forms/shared/navigation.ts +1 -1
  207. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +296 -0
  208. package/corpus/templates/macros/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  209. package/corpus/templates/macros/.agents/skills/turn-into-skill/SKILL.md +112 -0
  210. package/corpus/templates/macros/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  211. package/corpus/templates/macros/app/routes/extensions._index.tsx +1 -1
  212. package/corpus/templates/macros/app/routes/settings.$.tsx +1 -0
  213. package/corpus/templates/macros/app/routes/team.tsx +1 -1
  214. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +296 -0
  215. package/corpus/templates/mail/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  216. package/corpus/templates/mail/.agents/skills/turn-into-skill/SKILL.md +112 -0
  217. package/corpus/templates/mail/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  218. package/corpus/templates/mail/app/routes/extensions._index.tsx +1 -1
  219. package/corpus/templates/mail/app/routes/settings.$.tsx +1 -0
  220. package/corpus/templates/mail/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
  221. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  222. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +296 -0
  223. package/corpus/templates/plan/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  224. package/corpus/templates/plan/.agents/skills/turn-into-skill/SKILL.md +112 -0
  225. package/corpus/templates/plan/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  226. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +1 -1
  227. package/corpus/templates/plan/app/routes/extensions._index.tsx +1 -1
  228. package/corpus/templates/plan/app/routes/settings.$.tsx +1 -0
  229. package/corpus/templates/plan/app/routes/team.tsx +1 -1
  230. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +48 -5
  231. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +40 -2
  232. package/corpus/templates/slides/.agents/skills/image-generation-via-a2a/SKILL.md +6 -0
  233. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +15 -2
  234. package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +29 -24
  235. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +296 -0
  236. package/corpus/templates/slides/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  237. package/corpus/templates/slides/.agents/skills/turn-into-skill/SKILL.md +112 -0
  238. package/corpus/templates/slides/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  239. package/corpus/templates/slides/AGENTS.md +7 -9
  240. package/corpus/templates/slides/_gitignore +1 -0
  241. package/corpus/templates/slides/actions/analyze-brand-assets.ts +7 -7
  242. package/corpus/templates/slides/actions/export-pptx.ts +402 -36
  243. package/corpus/templates/slides/actions/import-file.ts +46 -85
  244. package/corpus/templates/slides/actions/import-from-url.ts +6 -6
  245. package/corpus/templates/slides/actions/import-google-slides-reference.ts +271 -12
  246. package/corpus/templates/slides/actions/import-pptx.ts +85 -60
  247. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +10 -13
  248. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
  249. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +5 -1
  250. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +25 -12
  251. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +112 -57
  252. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +202 -0
  253. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +26 -14
  254. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +176 -0
  255. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +182 -141
  256. package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
  257. package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +18 -0
  258. package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +85 -0
  259. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -0
  260. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +13 -3
  261. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +27 -1
  262. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -20
  263. package/corpus/templates/slides/app/global.css +6 -0
  264. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  265. package/corpus/templates/slides/app/lib/create-deck-generation.ts +10 -6
  266. package/corpus/templates/slides/app/lib/deck-preview-frame.ts +15 -0
  267. package/corpus/templates/slides/app/lib/deck-share-links.ts +9 -0
  268. package/corpus/templates/slides/app/lib/export-pptx-client.ts +89 -0
  269. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +3 -0
  270. package/corpus/templates/slides/app/pages/DeckEditor.tsx +17 -0
  271. package/corpus/templates/slides/app/pages/Index.tsx +107 -6
  272. package/corpus/templates/slides/app/routes/extensions._index.tsx +1 -1
  273. package/corpus/templates/slides/app/routes/settings.$.tsx +1 -0
  274. package/corpus/templates/slides/app/routes/team.tsx +1 -1
  275. package/corpus/templates/slides/changelog/2026-08-05-failed-deck-creation-and-reference-uploads-now-recover-clean.md +6 -0
  276. package/corpus/templates/slides/changelog/2026-08-05-slides-recover-completed-generation-results-after-a-sync-hic.md +6 -0
  277. package/corpus/templates/slides/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-st.md +6 -0
  278. package/corpus/templates/slides/changelog/2026-08-06-clicking-the-gray-canvas-around-a-slide-clears-its-selection.md +6 -0
  279. package/corpus/templates/slides/changelog/2026-08-06-deck-thumbnails-keep-a-consistent-listing-frame-across-aspec.md +6 -0
  280. package/corpus/templates/slides/changelog/2026-08-06-deep-linked-decks-open-directly-instead-of-being-interrupted.md +6 -0
  281. package/corpus/templates/slides/changelog/2026-08-06-google-slides-exports-now-show-clear-import-steps-when-direc.md +6 -0
  282. package/corpus/templates/slides/changelog/2026-08-06-import-pdf-google-slides-and-powerpoint-decks-directly-from-.md +6 -0
  283. package/corpus/templates/slides/changelog/2026-08-06-imported-powerpoint-exports-keep-text-and-images-editable-an.md +6 -0
  284. package/corpus/templates/slides/changelog/2026-08-06-presentation-prompts-continue-adding-rendered-slides-until-t.md +6 -0
  285. package/corpus/templates/slides/changelog/2026-08-06-public-decks-now-share-read-only-presentation-links-by-defau.md +6 -0
  286. package/corpus/templates/slides/changelog/2026-08-06-slides-offers-a-direct-connect-google-button-when-a-drive-co.md +6 -0
  287. package/corpus/templates/slides/changelog/2026-08-06-slides-preserve-uploaded-pdf-layouts-accept-larger-reference.md +6 -0
  288. package/corpus/templates/slides/changelog/2026-08-06-styled-words-stay-part-of-their-text-box-instead-of-becoming.md +6 -0
  289. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +7 -20
  290. package/corpus/templates/slides/server/handlers/import/html-converter.ts +236 -13
  291. package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +67 -0
  292. package/corpus/templates/slides/server/handlers/import/pptx-parser.ts +2 -0
  293. package/corpus/templates/slides/server/handlers/uploads.ts +7 -3
  294. package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -3
  295. package/corpus/templates/slides/server/lib/emails.ts +7 -2
  296. package/corpus/templates/slides/server/lib/google-docs-error.ts +50 -0
  297. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +1 -0
  298. package/corpus/templates/slides/server/lib/import-asset-storage.ts +127 -0
  299. package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -3
  300. package/corpus/templates/slides/server/routes/api/import-assets/[token].get.ts +70 -0
  301. package/corpus/templates/slides/shared/upload-types.ts +3 -0
  302. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +296 -0
  303. package/corpus/templates/tasks/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  304. package/corpus/templates/tasks/.agents/skills/turn-into-skill/SKILL.md +112 -0
  305. package/corpus/templates/tasks/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  306. package/corpus/templates/tasks/app/components/layout/Header.tsx +3 -1
  307. package/corpus/templates/tasks/app/routes/extensions._index.tsx +1 -1
  308. package/corpus/templates/tasks/app/routes/settings.$.tsx +1 -0
  309. package/dist/agent/engine/ai-sdk-engine.js +49 -3
  310. package/dist/agent/production-agent.d.ts +18 -0
  311. package/dist/agent/production-agent.js +80 -1
  312. package/dist/agent/types.d.ts +7 -0
  313. package/dist/cli/agent-plugin.d.ts +112 -0
  314. package/dist/cli/agent-plugin.js +1081 -0
  315. package/dist/cli/app-skill.js +39 -1
  316. package/dist/cli/code-agent-executor.d.ts +3 -0
  317. package/dist/cli/code-agent-executor.js +51 -13
  318. package/dist/cli/index.js +13 -0
  319. package/dist/cli/templates-meta.js +1 -1
  320. package/dist/client/AgentPanel.d.ts +3 -1
  321. package/dist/client/AgentPanel.js +9 -2
  322. package/dist/client/AssistantChat.d.ts +8 -0
  323. package/dist/client/AssistantChat.js +23 -8
  324. package/dist/client/NewWorkspaceAppFlow.js +1 -0
  325. package/dist/client/agent-chat-adapter.d.ts +1 -1
  326. package/dist/client/agent-chat-adapter.js +153 -7
  327. package/dist/client/agent-page/AgentWorkspaceContent.d.ts +1 -1
  328. package/dist/client/agent-page/AgentWorkspaceContent.js +13 -13
  329. package/dist/client/assistant-ui-recovery.d.ts +2 -1
  330. package/dist/client/assistant-ui-recovery.js +7 -3
  331. package/dist/client/chat/run-recovery.js +5 -1
  332. package/dist/client/embed-auth.js +6 -2
  333. package/dist/client/error-format.js +12 -0
  334. package/dist/client/integrations/IntegrationsPanel.js +40 -6
  335. package/dist/client/onboarding/FirstRunOnboarding.js +11 -4
  336. package/dist/client/org/OrgSwitcher.js +2 -2
  337. package/dist/client/org/TeamPage.js +92 -72
  338. package/dist/client/require-session.d.ts +1 -1
  339. package/dist/client/require-session.js +4 -4
  340. package/dist/client/resources/McpConnectionSuggestion.d.ts +1 -0
  341. package/dist/client/resources/McpConnectionSuggestion.js +27 -10
  342. package/dist/client/resources/McpIntegrationDialog.js +77 -45
  343. package/dist/client/resources/mcp-integration-catalog.d.ts +13 -1
  344. package/dist/client/resources/mcp-integration-catalog.js +255 -2
  345. package/dist/client/resources/mcp-integration-logos.js +18 -2
  346. package/dist/client/resources/use-mcp-servers.d.ts +5 -0
  347. package/dist/client/resources/use-mcp-servers.js +36 -0
  348. package/dist/client/settings/AccountSettingsCard.js +42 -36
  349. package/dist/client/settings/AutomationsSection.js +1 -1
  350. package/dist/client/settings/DemoModeSection.d.ts +3 -1
  351. package/dist/client/settings/DemoModeSection.js +4 -1
  352. package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
  353. package/dist/client/settings/SchedulingTimezoneField.d.ts +3 -1
  354. package/dist/client/settings/SchedulingTimezoneField.js +9 -5
  355. package/dist/client/settings/SettingsPanel.d.ts +4 -1
  356. package/dist/client/settings/SettingsPanel.js +21 -21
  357. package/dist/client/settings/SettingsTabsPage.d.ts +2 -2
  358. package/dist/client/settings/SettingsTabsPage.js +98 -45
  359. package/dist/client/settings/VoiceTranscriptionSection.js +6 -1
  360. package/dist/client/settings/useSettingsPanelController.js +47 -8
  361. package/dist/collab/routes.d.ts +1 -1
  362. package/dist/db/client.d.ts +9 -0
  363. package/dist/db/client.js +51 -16
  364. package/dist/db/create-get-db.js +2 -2
  365. package/dist/db/ddl-guard.d.ts +11 -10
  366. package/dist/db/ddl-guard.js +40 -12
  367. package/dist/deploy/build.d.ts +2 -0
  368. package/dist/deploy/build.js +97 -4
  369. package/dist/deploy/workspace-deploy.js +0 -2
  370. package/dist/guards/no-env-credentials.js +9 -0
  371. package/dist/ingestion/index.d.ts +1 -1
  372. package/dist/ingestion/pptx.d.ts +68 -0
  373. package/dist/ingestion/pptx.js +660 -213
  374. package/dist/ingestion/website.d.ts +30 -0
  375. package/dist/integrations/plugin.js +9 -6
  376. package/dist/localization/default-messages.d.ts +59 -0
  377. package/dist/localization/default-messages.js +62 -2
  378. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  379. package/dist/mcp-client/oauth-routes.d.ts +2 -0
  380. package/dist/mcp-client/oauth-routes.js +74 -7
  381. package/dist/mcp-client/routes.d.ts +1 -0
  382. package/dist/mcp-client/routes.js +49 -0
  383. package/dist/navigation/index.js +16 -5
  384. package/dist/notifications/routes.d.ts +1 -1
  385. package/dist/observability/routes.d.ts +3 -3
  386. package/dist/onboarding/app-profile.js +11 -0
  387. package/dist/progress/routes.d.ts +1 -1
  388. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  389. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  390. package/dist/resources/store.js +15 -1
  391. package/dist/scripts/call-agent.js +45 -4
  392. package/dist/secrets/routes.d.ts +6 -6
  393. package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
  394. package/dist/server/agent-chat/action-filters-a2a.js +36 -1
  395. package/dist/server/agent-chat-plugin.js +29 -8
  396. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  397. package/dist/server/auth.js +55 -17
  398. package/dist/server/better-auth-instance.js +5 -3
  399. package/dist/server/core-routes-plugin.d.ts +8 -1
  400. package/dist/server/core-routes-plugin.js +28 -3
  401. package/dist/server/credential-provider.d.ts +3 -3
  402. package/dist/server/credential-provider.js +90 -4
  403. package/dist/server/identity-sso.js +2 -1
  404. package/dist/server/onboarding-html.d.ts +2 -0
  405. package/dist/server/onboarding-html.js +171 -62
  406. package/dist/server/realtime-token.d.ts +1 -1
  407. package/dist/server/self-dispatch.d.ts +5 -4
  408. package/dist/server/self-dispatch.js +25 -14
  409. package/dist/server/ssr-handler.d.ts +1 -1
  410. package/dist/server/ssr-handler.js +1 -1
  411. package/dist/server/workspace-provider-oauth.js +1 -1
  412. package/dist/setup-connections/onboarding.js +1 -1
  413. package/dist/shared/cache-control.d.ts +26 -2
  414. package/dist/shared/cache-control.js +43 -2
  415. package/dist/shared/index.d.ts +1 -1
  416. package/dist/shared/index.js +1 -1
  417. package/dist/shared/password-policy.d.ts +3 -0
  418. package/dist/shared/password-policy.js +3 -0
  419. package/dist/shared/sign-in-journey.d.ts +5 -2
  420. package/dist/shared/sign-in-journey.js +10 -4
  421. package/dist/sharing/actions/set-resource-visibility.js +16 -4
  422. package/dist/sharing/registry.d.ts +14 -0
  423. package/dist/styles/agent-native.css +6 -0
  424. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  425. package/dist/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  426. package/dist/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  427. package/dist/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  428. package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
  429. package/dist/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  430. package/dist/templates/chat/app/root.tsx +1 -1
  431. package/dist/templates/chat/app/routes/agent.tsx +7 -7
  432. package/dist/templates/chat/app/routes/extensions._index.tsx +1 -1
  433. package/dist/templates/chat/app/routes/settings.$.tsx +1 -0
  434. package/dist/templates/chat/app/routes/team.tsx +1 -1
  435. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
  436. package/dist/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  437. package/dist/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
  438. package/dist/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  439. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
  440. package/dist/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  441. package/dist/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
  442. package/dist/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  443. package/dist/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
  444. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  445. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  446. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
  447. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
  448. package/dist/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  449. package/dist/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
  450. package/dist/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  451. package/dist/templates/workspace-root/AGENTS.md +7 -0
  452. package/dist/user-profile/actions/change-password.js +8 -3
  453. package/dist/user-profile/actions/set-password.js +5 -1
  454. package/dist/vite/action-types-plugin.js +59 -0
  455. package/docs/content/authentication.mdx +4 -5
  456. package/docs/content/skills-guide.mdx +13 -0
  457. package/docs/content/template-analytics-developers.mdx +1 -1
  458. package/docs/content/template-calendar-developers.mdx +1 -1
  459. package/docs/content/template-chat-developers.mdx +2 -2
  460. package/docs/content/template-content-developers.mdx +1 -1
  461. package/docs/content/template-mail-developers.mdx +1 -1
  462. package/docs/content/template-plan-developers.mdx +1 -1
  463. package/docs/content/toolkit-command-navigation.mdx +6 -6
  464. package/docs/content/toolkit-org-team.mdx +1 -1
  465. package/docs/content/toolkit-settings.mdx +8 -8
  466. package/docs/content/toolkit-setup-connections.mdx +1 -1
  467. package/package.json +6 -2
  468. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
  469. package/src/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  470. package/src/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
  471. package/src/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  472. package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
  473. package/src/templates/chat/app/hooks/use-navigation-state.ts +1 -1
  474. package/src/templates/chat/app/root.tsx +1 -1
  475. package/src/templates/chat/app/routes/agent.tsx +7 -7
  476. package/src/templates/chat/app/routes/extensions._index.tsx +1 -1
  477. package/src/templates/chat/app/routes/settings.$.tsx +1 -0
  478. package/src/templates/chat/app/routes/team.tsx +1 -1
  479. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
  480. package/src/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  481. package/src/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
  482. package/src/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  483. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
  484. package/src/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  485. package/src/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
  486. package/src/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  487. package/src/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
  488. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
  489. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
  490. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
  491. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
  492. package/src/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
  493. package/src/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
  494. package/src/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
  495. package/src/templates/workspace-root/AGENTS.md +7 -0
  496. package/corpus/templates/analytics/.agents/skills/dashboard-management/references/template-catalog-and-demo.md +0 -62
  497. package/corpus/templates/analytics/actions/install-dashboard-template.ts +0 -369
  498. package/corpus/templates/analytics/actions/list-dashboard-templates.ts +0 -47
  499. package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +0 -34
  500. package/corpus/templates/analytics/app/routes/catalog.tsx +0 -240
  501. package/src/templates/default/actions/navigate.spec.ts +0 -53
@@ -0,0 +1,1147 @@
1
+ import { getDbExec } from "@agent-native/core/db";
2
+
3
+ import {
4
+ getBigQueryProjectId,
5
+ runQuery,
6
+ type BigQueryTableRef,
7
+ } from "./bigquery.js";
8
+ import { requireRequestCredentialContext } from "./credentials-context.js";
9
+ import { getAccessToken } from "./gcloud.js";
10
+ import {
11
+ getScopedSettingRecord,
12
+ putScopedSettingRecord,
13
+ } from "./scoped-settings.js";
14
+
15
+ export const FIRST_PARTY_ANALYTICS_BACKEND_SETTING =
16
+ "first-party-analytics-backend";
17
+
18
+ export type FirstPartyAnalyticsSink = "postgres" | "dual" | "bigquery";
19
+
20
+ export interface FirstPartyAnalyticsBackendConfig {
21
+ sink: FirstPartyAnalyticsSink;
22
+ table: string | null;
23
+ backfillCursor?: string | null;
24
+ backfillCompleted?: boolean;
25
+ }
26
+
27
+ interface FirstPartyAnalyticsBackfillCursor {
28
+ receivedAt: string;
29
+ id: string;
30
+ }
31
+
32
+ export interface FirstPartyAnalyticsScope {
33
+ userEmail: string;
34
+ orgId: string | null;
35
+ }
36
+
37
+ interface FirstPartyAnalyticsBackendSetting {
38
+ sink?: unknown;
39
+ table?: unknown;
40
+ backfillCursor?: unknown;
41
+ backfillCompleted?: unknown;
42
+ }
43
+
44
+ interface FirstPartyAnalyticsEventRow {
45
+ id: string;
46
+ publicKeyId: string;
47
+ eventName: string;
48
+ userId: string | null;
49
+ anonymousId: string | null;
50
+ userKey: string | null;
51
+ sessionId: string | null;
52
+ timestamp: string;
53
+ eventDate: string | null;
54
+ receivedAt: string;
55
+ url: string | null;
56
+ path: string | null;
57
+ hostname: string | null;
58
+ referrer: string | null;
59
+ app: string | null;
60
+ template: string | null;
61
+ signedIn: string | null;
62
+ properties: string;
63
+ context: string;
64
+ ownerEmail: string;
65
+ orgId: string | null;
66
+ }
67
+
68
+ const TABLE_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
69
+ const PROJECT_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]{4,61}[A-Za-z0-9]$/;
70
+ const FIRST_PARTY_QUERY_TABLES = [
71
+ "analytics_events",
72
+ "analytics_event_daily_rollups",
73
+ "analytics_user_days",
74
+ ] as const;
75
+ const BACKEND_CONFIG_CACHE_TTL_MS = 30_000;
76
+
77
+ const backendConfigCache = new Map<
78
+ string,
79
+ { config: FirstPartyAnalyticsBackendConfig; expiresAt: number }
80
+ >();
81
+
82
+ function backendScopeKey(scope: FirstPartyAnalyticsScope): string {
83
+ return `${scope.orgId ? `o:${scope.orgId}` : "u:"}${scope.userEmail}`;
84
+ }
85
+
86
+ function parseTableRef(
87
+ raw: string | null | undefined,
88
+ fallbackProjectId: string,
89
+ ): BigQueryTableRef {
90
+ const value = raw?.trim().replace(/^`|`$/g, "");
91
+ const parts = value ? value.split(".") : [];
92
+ const [projectId, datasetId, tableId] =
93
+ parts.length === 3
94
+ ? parts
95
+ : parts.length === 2
96
+ ? [fallbackProjectId, parts[0], parts[1]]
97
+ : [fallbackProjectId, "analytics", "first_party_analytics_events_raw"];
98
+
99
+ if (
100
+ !PROJECT_ID_PATTERN.test(projectId) ||
101
+ !TABLE_ID_PATTERN.test(datasetId) ||
102
+ !TABLE_ID_PATTERN.test(tableId)
103
+ ) {
104
+ throw new Error(
105
+ "The first-party BigQuery table must be dataset.table or project.dataset.table",
106
+ );
107
+ }
108
+
109
+ return {
110
+ projectId,
111
+ datasetId,
112
+ tableId,
113
+ fullyQualified: `${projectId}.${datasetId}.${tableId}`,
114
+ };
115
+ }
116
+
117
+ function normalizeSink(value: unknown): FirstPartyAnalyticsSink {
118
+ return value === "dual" || value === "bigquery" ? value : "postgres";
119
+ }
120
+
121
+ export async function getFirstPartyAnalyticsBackend(
122
+ scope: FirstPartyAnalyticsScope,
123
+ ): Promise<FirstPartyAnalyticsBackendConfig> {
124
+ const cacheKey = backendScopeKey(scope);
125
+ const cached = backendConfigCache.get(cacheKey);
126
+ if (cached && cached.expiresAt > Date.now()) return cached.config;
127
+
128
+ const setting = (await getScopedSettingRecord(
129
+ { email: scope.userEmail, orgId: scope.orgId },
130
+ FIRST_PARTY_ANALYTICS_BACKEND_SETTING,
131
+ )) as FirstPartyAnalyticsBackendSetting | null;
132
+ const config = {
133
+ sink: normalizeSink(setting?.sink),
134
+ table: typeof setting?.table === "string" ? setting.table : null,
135
+ backfillCursor:
136
+ typeof setting?.backfillCursor === "string"
137
+ ? setting.backfillCursor
138
+ : null,
139
+ backfillCompleted: setting?.backfillCompleted === true,
140
+ };
141
+ backendConfigCache.set(cacheKey, {
142
+ config,
143
+ expiresAt: Date.now() + BACKEND_CONFIG_CACHE_TTL_MS,
144
+ });
145
+ return config;
146
+ }
147
+
148
+ export async function saveFirstPartyAnalyticsBackend(
149
+ scope: FirstPartyAnalyticsScope,
150
+ config: FirstPartyAnalyticsBackendConfig,
151
+ ): Promise<void> {
152
+ await putScopedSettingRecord(
153
+ { email: scope.userEmail, orgId: scope.orgId },
154
+ FIRST_PARTY_ANALYTICS_BACKEND_SETTING,
155
+ {
156
+ sink: config.sink,
157
+ ...(config.table ? { table: config.table } : {}),
158
+ ...(config.backfillCursor !== undefined
159
+ ? { backfillCursor: config.backfillCursor }
160
+ : {}),
161
+ ...(config.backfillCompleted !== undefined
162
+ ? { backfillCompleted: config.backfillCompleted }
163
+ : {}),
164
+ updatedAt: new Date().toISOString(),
165
+ },
166
+ );
167
+ backendConfigCache.delete(backendScopeKey(scope));
168
+ }
169
+
170
+ export function resetFirstPartyAnalyticsBackendCacheForTests(): void {
171
+ backendConfigCache.clear();
172
+ }
173
+
174
+ export async function getFirstPartyAnalyticsTable(
175
+ configuredTable?: string | null,
176
+ ): Promise<BigQueryTableRef> {
177
+ const projectId = await getBigQueryProjectId();
178
+ return parseTableRef(configuredTable, projectId);
179
+ }
180
+
181
+ function tableName(table: BigQueryTableRef, suffix: string): string {
182
+ return `${table.projectId}.${table.datasetId}.${table.tableId}${suffix}`;
183
+ }
184
+
185
+ function firstPartyAnalyticsRawTable(table: BigQueryTableRef): string {
186
+ return table.fullyQualified;
187
+ }
188
+
189
+ export function firstPartyAnalyticsPhysicalTables(table: BigQueryTableRef): {
190
+ events: string;
191
+ dailyRollups: string;
192
+ userDays: string;
193
+ } {
194
+ return {
195
+ events: tableName(table, "_query"),
196
+ dailyRollups: tableName(table, "_daily_rollups"),
197
+ userDays: tableName(table, "_user_days"),
198
+ };
199
+ }
200
+
201
+ const FIRST_PARTY_ANALYTICS_RAW_SCHEMA = [
202
+ ["id", "STRING"],
203
+ ["public_key_id", "STRING"],
204
+ ["event_name", "STRING"],
205
+ ["user_id", "STRING"],
206
+ ["anonymous_id", "STRING"],
207
+ ["user_key", "STRING"],
208
+ ["session_id", "STRING"],
209
+ ["timestamp", "TIMESTAMP"],
210
+ ["event_date", "DATE"],
211
+ ["received_at", "TIMESTAMP"],
212
+ ["url", "STRING"],
213
+ ["path", "STRING"],
214
+ ["hostname", "STRING"],
215
+ ["referrer", "STRING"],
216
+ ["app", "STRING"],
217
+ ["template", "STRING"],
218
+ ["signed_in", "STRING"],
219
+ ["properties", "STRING"],
220
+ ["context", "STRING"],
221
+ ["owner_email", "STRING"],
222
+ ["org_id", "STRING"],
223
+ ] as const;
224
+
225
+ const FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS = [
226
+ "id",
227
+ "public_key_id",
228
+ "event_name",
229
+ "user_id",
230
+ "anonymous_id",
231
+ "user_key",
232
+ "session_id",
233
+ "timestamp",
234
+ "event_date",
235
+ "received_at",
236
+ "url",
237
+ "path",
238
+ "hostname",
239
+ "referrer",
240
+ "app",
241
+ "template",
242
+ "signed_in",
243
+ "properties",
244
+ "context",
245
+ "owner_email",
246
+ "org_id",
247
+ ] as const;
248
+
249
+ const FIRST_PARTY_ANALYTICS_QUERY_SCHEMA = FIRST_PARTY_ANALYTICS_RAW_SCHEMA;
250
+ const FIRST_PARTY_ANALYTICS_DAILY_ROLLUP_SCHEMA = [
251
+ ["id", "STRING"],
252
+ ["tenant_key", "STRING"],
253
+ ["owner_email", "STRING"],
254
+ ["org_id", "STRING"],
255
+ ["event_date", "DATE"],
256
+ ["event_name", "STRING"],
257
+ ["app", "STRING"],
258
+ ["template", "STRING"],
259
+ ["event_count", "INT64"],
260
+ ] as const;
261
+ const FIRST_PARTY_ANALYTICS_USER_DAYS_SCHEMA = [
262
+ ["id", "STRING"],
263
+ ["tenant_key", "STRING"],
264
+ ["owner_email", "STRING"],
265
+ ["org_id", "STRING"],
266
+ ["event_date", "DATE"],
267
+ ["user_key", "STRING"],
268
+ ] as const;
269
+
270
+ function sqlList(values: readonly string[]): string {
271
+ return values.map((value) => sqlLiteral(value)).join(", ");
272
+ }
273
+
274
+ function firstPartyEventRowToBigQuery(
275
+ row: FirstPartyAnalyticsEventRow | Record<string, unknown>,
276
+ ): Record<string, unknown> {
277
+ const record = row as Record<string, unknown>;
278
+ const value = (camel: string, snake: string): unknown =>
279
+ record[camel] ?? record[snake];
280
+ return {
281
+ id: value("id", "id"),
282
+ public_key_id: value("publicKeyId", "public_key_id"),
283
+ event_name: value("eventName", "event_name"),
284
+ user_id: value("userId", "user_id"),
285
+ anonymous_id: value("anonymousId", "anonymous_id"),
286
+ user_key: value("userKey", "user_key"),
287
+ session_id: value("sessionId", "session_id"),
288
+ timestamp: value("timestamp", "timestamp"),
289
+ event_date: value("eventDate", "event_date"),
290
+ received_at: value("receivedAt", "received_at"),
291
+ url: value("url", "url"),
292
+ path: value("path", "path"),
293
+ hostname: value("hostname", "hostname"),
294
+ referrer: value("referrer", "referrer"),
295
+ app: value("app", "app"),
296
+ template: value("template", "template"),
297
+ signed_in: value("signedIn", "signed_in"),
298
+ properties: value("properties", "properties") ?? "{}",
299
+ context: value("context", "context") ?? "{}",
300
+ owner_email: value("ownerEmail", "owner_email"),
301
+ org_id: value("orgId", "org_id"),
302
+ };
303
+ }
304
+
305
+ async function insertBatch(
306
+ table: BigQueryTableRef,
307
+ token: string,
308
+ rows: Record<string, unknown>[],
309
+ ): Promise<void> {
310
+ const response = await fetch(
311
+ `https://bigquery.googleapis.com/bigquery/v2/projects/${table.projectId}/datasets/${table.datasetId}/tables/${table.tableId}/insertAll`,
312
+ {
313
+ method: "POST",
314
+ headers: {
315
+ Authorization: `Bearer ${token}`,
316
+ "Content-Type": "application/json",
317
+ },
318
+ body: JSON.stringify({
319
+ skipInvalidRows: false,
320
+ ignoreUnknownValues: false,
321
+ rows: rows.map((row) => ({
322
+ insertId: typeof row.id === "string" ? row.id : undefined,
323
+ json: row,
324
+ })),
325
+ }),
326
+ },
327
+ );
328
+ if (!response.ok) {
329
+ const text = await response.text();
330
+ throw new Error(
331
+ `BigQuery event insert failed (${response.status}): ${text}`,
332
+ );
333
+ }
334
+ const result = (await response.json()) as {
335
+ insertErrors?: Array<{
336
+ index?: number;
337
+ errors?: Array<{ message?: string }>;
338
+ }>;
339
+ };
340
+ if (result.insertErrors?.length) {
341
+ const detail = result.insertErrors
342
+ .flatMap((entry) => entry.errors ?? [])
343
+ .map((entry) => entry.message)
344
+ .filter((message): message is string => Boolean(message))
345
+ .slice(0, 3)
346
+ .join("; ");
347
+ throw new Error(
348
+ `BigQuery rejected ${result.insertErrors.length} event row(s)${detail ? `: ${detail}` : ""}`,
349
+ );
350
+ }
351
+ }
352
+
353
+ export async function insertFirstPartyAnalyticsRows(
354
+ rows: Array<FirstPartyAnalyticsEventRow | Record<string, unknown>>,
355
+ configuredTable?: string | null,
356
+ ): Promise<number> {
357
+ if (!rows.length) return 0;
358
+ requireRequestCredentialContext("GOOGLE_APPLICATION_CREDENTIALS_JSON");
359
+ const [table, token] = await Promise.all([
360
+ getFirstPartyAnalyticsTable(configuredTable),
361
+ getAccessToken(),
362
+ ]);
363
+ const payloadRows = rows.map(firstPartyEventRowToBigQuery);
364
+ for (let offset = 0; offset < payloadRows.length; offset += 50) {
365
+ await insertBatch(table, token, payloadRows.slice(offset, offset + 50));
366
+ }
367
+ return payloadRows.length;
368
+ }
369
+
370
+ function sqlLiteral(value: string | null): string {
371
+ if (value === null) return "NULL";
372
+ return `'${value.replace(/'/g, "''")}'`;
373
+ }
374
+
375
+ function bindSqlArguments(sql: string, args: Array<string | null>): string {
376
+ let index = 0;
377
+ return sql.replace(/\?/g, () => {
378
+ const value = args[index++];
379
+ if (value === undefined) {
380
+ throw new Error("First-party BigQuery query has too few bind arguments");
381
+ }
382
+ return sqlLiteral(value);
383
+ });
384
+ }
385
+
386
+ function maskSqlLiterals(sql: string): string {
387
+ const chars = [...sql];
388
+ let inLiteral = false;
389
+ for (let index = 0; index < chars.length; index++) {
390
+ if (chars[index] !== "'") continue;
391
+ if (!inLiteral) {
392
+ inLiteral = true;
393
+ chars[index] = " ";
394
+ continue;
395
+ }
396
+ if (chars[index + 1] === "'") {
397
+ chars[index] = " ";
398
+ chars[index + 1] = " ";
399
+ index++;
400
+ continue;
401
+ }
402
+ chars[index] = " ";
403
+ inLiteral = false;
404
+ }
405
+ for (let index = 0; index < chars.length; index++) {
406
+ if (inLiteral) chars[index] = " ";
407
+ }
408
+ return chars.join("");
409
+ }
410
+
411
+ function rewriteOutsideSqlLiterals(
412
+ sql: string,
413
+ rewrite: (code: string) => string,
414
+ ): string {
415
+ let result = "";
416
+ let cursor = 0;
417
+ while (cursor < sql.length) {
418
+ const literalStart = sql.indexOf("'", cursor);
419
+ if (literalStart === -1) {
420
+ result += rewrite(sql.slice(cursor));
421
+ break;
422
+ }
423
+ result += rewrite(sql.slice(cursor, literalStart));
424
+ let literalEnd = literalStart + 1;
425
+ while (literalEnd < sql.length) {
426
+ if (sql[literalEnd] !== "'") {
427
+ literalEnd++;
428
+ continue;
429
+ }
430
+ if (sql[literalEnd + 1] === "'") {
431
+ literalEnd += 2;
432
+ continue;
433
+ }
434
+ literalEnd++;
435
+ break;
436
+ }
437
+ result += sql.slice(literalStart, literalEnd);
438
+ cursor = literalEnd;
439
+ }
440
+ return result;
441
+ }
442
+
443
+ function findMatchingSqlParen(sql: string, openIndex: number): number {
444
+ let depth = 0;
445
+ let inLiteral = false;
446
+ for (let index = openIndex; index < sql.length; index++) {
447
+ const char = sql[index];
448
+ if (char === "'") {
449
+ if (inLiteral && sql[index + 1] === "'") {
450
+ index++;
451
+ continue;
452
+ }
453
+ inLiteral = !inLiteral;
454
+ continue;
455
+ }
456
+ if (inLiteral) continue;
457
+ if (char === "(") depth++;
458
+ if (char === ")") {
459
+ depth--;
460
+ if (depth === 0) return index;
461
+ }
462
+ }
463
+ return -1;
464
+ }
465
+
466
+ function splitTopLevelSqlArgs(value: string): string[] {
467
+ const args: string[] = [];
468
+ let start = 0;
469
+ let depth = 0;
470
+ let inLiteral = false;
471
+ for (let index = 0; index < value.length; index++) {
472
+ const char = value[index];
473
+ if (char === "'") {
474
+ if (inLiteral && value[index + 1] === "'") {
475
+ index++;
476
+ continue;
477
+ }
478
+ inLiteral = !inLiteral;
479
+ continue;
480
+ }
481
+ if (inLiteral) continue;
482
+ if (char === "(") depth++;
483
+ if (char === ")") depth--;
484
+ if (char === "," && depth === 0) {
485
+ args.push(value.slice(start, index).trim());
486
+ start = index + 1;
487
+ }
488
+ }
489
+ args.push(value.slice(start).trim());
490
+ return args;
491
+ }
492
+
493
+ function rewriteSqlFunctionCalls(
494
+ sql: string,
495
+ functionName: string,
496
+ rewrite: (args: string[]) => string,
497
+ ): string {
498
+ const functionRe = new RegExp(`\\b${functionName}\\s*\\(`, "gi");
499
+ const masked = maskSqlLiterals(sql);
500
+ let cursor = 0;
501
+ let result = "";
502
+ let match = functionRe.exec(masked);
503
+ while (match) {
504
+ const openIndex = match.index + match[0].lastIndexOf("(");
505
+ const closeIndex = findMatchingSqlParen(sql, openIndex);
506
+ if (closeIndex === -1) {
507
+ throw new Error(
508
+ `First-party BigQuery query has an unterminated ${functionName} call`,
509
+ );
510
+ }
511
+ result += sql.slice(cursor, match.index);
512
+ result += rewrite(
513
+ splitTopLevelSqlArgs(sql.slice(openIndex + 1, closeIndex)),
514
+ );
515
+ cursor = closeIndex + 1;
516
+ functionRe.lastIndex = cursor;
517
+ match = functionRe.exec(masked);
518
+ }
519
+ return match ? result : result + sql.slice(cursor);
520
+ }
521
+
522
+ function replacePostgresCastsInCode(code: string): string {
523
+ const castType = new RegExp(
524
+ "::\\s*(date|timestamp|timestamptz|int|int2|int4|int8|integer|float|float4|float8|double\\s+precision|numeric|text|varchar|boolean|bool|json|jsonb)\\b",
525
+ "gi",
526
+ );
527
+ let result = code;
528
+ let match = castType.exec(result);
529
+ while (match) {
530
+ const normalizedType = match[1].toLowerCase().replace(/\s+/g, " ");
531
+ const mappedType: Record<string, string> = {
532
+ bool: "BOOL",
533
+ boolean: "BOOL",
534
+ date: "DATE",
535
+ double: "FLOAT64",
536
+ "double precision": "FLOAT64",
537
+ float: "FLOAT64",
538
+ float4: "FLOAT64",
539
+ float8: "FLOAT64",
540
+ int: "INT64",
541
+ int2: "INT64",
542
+ int4: "INT64",
543
+ int8: "INT64",
544
+ integer: "INT64",
545
+ numeric: "NUMERIC",
546
+ text: "STRING",
547
+ timestamp: "TIMESTAMP",
548
+ timestamptz: "TIMESTAMP",
549
+ varchar: "STRING",
550
+ };
551
+ const targetType = mappedType[normalizedType];
552
+ if (!targetType) {
553
+ throw new Error(
554
+ `First-party BigQuery query does not support PostgreSQL ${normalizedType} casts`,
555
+ );
556
+ }
557
+
558
+ let operandEnd = match.index;
559
+ while (operandEnd > 0 && /\s/.test(result[operandEnd - 1] ?? "")) {
560
+ operandEnd--;
561
+ }
562
+ let operandStart = operandEnd;
563
+ if (result[operandEnd - 1] === ")") {
564
+ let depth = 0;
565
+ for (let index = operandEnd - 1; index >= 0; index--) {
566
+ if (result[index] === ")") depth++;
567
+ if (result[index] !== "(") continue;
568
+ depth--;
569
+ if (depth === 0) {
570
+ operandStart = index;
571
+ break;
572
+ }
573
+ }
574
+ } else {
575
+ while (
576
+ operandStart > 0 &&
577
+ /[A-Za-z0-9_.$]/.test(result[operandStart - 1] ?? "")
578
+ ) {
579
+ operandStart--;
580
+ }
581
+ }
582
+ if (operandStart === operandEnd) {
583
+ throw new Error(
584
+ "First-party BigQuery query has an invalid PostgreSQL cast",
585
+ );
586
+ }
587
+ const operand = result.slice(operandStart, operandEnd).trim();
588
+ const castEnd = match.index + match[0].length;
589
+ result = `${result.slice(0, operandStart)}CAST(${operand} AS ${targetType})${result.slice(castEnd)}`;
590
+ castType.lastIndex =
591
+ operandStart + `CAST(${operand} AS ${targetType})`.length;
592
+ match = castType.exec(result);
593
+ }
594
+ return result;
595
+ }
596
+
597
+ function translatePostgresDateExpression(value: string): string {
598
+ let translated = replacePostgresCastsInCode(value.trim());
599
+ translated = translated
600
+ .replace(/\bnow\s*\(\s*\)/gi, "CURRENT_TIMESTAMP()")
601
+ .replace(/\bCURRENT_DATE\b(?!\s*\()/gi, "CURRENT_DATE()")
602
+ .replace(/\bCURRENT_TIMESTAMP\b(?!\s*\()/gi, "CURRENT_TIMESTAMP()");
603
+ translated = replaceBigQueryDateArithmetic(translated);
604
+ return translated.trim();
605
+ }
606
+
607
+ function replaceBigQueryDateArithmetic(code: string): string {
608
+ const intervalExpression =
609
+ /((?:[A-Za-z_][A-Za-z0-9_.]*\(\)|[A-Za-z_][A-Za-z0-9_.]*|CAST\([^()]+\s+AS\s+[A-Z0-9]+\)))\s*([+-])\s*INTERVAL\s+(\d+)\s+(DAY|WEEK|MONTH)\b/gi;
610
+ let translated = code.replace(
611
+ intervalExpression,
612
+ (
613
+ _match,
614
+ operand: string,
615
+ operator: string,
616
+ amount: string,
617
+ unit: string,
618
+ ) => {
619
+ const functionName = /CURRENT_TIMESTAMP|TIMESTAMP/i.test(operand)
620
+ ? "TIMESTAMP"
621
+ : "DATE";
622
+ return `${functionName}_${operator === "+" ? "ADD" : "SUB"}(${operand}, INTERVAL ${amount} ${unit.toUpperCase()})`;
623
+ },
624
+ );
625
+ const dateIntegerExpression =
626
+ /((?:[A-Za-z_][A-Za-z0-9_]*\.)*(?:date|event_date|start_date|end_date|cohort_date)|CAST\([^()]+\s+AS\s+DATE\))\s*\+\s*([A-Za-z_][A-Za-z0-9_.]*)/gi;
627
+ translated = translated.replace(
628
+ dateIntegerExpression,
629
+ (_match, dateExpression: string, offsetExpression: string) =>
630
+ `DATE_ADD(${dateExpression}, INTERVAL ${offsetExpression} DAY)`,
631
+ );
632
+ return translated;
633
+ }
634
+
635
+ function translatePostgresJsonOperators(sql: string): string {
636
+ return sql.replace(
637
+ /\b([A-Za-z_][A-Za-z0-9_.]*)\s*::\s*jsonb\s*->>\s*'([^']+)'/gi,
638
+ (_match, expression: string, key: string) =>
639
+ `JSON_VALUE(${expression}, '$.${key}')`,
640
+ );
641
+ }
642
+
643
+ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
644
+ let translated = translatePostgresJsonOperators(sql);
645
+ translated = translated.replace(
646
+ /\bINTERVAL\s*'(\d+)\s+(day|days|week|weeks|month|months)'/gi,
647
+ (_match, amount: string, unit: string) =>
648
+ `INTERVAL ${amount} ${unit.replace(/s$/i, "").toUpperCase()}`,
649
+ );
650
+ translated = rewriteSqlFunctionCalls(translated, "date_trunc", (args) => {
651
+ if (args.length !== 2 || !/^'week'$/i.test(args[0] ?? "")) {
652
+ throw new Error(
653
+ "First-party BigQuery query only supports PostgreSQL date_trunc('week', ...) expressions",
654
+ );
655
+ }
656
+ return `DATE_TRUNC(CAST(${translatePostgresDateExpression(args[1] ?? "")} AS DATE), WEEK(MONDAY))`;
657
+ });
658
+ translated = rewriteSqlFunctionCalls(translated, "to_char", (args) => {
659
+ if (args.length !== 2 || !/^'YYYY-MM-DD'$/i.test(args[1] ?? "")) {
660
+ throw new Error(
661
+ "First-party BigQuery query only supports PostgreSQL to_char(..., 'YYYY-MM-DD') expressions",
662
+ );
663
+ }
664
+ return `FORMAT_DATE('%Y-%m-%d', CAST(${translatePostgresDateExpression(args[0] ?? "")} AS DATE))`;
665
+ });
666
+ translated = rewriteSqlFunctionCalls(translated, "chr", (args) => {
667
+ if (args.length !== 1) {
668
+ throw new Error("First-party BigQuery query has an invalid chr call");
669
+ }
670
+ return `CHR(${args[0]})`;
671
+ });
672
+ let previous = "";
673
+ while (previous !== translated) {
674
+ previous = translated;
675
+ translated = rewriteSqlFunctionCalls(translated, "split_part", (args) => {
676
+ const index = Number(args[2]);
677
+ if (args.length !== 3 || !Number.isInteger(index) || index < 1) {
678
+ throw new Error(
679
+ "First-party BigQuery query only supports split_part with a positive integer index",
680
+ );
681
+ }
682
+ return `SPLIT(${args[0]}, ${args[1]})[SAFE_OFFSET(${index - 1})]`;
683
+ });
684
+ }
685
+ translated = rewriteOutsideSqlLiterals(translated, (code) => {
686
+ let rewritten = replacePostgresCastsInCode(code);
687
+ rewritten = rewritten
688
+ .replace(/\bnow\s*\(\s*\)/gi, "CURRENT_TIMESTAMP()")
689
+ .replace(/\bCURRENT_DATE\b(?!\s*\()/gi, "CURRENT_DATE()")
690
+ .replace(/\bCURRENT_TIMESTAMP\b(?!\s*\()/gi, "CURRENT_TIMESTAMP()");
691
+ return replaceBigQueryDateArithmetic(rewritten);
692
+ });
693
+
694
+ const code = maskSqlLiterals(translated);
695
+ const unsupported: Array<[RegExp, string]> = [
696
+ [/\bto_char\s*\(/i, "to_char"],
697
+ [/\bdate_trunc\s*\(\s*'/i, "date_trunc"],
698
+ [/\b(?:ILIKE|SIMILAR\s+TO)\b/i, "ILIKE/SIMILAR TO"],
699
+ [
700
+ /\b(?:LATERAL|generate_series|unnest|string_to_array)\b/i,
701
+ "set-returning PostgreSQL functions",
702
+ ],
703
+ [/::\s*[A-Za-z_]/i, "PostgreSQL casts"],
704
+ [/\bINTERVAL\s*'/i, "PostgreSQL interval literals"],
705
+ [/\bAT\s+TIME\s+ZONE\b/i, "AT TIME ZONE"],
706
+ [/\bFILTER\s*\(\s*WHERE\b/i, "FILTER (WHERE ...)"],
707
+ ];
708
+ const incompatible = unsupported.find(([pattern]) => pattern.test(code));
709
+ if (incompatible) {
710
+ throw new Error(
711
+ `First-party analytics query uses unsupported PostgreSQL syntax (${incompatible[1]}) after BigQuery translation`,
712
+ );
713
+ }
714
+ return translated;
715
+ }
716
+
717
+ function qualifyQuerySources(sql: string, table: BigQueryTableRef): string {
718
+ const physical = firstPartyAnalyticsPhysicalTables(table);
719
+ const sourceMap: Record<string, string> = {
720
+ // Event predicates are injected by scopedAnalyticsSql. Use the raw table
721
+ // here so those predicates run before the retry-deduplication window.
722
+ analytics_events: firstPartyAnalyticsRawTable(table),
723
+ analytics_event_daily_rollups: physical.dailyRollups,
724
+ analytics_user_days: physical.userDays,
725
+ };
726
+ const sourcePattern = FIRST_PARTY_QUERY_TABLES.join("|");
727
+ return sql.replace(
728
+ new RegExp(`\\b(from|join)\\s+(${sourcePattern})\\b`, "gi"),
729
+ (_match, keyword: string, logicalName: string) =>
730
+ `${keyword} \`${sourceMap[logicalName.toLowerCase()] ?? logicalName}\``,
731
+ );
732
+ }
733
+
734
+ function addPartitionPrunedEventDeduplication(
735
+ sql: string,
736
+ table: BigQueryTableRef,
737
+ ): string {
738
+ const quote = String.fromCharCode(96);
739
+ const source =
740
+ "SELECT * FROM " +
741
+ quote +
742
+ firstPartyAnalyticsRawTable(table) +
743
+ quote +
744
+ " WHERE";
745
+ let result = "";
746
+ let cursor = 0;
747
+ while (cursor < sql.length) {
748
+ const sourceIndex = sql.indexOf(source, cursor);
749
+ if (sourceIndex === -1) return result + sql.slice(cursor);
750
+ const predicateStart = sourceIndex + source.length;
751
+ let depth = 0;
752
+ let inLiteral = false;
753
+ let predicateEnd = sql.length;
754
+ for (let index = predicateStart; index < sql.length; index++) {
755
+ const char = sql[index];
756
+ if (char === "'") {
757
+ if (inLiteral && sql[index + 1] === "'") {
758
+ index++;
759
+ continue;
760
+ }
761
+ inLiteral = !inLiteral;
762
+ continue;
763
+ }
764
+ if (inLiteral) continue;
765
+ if (char === "(") {
766
+ depth++;
767
+ continue;
768
+ }
769
+ if (char === ")") {
770
+ if (depth === 0) {
771
+ predicateEnd = index;
772
+ break;
773
+ }
774
+ depth--;
775
+ continue;
776
+ }
777
+ if (depth === 0 && /^UNION\s+ALL\b/i.test(sql.slice(index))) {
778
+ predicateEnd = index;
779
+ break;
780
+ }
781
+ }
782
+ result +=
783
+ sql.slice(cursor, predicateEnd) +
784
+ " QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY received_at DESC) = 1";
785
+ cursor = predicateEnd;
786
+ }
787
+ return result;
788
+ }
789
+
790
+ export function renderFirstPartyAnalyticsBigQuerySql(
791
+ scopedSql: string,
792
+ args: Array<string | null>,
793
+ table: BigQueryTableRef,
794
+ ): string {
795
+ // The Postgres/SQLite scope builder uses a text fallback for nullable event
796
+ // dates. BigQuery's event_date is a DATE, and the fallback is unnecessary
797
+ // because the sink normalizes it before insert.
798
+ const normalizedScopeSql = scopedSql.replace(
799
+ /\(COALESCE\(NULLIF\(event_date, ''\), substr\(timestamp, 1, 10\)\) <= \?\)/g,
800
+ "(event_date <= ?)",
801
+ );
802
+ const translated =
803
+ translateFirstPartyAnalyticsBigQuerySql(normalizedScopeSql);
804
+ const bound = bindSqlArguments(translated, args);
805
+ return addPartitionPrunedEventDeduplication(
806
+ qualifyQuerySources(bound, table),
807
+ table,
808
+ );
809
+ }
810
+
811
+ export async function queryFirstPartyAnalyticsInBigQuery(
812
+ scopedSql: string,
813
+ args: Array<string | null>,
814
+ table: BigQueryTableRef,
815
+ ): Promise<{
816
+ rows: Record<string, unknown>[];
817
+ schema: { name: string; type: string }[];
818
+ }> {
819
+ const result = await runQuery(
820
+ `SELECT * FROM (${renderFirstPartyAnalyticsBigQuerySql(scopedSql, args, table)}) AS first_party_analytics_query LIMIT 5000`,
821
+ );
822
+ return { rows: result.rows, schema: result.schema };
823
+ }
824
+
825
+ export async function assertFirstPartyAnalyticsBigQueryReady(
826
+ configuredTable?: string | null,
827
+ ): Promise<{ table: BigQueryTableRef; rowCount: number }> {
828
+ const table = await getFirstPartyAnalyticsTable(configuredTable);
829
+ const expectedColumns = new Map<string, readonly string[]>([
830
+ [table.tableId, FIRST_PARTY_ANALYTICS_RAW_SCHEMA.map(([name]) => name)],
831
+ [
832
+ table.tableId + "_query",
833
+ FIRST_PARTY_ANALYTICS_QUERY_SCHEMA.map(([name]) => name),
834
+ ],
835
+ [
836
+ table.tableId + "_daily_rollups",
837
+ FIRST_PARTY_ANALYTICS_DAILY_ROLLUP_SCHEMA.map(([name]) => name),
838
+ ],
839
+ [
840
+ table.tableId + "_user_days",
841
+ FIRST_PARTY_ANALYTICS_USER_DAYS_SCHEMA.map(([name]) => name),
842
+ ],
843
+ ]);
844
+ const tableNames = [...expectedColumns.keys()];
845
+ const quote = String.fromCharCode(96);
846
+ const informationSchema =
847
+ quote +
848
+ table.projectId +
849
+ "." +
850
+ table.datasetId +
851
+ ".INFORMATION_SCHEMA" +
852
+ quote;
853
+ const tableMetadata = await runQuery(
854
+ "SELECT table_name FROM " +
855
+ informationSchema +
856
+ ".TABLES WHERE table_name IN (" +
857
+ sqlList(tableNames) +
858
+ ")",
859
+ );
860
+ const presentTables = new Set(
861
+ tableMetadata.rows
862
+ .map((row) => row.table_name)
863
+ .filter((value): value is string => typeof value === "string"),
864
+ );
865
+ const missingTables = tableNames.filter((name) => !presentTables.has(name));
866
+ if (missingTables.length) {
867
+ throw new Error(
868
+ "First-party Analytics BigQuery is missing required tables or views: " +
869
+ missingTables.join(", "),
870
+ );
871
+ }
872
+ const columnMetadata = await runQuery(
873
+ "SELECT table_name, column_name FROM " +
874
+ informationSchema +
875
+ ".COLUMNS WHERE table_name IN (" +
876
+ sqlList(tableNames) +
877
+ ")",
878
+ );
879
+ const presentColumns = new Map<string, Set<string>>();
880
+ for (const row of columnMetadata.rows) {
881
+ if (
882
+ typeof row.table_name !== "string" ||
883
+ typeof row.column_name !== "string"
884
+ ) {
885
+ continue;
886
+ }
887
+ const columns = presentColumns.get(row.table_name) ?? new Set<string>();
888
+ columns.add(row.column_name);
889
+ presentColumns.set(row.table_name, columns);
890
+ }
891
+ const missingColumns: string[] = [];
892
+ for (const [tableName, columns] of expectedColumns) {
893
+ for (const column of columns) {
894
+ if (!presentColumns.get(tableName)?.has(column)) {
895
+ missingColumns.push(tableName + "." + column);
896
+ }
897
+ }
898
+ }
899
+ if (missingColumns.length) {
900
+ throw new Error(
901
+ "First-party Analytics BigQuery is missing required columns: " +
902
+ missingColumns.join(", "),
903
+ );
904
+ }
905
+ const result = await runQuery(
906
+ "SELECT COUNT(*) AS row_count FROM " + quote + table.fullyQualified + quote,
907
+ );
908
+ const rowCount = Number(result.rows[0]?.row_count ?? 0);
909
+ return { table, rowCount: Number.isFinite(rowCount) ? rowCount : 0 };
910
+ }
911
+
912
+ export async function getFirstPartyAnalyticsBigQueryMetrics(
913
+ scope: FirstPartyAnalyticsScope,
914
+ configuredTable?: string | null,
915
+ ): Promise<{
916
+ eventCount: number;
917
+ dailyRollupRows: number;
918
+ firstEventDate: string | null;
919
+ lastEventDate: string | null;
920
+ }> {
921
+ const table = await getFirstPartyAnalyticsTable(configuredTable);
922
+ const physical = firstPartyAnalyticsPhysicalTables(table);
923
+ const ownerEmail = sqlLiteral(scope.userEmail);
924
+ const today = sqlLiteral(new Date().toISOString().slice(0, 10));
925
+ const tenantFilter = scope.orgId
926
+ ? `(org_id = ${sqlLiteral(scope.orgId)} OR (org_id IS NULL AND owner_email = ${ownerEmail}))`
927
+ : `(org_id IS NULL AND owner_email = ${ownerEmail})`;
928
+ const result = await runQuery(`
929
+ WITH scoped_events AS (
930
+ SELECT *
931
+ FROM \`${physical.events}\`
932
+ WHERE ${tenantFilter} AND event_date <= ${today}
933
+ )
934
+ SELECT
935
+ COUNT(*) AS event_count,
936
+ COUNT(DISTINCT CONCAT(
937
+ CAST(event_date AS STRING), '|', event_name, '|', COALESCE(app, ''),
938
+ '|', COALESCE(template, '')
939
+ )) AS daily_rollup_rows,
940
+ MIN(event_date) AS first_event_date,
941
+ MAX(event_date) AS last_event_date
942
+ FROM scoped_events
943
+ `);
944
+ const row = result.rows[0] ?? {};
945
+ const readValue = (...names: string[]): unknown => {
946
+ for (const name of names) {
947
+ if (Object.prototype.hasOwnProperty.call(row, name)) return row[name];
948
+ }
949
+ return undefined;
950
+ };
951
+ const numberValue = (value: unknown): number => {
952
+ const parsed = Number(value);
953
+ return Number.isFinite(parsed) ? parsed : 0;
954
+ };
955
+ const dateValue = (value: unknown): string | null => {
956
+ if (typeof value === "string" && value) return value.slice(0, 10);
957
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
958
+ return value.toISOString().slice(0, 10);
959
+ }
960
+ if (value && typeof value === "object" && "value" in value) {
961
+ return dateValue((value as { value?: unknown }).value);
962
+ }
963
+ return null;
964
+ };
965
+ return {
966
+ eventCount: numberValue(readValue("event_count", "eventCount")),
967
+ dailyRollupRows: numberValue(
968
+ readValue("daily_rollup_rows", "dailyRollupRows"),
969
+ ),
970
+ firstEventDate: dateValue(readValue("first_event_date", "firstEventDate")),
971
+ lastEventDate: dateValue(readValue("last_event_date", "lastEventDate")),
972
+ };
973
+ }
974
+
975
+ export interface FirstPartyAnalyticsBackfillBatch {
976
+ nextCursor: string | null;
977
+ copied: number;
978
+ complete: boolean;
979
+ }
980
+
981
+ function parseBackfillCursor(
982
+ cursor: string | null,
983
+ ): FirstPartyAnalyticsBackfillCursor {
984
+ if (!cursor) return { receivedAt: "", id: "" };
985
+ try {
986
+ const parsed = JSON.parse(
987
+ cursor,
988
+ ) as Partial<FirstPartyAnalyticsBackfillCursor>;
989
+ if (
990
+ typeof parsed.receivedAt === "string" &&
991
+ typeof parsed.id === "string"
992
+ ) {
993
+ return { receivedAt: parsed.receivedAt, id: parsed.id };
994
+ }
995
+ // coercion-ok: legacy cursors restart from the beginning to prevent skipped dual-write events.
996
+ } catch {
997
+ // Legacy cursors were only event ids. Restart from the beginning so a
998
+ // dual-write failure cannot leave an event below the new tuple cursor.
999
+ }
1000
+ return { receivedAt: "", id: "" };
1001
+ }
1002
+
1003
+ function serializeBackfillCursor(
1004
+ cursor: FirstPartyAnalyticsBackfillCursor,
1005
+ ): string {
1006
+ return JSON.stringify(cursor);
1007
+ }
1008
+
1009
+ function backfillBranchSql(
1010
+ predicate: string,
1011
+ predicateArgs: string[],
1012
+ cursor: FirstPartyAnalyticsBackfillCursor,
1013
+ ): {
1014
+ sql: string;
1015
+ args: string[];
1016
+ } {
1017
+ const cursorSql = cursor.receivedAt
1018
+ ? "(received_at > ? OR (received_at = ? AND id > ?))"
1019
+ : "";
1020
+ const cursorArgs = cursor.receivedAt
1021
+ ? [cursor.receivedAt, cursor.receivedAt, cursor.id]
1022
+ : [];
1023
+ return {
1024
+ sql: `SELECT id, received_at
1025
+ FROM analytics_events
1026
+ WHERE ${predicate}${cursorSql ? ` AND ${cursorSql}` : ""}
1027
+ ORDER BY received_at ASC, id ASC LIMIT ?`,
1028
+ args: [...predicateArgs, ...cursorArgs],
1029
+ };
1030
+ }
1031
+
1032
+ function backfillRowsByIdsSql(ids: string[]): {
1033
+ sql: string;
1034
+ args: string[];
1035
+ } {
1036
+ return {
1037
+ sql: `SELECT ${FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ")}
1038
+ FROM analytics_events
1039
+ WHERE id IN (${ids.map(() => "?").join(", ")})`,
1040
+ args: ids,
1041
+ };
1042
+ }
1043
+
1044
+ function backfillRowCursor(
1045
+ row: Record<string, unknown>,
1046
+ ): FirstPartyAnalyticsBackfillCursor {
1047
+ const id = row.id;
1048
+ const receivedAt = row.received_at ?? row.receivedAt;
1049
+ if (typeof id !== "string" || !id) {
1050
+ throw new Error("First-party analytics backfill row is missing its id");
1051
+ }
1052
+ if (typeof receivedAt !== "string" || !receivedAt) {
1053
+ throw new Error(
1054
+ "First-party analytics backfill row is missing its received_at",
1055
+ );
1056
+ }
1057
+ return { receivedAt, id };
1058
+ }
1059
+
1060
+ function compareBackfillRows(
1061
+ left: Record<string, unknown>,
1062
+ right: Record<string, unknown>,
1063
+ ): number {
1064
+ const leftCursor = backfillRowCursor(left);
1065
+ const rightCursor = backfillRowCursor(right);
1066
+ return (
1067
+ leftCursor.receivedAt.localeCompare(rightCursor.receivedAt) ||
1068
+ leftCursor.id.localeCompare(rightCursor.id)
1069
+ );
1070
+ }
1071
+
1072
+ export async function backfillFirstPartyAnalyticsBatch(
1073
+ scope: FirstPartyAnalyticsScope,
1074
+ cursor: string | null,
1075
+ limit: number,
1076
+ configuredTable?: string | null,
1077
+ ): Promise<FirstPartyAnalyticsBackfillBatch> {
1078
+ const boundedLimit = Math.min(Math.max(Math.floor(limit), 1), 500);
1079
+ const db = getDbExec();
1080
+ const parsedCursor = parseBackfillCursor(cursor);
1081
+ const branches = scope.orgId
1082
+ ? [
1083
+ { predicate: "org_id = ?", args: [scope.orgId] },
1084
+ {
1085
+ predicate: "org_id IS NULL AND owner_email = ?",
1086
+ args: [scope.userEmail],
1087
+ },
1088
+ ]
1089
+ : [
1090
+ {
1091
+ predicate: "org_id IS NULL AND owner_email = ?",
1092
+ args: [scope.userEmail],
1093
+ },
1094
+ ];
1095
+ const rows: Record<string, unknown>[] = [];
1096
+ for (const branch of branches) {
1097
+ const scoped = backfillBranchSql(
1098
+ branch.predicate,
1099
+ branch.args,
1100
+ parsedCursor,
1101
+ );
1102
+ const result = await db.execute({
1103
+ sql: scoped.sql,
1104
+ args: [...scoped.args, boundedLimit],
1105
+ timeoutMs: 20_000,
1106
+ maxAttempts: 1,
1107
+ });
1108
+ rows.push(...(result.rows as Record<string, unknown>[]));
1109
+ }
1110
+ rows.sort(compareBackfillRows);
1111
+ const selectedRows = rows.slice(0, boundedLimit);
1112
+ if (!selectedRows.length) {
1113
+ return {
1114
+ nextCursor: serializeBackfillCursor(parsedCursor),
1115
+ copied: 0,
1116
+ complete: true,
1117
+ };
1118
+ }
1119
+
1120
+ const selectedIds = selectedRows.map((row) => backfillRowCursor(row).id);
1121
+ const hydratedQuery = backfillRowsByIdsSql(selectedIds);
1122
+ const hydratedResult = await db.execute({
1123
+ sql: hydratedQuery.sql,
1124
+ args: hydratedQuery.args,
1125
+ timeoutMs: 20_000,
1126
+ maxAttempts: 1,
1127
+ });
1128
+ const hydratedById = new Map(
1129
+ (hydratedResult.rows as Record<string, unknown>[]).map((row) => [
1130
+ backfillRowCursor(row).id,
1131
+ row,
1132
+ ]),
1133
+ );
1134
+ const selectedEvents = selectedIds
1135
+ .map((id) => hydratedById.get(id))
1136
+ .filter((row): row is Record<string, unknown> => row !== undefined);
1137
+ await insertFirstPartyAnalyticsRows(selectedEvents, configuredTable);
1138
+ const lastCursor = backfillRowCursor(selectedRows[selectedRows.length - 1]!);
1139
+ return {
1140
+ nextCursor: serializeBackfillCursor({
1141
+ receivedAt: lastCursor.receivedAt,
1142
+ id: lastCursor.id,
1143
+ }),
1144
+ copied: selectedEvents.length,
1145
+ complete: rows.length < boundedLimit,
1146
+ };
1147
+ }