@agent-native/core 0.77.24 → 0.78.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/template-analytics.md +5 -0
  4. package/corpus/core/docs/content/tracking.md +45 -0
  5. package/corpus/core/package.json +3 -1
  6. package/corpus/core/src/agent/types.ts +22 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +34 -18
  8. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  9. package/corpus/core/src/client/agent-chat.ts +173 -0
  10. package/corpus/core/src/client/analytics.ts +141 -5
  11. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  12. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  13. package/corpus/core/src/client/composer/types.ts +22 -0
  14. package/corpus/core/src/client/index.ts +14 -0
  15. package/corpus/core/src/client/session-replay.ts +800 -0
  16. package/corpus/core/src/db/schema.ts +12 -0
  17. package/corpus/core/src/deploy/build.ts +75 -0
  18. package/corpus/core/src/private-blob/index.ts +18 -0
  19. package/corpus/core/src/private-blob/registry.ts +241 -0
  20. package/corpus/core/src/private-blob/types.ts +55 -0
  21. package/corpus/core/src/scripts/utils.ts +34 -6
  22. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  23. package/corpus/core/src/server/email.ts +5 -0
  24. package/corpus/core/src/server/index.ts +1 -0
  25. package/corpus/core/src/styles/agent-native.css +24 -0
  26. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
  27. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
  28. package/corpus/templates/analytics/AGENTS.md +32 -1
  29. package/corpus/templates/analytics/DEVELOPING.md +9 -1
  30. package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
  31. package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
  32. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
  33. package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
  34. package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
  35. package/corpus/templates/analytics/actions/navigate.ts +15 -5
  36. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  37. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
  38. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
  39. package/corpus/templates/analytics/actions/view-screen.ts +50 -2
  40. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
  41. package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
  44. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
  45. package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
  53. package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
  54. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
  55. package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
  56. package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
  60. package/corpus/templates/analytics/package.json +5 -2
  61. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
  62. package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
  63. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
  64. package/corpus/templates/analytics/server/db/index.ts +12 -0
  65. package/corpus/templates/analytics/server/db/schema.ts +131 -0
  66. package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
  67. package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
  68. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
  69. package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
  73. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
  74. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
  75. package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
  76. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  77. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
  78. package/corpus/templates/analytics/server/plugins/db.ts +200 -0
  79. package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
  80. package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
  81. package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
  82. package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
  83. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
  84. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
  86. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
  87. package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
  88. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  89. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  90. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  91. package/corpus/templates/assets/AGENTS.md +18 -3
  92. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  93. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  94. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  95. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  96. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  97. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  98. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  99. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  100. package/corpus/templates/assets/actions/navigate.ts +1 -1
  101. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  102. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  103. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  104. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  105. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  106. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  107. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  108. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  109. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  110. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  111. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  112. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  113. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  114. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  115. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  116. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  117. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  118. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  119. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  120. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  121. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  122. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  123. package/corpus/templates/assets/server/db/index.ts +1 -1
  124. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  125. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  126. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  127. package/corpus/templates/assets/shared/api.ts +6 -3
  128. package/dist/agent/types.d.ts +21 -0
  129. package/dist/agent/types.d.ts.map +1 -1
  130. package/dist/agent/types.js.map +1 -1
  131. package/dist/client/AgentPanel.d.ts +6 -2
  132. package/dist/client/AgentPanel.d.ts.map +1 -1
  133. package/dist/client/AgentPanel.js +10 -4
  134. package/dist/client/AgentPanel.js.map +1 -1
  135. package/dist/client/AssistantChat.d.ts +6 -0
  136. package/dist/client/AssistantChat.d.ts.map +1 -1
  137. package/dist/client/AssistantChat.js +15 -3
  138. package/dist/client/AssistantChat.js.map +1 -1
  139. package/dist/client/agent-chat.d.ts +32 -0
  140. package/dist/client/agent-chat.d.ts.map +1 -1
  141. package/dist/client/agent-chat.js +113 -0
  142. package/dist/client/agent-chat.js.map +1 -1
  143. package/dist/client/analytics.d.ts +22 -3
  144. package/dist/client/analytics.d.ts.map +1 -1
  145. package/dist/client/analytics.js +96 -4
  146. package/dist/client/analytics.js.map +1 -1
  147. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  148. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  149. package/dist/client/composer/TiptapComposer.js +228 -56
  150. package/dist/client/composer/TiptapComposer.js.map +1 -1
  151. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  152. package/dist/client/composer/extensions/MentionReference.js +3 -0
  153. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  154. package/dist/client/composer/types.d.ts +21 -0
  155. package/dist/client/composer/types.d.ts.map +1 -1
  156. package/dist/client/composer/types.js.map +1 -1
  157. package/dist/client/index.d.ts +2 -2
  158. package/dist/client/index.d.ts.map +1 -1
  159. package/dist/client/index.js +2 -2
  160. package/dist/client/index.js.map +1 -1
  161. package/dist/client/session-replay.d.ts +41 -0
  162. package/dist/client/session-replay.d.ts.map +1 -0
  163. package/dist/client/session-replay.js +595 -0
  164. package/dist/client/session-replay.js.map +1 -0
  165. package/dist/collab/awareness.d.ts +2 -2
  166. package/dist/collab/awareness.d.ts.map +1 -1
  167. package/dist/collab/routes.d.ts +2 -2
  168. package/dist/db/schema.d.ts +3 -1
  169. package/dist/db/schema.d.ts.map +1 -1
  170. package/dist/db/schema.js +6 -2
  171. package/dist/db/schema.js.map +1 -1
  172. package/dist/deploy/build.d.ts +8 -0
  173. package/dist/deploy/build.d.ts.map +1 -1
  174. package/dist/deploy/build.js +66 -0
  175. package/dist/deploy/build.js.map +1 -1
  176. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +8 -8
  179. package/dist/private-blob/index.d.ts +3 -0
  180. package/dist/private-blob/index.d.ts.map +1 -0
  181. package/dist/private-blob/index.js +2 -0
  182. package/dist/private-blob/index.js.map +1 -0
  183. package/dist/private-blob/registry.d.ts +10 -0
  184. package/dist/private-blob/registry.d.ts.map +1 -0
  185. package/dist/private-blob/registry.js +152 -0
  186. package/dist/private-blob/registry.js.map +1 -0
  187. package/dist/private-blob/types.d.ts +51 -0
  188. package/dist/private-blob/types.d.ts.map +1 -0
  189. package/dist/private-blob/types.js +2 -0
  190. package/dist/private-blob/types.js.map +1 -0
  191. package/dist/resources/handlers.d.ts +3 -3
  192. package/dist/scripts/utils.d.ts +4 -4
  193. package/dist/scripts/utils.d.ts.map +1 -1
  194. package/dist/scripts/utils.js +30 -6
  195. package/dist/scripts/utils.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +5 -0
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  200. package/dist/server/email.d.ts +2 -0
  201. package/dist/server/email.d.ts.map +1 -1
  202. package/dist/server/email.js +3 -0
  203. package/dist/server/email.js.map +1 -1
  204. package/dist/server/index.d.ts +1 -1
  205. package/dist/server/index.d.ts.map +1 -1
  206. package/dist/server/index.js.map +1 -1
  207. package/dist/server/transcribe-voice.d.ts +1 -1
  208. package/dist/styles/agent-native.css +24 -0
  209. package/docs/content/template-analytics.md +5 -0
  210. package/docs/content/tracking.md +45 -0
  211. package/package.json +3 -1
  212. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
@@ -3,8 +3,10 @@ import {
3
3
  text,
4
4
  integer,
5
5
  now,
6
+ index,
6
7
  ownableColumns,
7
8
  createSharesTable,
9
+ uniqueIndex,
8
10
  } from "@agent-native/core/db/schema";
9
11
 
10
12
  /**
@@ -49,6 +51,38 @@ export const dashboardViews = table("dashboard_views", {
49
51
  createdAt: text("created_at").notNull().default(now()),
50
52
  });
51
53
 
54
+ /**
55
+ * Scheduled email snapshots for SQL dashboards. Each row belongs to the user
56
+ * who created the subscription; dashboard access is re-checked before every
57
+ * send so revoking dashboard access also stops future deliveries.
58
+ */
59
+ export const dashboardReportSubscriptions = table(
60
+ "dashboard_report_subscriptions",
61
+ {
62
+ id: text("id").primaryKey(),
63
+ dashboardId: text("dashboard_id").notNull(),
64
+ name: text("name").notNull(),
65
+ recipients: text("recipients").notNull().default("[]"),
66
+ filters: text("filters").notNull().default("{}"),
67
+ frequency: text("frequency", { enum: ["daily"] })
68
+ .notNull()
69
+ .default("daily"),
70
+ timeOfDay: text("time_of_day").notNull().default("09:00"),
71
+ timezone: text("timezone").notNull().default("UTC"),
72
+ enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
73
+ nextRunAt: text("next_run_at"),
74
+ lastRunAt: text("last_run_at"),
75
+ lastStatus: text("last_status", {
76
+ enum: ["success", "error", "running"],
77
+ }),
78
+ lastError: text("last_error"),
79
+ createdAt: text("created_at").notNull().default(now()),
80
+ updatedAt: text("updated_at").notNull().default(now()),
81
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
82
+ orgId: text("org_id"),
83
+ },
84
+ );
85
+
52
86
  /**
53
87
  * Ad-hoc analyses. Previously stored in the settings KV store under
54
88
  * `adhoc-analysis-{id}`. Those keys are read as a fallback during lazy
@@ -102,6 +136,13 @@ export const analyticsPublicKeys = table("analytics_public_keys", {
102
136
  name: text("name").notNull(),
103
137
  publicKey: text("public_key").notNull(),
104
138
  publicKeyPrefix: text("public_key_prefix").notNull(),
139
+ replayAllowedOrigins: text("replay_allowed_origins").notNull().default("[]"),
140
+ replayMaxBytesPerDay: integer("replay_max_bytes_per_day")
141
+ .notNull()
142
+ .default(100 * 1024 * 1024),
143
+ replayMaxRequestsPerMinute: integer("replay_max_requests_per_minute")
144
+ .notNull()
145
+ .default(120),
105
146
  createdAt: text("created_at").notNull().default(now()),
106
147
  lastUsedAt: text("last_used_at"),
107
148
  revokedAt: text("revoked_at"),
@@ -137,3 +178,93 @@ export const analyticsEvents = table("analytics_events", {
137
178
  ownerEmail: text("owner_email").notNull().default("local@localhost"),
138
179
  orgId: text("org_id"),
139
180
  });
181
+
182
+ /**
183
+ * Session replay summaries recorded through the first-party analytics replay
184
+ * endpoint. Raw replay chunks live in session_replay_chunks and are only read
185
+ * through scoped replay helpers, not first-party dashboard SQL.
186
+ */
187
+ export const sessionRecordings = table("session_recordings", {
188
+ id: text("id").primaryKey(),
189
+ publicKeyId: text("public_key_id").notNull(),
190
+ clientRecordingId: text("client_recording_id").notNull(),
191
+ sessionId: text("session_id").notNull(),
192
+ userId: text("user_id"),
193
+ anonymousId: text("anonymous_id"),
194
+ userKey: text("user_key"),
195
+ startedAt: text("started_at").notNull(),
196
+ endedAt: text("ended_at"),
197
+ durationMs: integer("duration_ms"),
198
+ chunkCount: integer("chunk_count").notNull().default(0),
199
+ eventCount: integer("event_count").notNull().default(0),
200
+ totalBytes: integer("total_bytes").notNull().default(0),
201
+ pageCount: integer("page_count").notNull().default(0),
202
+ errorCount: integer("error_count").notNull().default(0),
203
+ rageClickCount: integer("rage_click_count").notNull().default(0),
204
+ privacyMode: text("privacy_mode").notNull().default("unknown"),
205
+ firstUrl: text("first_url"),
206
+ lastUrl: text("last_url"),
207
+ path: text("path"),
208
+ hostname: text("hostname"),
209
+ referrer: text("referrer"),
210
+ app: text("app"),
211
+ template: text("template"),
212
+ status: text("status", { enum: ["active", "completed"] })
213
+ .notNull()
214
+ .default("active"),
215
+ metadata: text("metadata").notNull().default("{}"),
216
+ createdAt: text("created_at").notNull().default(now()),
217
+ updatedAt: text("updated_at").notNull().default(now()),
218
+ lastIngestedAt: text("last_ingested_at"),
219
+ ...ownableColumns(),
220
+ });
221
+
222
+ export const sessionRecordingShares = createSharesTable(
223
+ "session_recording_shares",
224
+ );
225
+
226
+ export const sessionReplayChunks = table(
227
+ "session_replay_chunks",
228
+ {
229
+ id: text("id").primaryKey(),
230
+ recordingId: text("recording_id").notNull(),
231
+ seq: integer("seq").notNull(),
232
+ checksum: text("checksum").notNull(),
233
+ byteLength: integer("byte_length").notNull().default(0),
234
+ eventCount: integer("event_count").notNull().default(0),
235
+ startedAt: text("started_at"),
236
+ endedAt: text("ended_at"),
237
+ storageKind: text("storage_kind", { enum: ["inline", "blob"] }).notNull(),
238
+ storageRef: text("storage_ref"),
239
+ inlineData: text("inline_data"),
240
+ createdAt: text("created_at").notNull().default(now()),
241
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
242
+ orgId: text("org_id"),
243
+ },
244
+ (chunk) => ({
245
+ recordingSeqUnique: uniqueIndex(
246
+ "session_replay_chunks_recording_seq_idx",
247
+ ).on(chunk.recordingId, chunk.seq),
248
+ }),
249
+ );
250
+
251
+ export const sessionReplayIngests = table(
252
+ "session_replay_ingests",
253
+ {
254
+ id: text("id").primaryKey(),
255
+ publicKeyId: text("public_key_id").notNull(),
256
+ recordingId: text("recording_id").notNull(),
257
+ byteLength: integer("byte_length").notNull().default(0),
258
+ createdAt: text("created_at").notNull().default(now()),
259
+ ownerEmail: text("owner_email").notNull().default("local@localhost"),
260
+ orgId: text("org_id"),
261
+ },
262
+ (ingest) => ({
263
+ publicKeyCreatedAtIdx: index(
264
+ "session_replay_ingests_public_key_created_at_idx",
265
+ ).on(ingest.publicKeyId, ingest.createdAt),
266
+ recordingIdx: index("session_replay_ingests_recording_idx").on(
267
+ ingest.recordingId,
268
+ ),
269
+ }),
270
+ );
@@ -0,0 +1,281 @@
1
+ import { readBody } from "@agent-native/core/server";
2
+ import {
3
+ defineEventHandler,
4
+ getHeader,
5
+ getQuery,
6
+ getRouterParam,
7
+ setResponseHeader,
8
+ setResponseStatus,
9
+ } from "h3";
10
+
11
+ import { runApiHandlerWithContext } from "../lib/credentials";
12
+ import {
13
+ getSessionReplayManifest,
14
+ getSessionReplayEvents,
15
+ getSessionReplaySummary,
16
+ listSessionRecordings,
17
+ parseSessionReplayIngestPayload,
18
+ recordSessionReplayChunks,
19
+ readSessionReplayChunkBytes,
20
+ type SessionReplayListFilters,
21
+ } from "../lib/session-replay.js";
22
+
23
+ const PUBLIC_KEY_QUERY_NAMES = new Set([
24
+ "apiKey",
25
+ "analyticsKey",
26
+ "key",
27
+ "publicKey",
28
+ "writeKey",
29
+ ]);
30
+
31
+ function readOrigin(event: any): string | null {
32
+ const origin = getHeader(event, "origin");
33
+ if (typeof origin === "string" && origin.trim()) return origin.trim();
34
+ return null;
35
+ }
36
+
37
+ function setCors(event: any): void {
38
+ const origin = readOrigin(event);
39
+ if (origin) {
40
+ setResponseHeader(event, "Access-Control-Allow-Origin", origin);
41
+ setResponseHeader(event, "Vary", "Origin");
42
+ }
43
+ setResponseHeader(
44
+ event,
45
+ "Access-Control-Allow-Methods",
46
+ "GET, POST, OPTIONS",
47
+ );
48
+ setResponseHeader(
49
+ event,
50
+ "Access-Control-Allow-Headers",
51
+ "content-type, x-agent-native-analytics-key",
52
+ );
53
+ setResponseHeader(event, "Access-Control-Max-Age", "86400");
54
+ }
55
+
56
+ function statusFromError(error: any): number {
57
+ return typeof error?.statusCode === "number" ? error.statusCode : 400;
58
+ }
59
+
60
+ function messageFromError(error: any): string {
61
+ return error?.message || String(error);
62
+ }
63
+
64
+ function hasQueryKey(query: Record<string, unknown>): boolean {
65
+ return Object.keys(query).some((key) => PUBLIC_KEY_QUERY_NAMES.has(key));
66
+ }
67
+
68
+ function injectHeaderKey(body: unknown, headerKey?: string): unknown {
69
+ if (!headerKey) return body;
70
+ if (typeof body === "string" && body.trim()) {
71
+ return { ...JSON.parse(body), publicKey: headerKey };
72
+ }
73
+ if (body && typeof body === "object" && !Array.isArray(body)) {
74
+ return { ...(body as Record<string, unknown>), publicKey: headerKey };
75
+ }
76
+ return { publicKey: headerKey };
77
+ }
78
+
79
+ function requestByteLength(body: unknown): number {
80
+ if (typeof body === "string") return Buffer.byteLength(body, "utf8");
81
+ try {
82
+ return Buffer.byteLength(JSON.stringify(body), "utf8");
83
+ } catch {
84
+ return 0;
85
+ }
86
+ }
87
+
88
+ function asString(value: unknown): string | undefined {
89
+ if (typeof value === "string" && value.trim()) return value.trim();
90
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].trim()) {
91
+ return value[0].trim();
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function asInt(value: unknown): number | undefined {
97
+ const raw = asString(value);
98
+ if (!raw || !/^\d+$/.test(raw)) return undefined;
99
+ return Number(raw);
100
+ }
101
+
102
+ function asBool(value: unknown): boolean | undefined {
103
+ const raw = asString(value)?.toLowerCase();
104
+ if (!raw) return undefined;
105
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
106
+ if (["0", "false", "no", "off"].includes(raw)) return false;
107
+ return undefined;
108
+ }
109
+
110
+ function asStatus(value: unknown): "active" | "completed" | undefined {
111
+ const raw = asString(value);
112
+ return raw === "active" || raw === "completed" ? raw : undefined;
113
+ }
114
+
115
+ function listFiltersFromQuery(
116
+ query: Record<string, unknown>,
117
+ ): SessionReplayListFilters {
118
+ return {
119
+ query: asString(query.query) ?? asString(query.q),
120
+ app: asString(query.app),
121
+ template: asString(query.template),
122
+ sessionId: asString(query.sessionId),
123
+ userId: asString(query.userId),
124
+ anonymousId: asString(query.anonymousId),
125
+ path: asString(query.path),
126
+ from: asString(query.from),
127
+ to: asString(query.to),
128
+ minDurationMs: asInt(query.minDurationMs),
129
+ hasErrors: asBool(query.hasErrors),
130
+ hasRageClicks: asBool(query.hasRageClicks),
131
+ status: asStatus(query.status),
132
+ limit: asInt(query.limit),
133
+ };
134
+ }
135
+
136
+ export const handleSessionReplayOptions = defineEventHandler((event) => {
137
+ setCors(event);
138
+ setResponseStatus(event, 204);
139
+ return "";
140
+ });
141
+
142
+ export const handleSessionReplayIngest = defineEventHandler(async (event) => {
143
+ setCors(event);
144
+ try {
145
+ const query = getQuery(event);
146
+ if (hasQueryKey(query)) {
147
+ throw Object.assign(
148
+ new Error(
149
+ "Analytics public keys must be sent in the request body or x-agent-native-analytics-key header, not the query string",
150
+ ),
151
+ { statusCode: 400 },
152
+ );
153
+ }
154
+
155
+ const headerKey = getHeader(event, "x-agent-native-analytics-key");
156
+ const rawBody = await readBody(event);
157
+ const body = injectHeaderKey(rawBody, headerKey);
158
+ const parsed = parseSessionReplayIngestPayload(body);
159
+ const result = await recordSessionReplayChunks(parsed, {
160
+ origin: readOrigin(event),
161
+ requestBytes: requestByteLength(rawBody),
162
+ });
163
+ setResponseStatus(event, 202);
164
+ return { success: true, ...result };
165
+ } catch (error: any) {
166
+ setResponseStatus(event, statusFromError(error));
167
+ return { error: messageFromError(error) };
168
+ }
169
+ });
170
+
171
+ export const handleSessionReplayList = defineEventHandler(async (event) => {
172
+ return runApiHandlerWithContext(event, async (ctx) => {
173
+ try {
174
+ const recordings = await listSessionRecordings(
175
+ { userEmail: ctx.userEmail, orgId: ctx.orgId ?? null },
176
+ listFiltersFromQuery(getQuery(event)),
177
+ );
178
+ return { recordings };
179
+ } catch (error: any) {
180
+ setResponseStatus(event, statusFromError(error));
181
+ return { error: messageFromError(error) };
182
+ }
183
+ });
184
+ });
185
+
186
+ export const handleSessionReplaySummary = defineEventHandler(async (event) => {
187
+ const recordingId = getRouterParam(event, "recordingId");
188
+ if (!recordingId) {
189
+ setResponseStatus(event, 400);
190
+ return { error: "Missing recordingId" };
191
+ }
192
+
193
+ return runApiHandlerWithContext(event, async (ctx) => {
194
+ try {
195
+ const recording = await getSessionReplaySummary(recordingId, {
196
+ userEmail: ctx.userEmail,
197
+ orgId: ctx.orgId ?? null,
198
+ });
199
+ return { recording };
200
+ } catch (error: any) {
201
+ setResponseStatus(event, statusFromError(error));
202
+ return { error: messageFromError(error) };
203
+ }
204
+ });
205
+ });
206
+
207
+ export const handleSessionReplayEvents = defineEventHandler(async (event) => {
208
+ const recordingId = getRouterParam(event, "recordingId");
209
+ if (!recordingId) {
210
+ setResponseStatus(event, 400);
211
+ return { error: "Missing recordingId" };
212
+ }
213
+
214
+ return runApiHandlerWithContext(event, async (ctx) => {
215
+ try {
216
+ const query = getQuery(event);
217
+ return await getSessionReplayEvents(
218
+ recordingId,
219
+ { userEmail: ctx.userEmail, orgId: ctx.orgId ?? null },
220
+ {
221
+ startSeq: asInt(query.startSeq),
222
+ endSeq: asInt(query.endSeq),
223
+ limit: asInt(query.limit),
224
+ },
225
+ );
226
+ } catch (error: any) {
227
+ setResponseStatus(event, statusFromError(error));
228
+ return { error: messageFromError(error) };
229
+ }
230
+ });
231
+ });
232
+
233
+ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
234
+ const recordingId = getRouterParam(event, "recordingId");
235
+ if (!recordingId) {
236
+ setResponseStatus(event, 400);
237
+ return { error: "Missing recordingId" };
238
+ }
239
+
240
+ return runApiHandlerWithContext(event, async (ctx) => {
241
+ try {
242
+ return await getSessionReplayManifest(recordingId, {
243
+ userEmail: ctx.userEmail,
244
+ orgId: ctx.orgId ?? null,
245
+ });
246
+ } catch (error: any) {
247
+ setResponseStatus(event, statusFromError(error));
248
+ return { error: messageFromError(error) };
249
+ }
250
+ });
251
+ });
252
+
253
+ export const handleSessionReplayChunkBytes = defineEventHandler(
254
+ async (event) => {
255
+ const recordingId = getRouterParam(event, "recordingId");
256
+ const seqRaw = getRouterParam(event, "seq");
257
+ const seq = asInt(seqRaw);
258
+ if (!recordingId || seq === undefined) {
259
+ setResponseStatus(event, 400);
260
+ return { error: "Missing recordingId or seq" };
261
+ }
262
+
263
+ return runApiHandlerWithContext(event, async (ctx) => {
264
+ try {
265
+ const result = await readSessionReplayChunkBytes(recordingId, seq, {
266
+ userEmail: ctx.userEmail,
267
+ orgId: ctx.orgId ?? null,
268
+ });
269
+ setResponseHeader(event, "Content-Type", "application/json");
270
+ setResponseHeader(event, "Content-Encoding", "gzip");
271
+ setResponseHeader(event, "Cache-Control", "no-store");
272
+ setResponseHeader(event, "X-Session-Replay-Seq", String(result.seq));
273
+ setResponseHeader(event, "X-Session-Replay-Checksum", result.checksum);
274
+ return result.data;
275
+ } catch (error: any) {
276
+ setResponseStatus(event, statusFromError(error));
277
+ return { error: messageFromError(error) };
278
+ }
279
+ });
280
+ },
281
+ );