@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
@@ -0,0 +1,419 @@
1
+ import { CodeSurface } from "@agent-native/core/blocks";
2
+ import { useActionQuery, useT } from "@agent-native/core/client";
3
+ import {
4
+ IconCode,
5
+ IconFilter,
6
+ IconPlayerPlay,
7
+ IconRefresh,
8
+ IconSearch,
9
+ } from "@tabler/icons-react";
10
+ import { useMemo } from "react";
11
+ import { Link, useSearchParams } from "react-router";
12
+
13
+ import { Badge } from "@/components/ui/badge";
14
+ import { Button } from "@/components/ui/button";
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from "@/components/ui/card";
22
+ import { Input } from "@/components/ui/input";
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ } from "@/components/ui/select";
30
+ import { Skeleton } from "@/components/ui/skeleton";
31
+ import {
32
+ Table,
33
+ TableBody,
34
+ TableCell,
35
+ TableHead,
36
+ TableHeader,
37
+ TableRow,
38
+ } from "@/components/ui/table";
39
+ import { cn } from "@/lib/utils";
40
+
41
+ type ReplayRange = "24h" | "7d" | "30d" | "90d" | "all";
42
+
43
+ type SessionRecordingSummary = {
44
+ id: string;
45
+ clientRecordingId: string;
46
+ sessionId: string;
47
+ userId: string | null;
48
+ anonymousId: string | null;
49
+ userKey: string | null;
50
+ startedAt: string;
51
+ endedAt: string | null;
52
+ durationMs: number | null;
53
+ chunkCount: number;
54
+ eventCount: number;
55
+ totalBytes: number;
56
+ pageCount: number;
57
+ errorCount: number;
58
+ rageClickCount: number;
59
+ privacyMode: string;
60
+ firstUrl: string | null;
61
+ lastUrl: string | null;
62
+ path: string | null;
63
+ hostname: string | null;
64
+ referrer: string | null;
65
+ app: string | null;
66
+ template: string | null;
67
+ status: "active" | "completed";
68
+ createdAt: string;
69
+ updatedAt: string;
70
+ lastIngestedAt: string | null;
71
+ };
72
+
73
+ const RANGE_OPTIONS: ReplayRange[] = ["24h", "7d", "30d", "90d", "all"];
74
+
75
+ export default function SessionsPage() {
76
+ const t = useT();
77
+ const [searchParams, setSearchParams] = useSearchParams();
78
+ const range = readRange(searchParams.get("range"));
79
+ const app = searchParams.get("app") ?? "";
80
+ const query = searchParams.get("q") ?? "";
81
+ const from = useMemo(() => rangeToFrom(range), [range]);
82
+
83
+ const { data, isLoading, isFetching, refetch, error } = useActionQuery<
84
+ SessionRecordingSummary[]
85
+ >(
86
+ "list-session-recordings",
87
+ {
88
+ from: from ?? undefined,
89
+ app: app || undefined,
90
+ query: query || undefined,
91
+ limit: 100,
92
+ },
93
+ { staleTime: 30_000 },
94
+ );
95
+
96
+ const recordings = data ?? [];
97
+
98
+ function updateFilter(key: string, value: string) {
99
+ const next = new URLSearchParams(searchParams);
100
+ const emptyDefault =
101
+ (key === "range" && value === "30d") || value.trim() === "";
102
+ if (emptyDefault) next.delete(key);
103
+ else next.set(key, value);
104
+ setSearchParams(next, { replace: true });
105
+ }
106
+
107
+ return (
108
+ <div className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6">
109
+ <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
110
+ <div className="min-w-0 space-y-2">
111
+ <div className="flex items-center gap-2">
112
+ <IconPlayerPlay className="h-5 w-5 text-primary" />
113
+ <h1 className="text-2xl font-semibold tracking-normal">
114
+ {t("sessions.title")}
115
+ </h1>
116
+ </div>
117
+ <p className="max-w-3xl text-sm text-muted-foreground">
118
+ {t("sessions.description")}
119
+ </p>
120
+ </div>
121
+ <Button
122
+ type="button"
123
+ variant="outline"
124
+ onClick={() => void refetch()}
125
+ disabled={isFetching}
126
+ >
127
+ <IconRefresh
128
+ className={cn("h-4 w-4", isFetching && "animate-spin")}
129
+ />
130
+ {t("sessions.refresh")}
131
+ </Button>
132
+ </div>
133
+
134
+ <Card>
135
+ <CardHeader className="pb-4">
136
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
137
+ <div>
138
+ <CardTitle className="flex items-center gap-2 text-base">
139
+ <IconFilter className="h-4 w-4" />
140
+ {t("sessions.filters")}
141
+ </CardTitle>
142
+ <CardDescription>
143
+ {t("sessions.filtersDescription")}
144
+ </CardDescription>
145
+ </div>
146
+ <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
147
+ {data ? (
148
+ <Badge variant="outline">
149
+ {t("sessions.showing", {
150
+ count: String(recordings.length),
151
+ })}
152
+ </Badge>
153
+ ) : null}
154
+ </div>
155
+ </div>
156
+ </CardHeader>
157
+ <CardContent>
158
+ <div className="grid gap-3 md:grid-cols-[minmax(0,1.4fr)_minmax(160px,0.7fr)_160px]">
159
+ <div className="relative">
160
+ <IconSearch className="pointer-events-none absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
161
+ <Input
162
+ value={query}
163
+ onChange={(event) => updateFilter("q", event.target.value)}
164
+ placeholder={t("sessions.searchPlaceholder")}
165
+ className="ps-9"
166
+ />
167
+ </div>
168
+ <Input
169
+ value={app}
170
+ onChange={(event) => updateFilter("app", event.target.value)}
171
+ placeholder={t("sessions.appPlaceholder")}
172
+ />
173
+ <Select
174
+ value={range}
175
+ onValueChange={(value) => updateFilter("range", value)}
176
+ >
177
+ <SelectTrigger aria-label={t("sessions.range")}>
178
+ <SelectValue />
179
+ </SelectTrigger>
180
+ <SelectContent>
181
+ {RANGE_OPTIONS.map((value) => (
182
+ <SelectItem key={value} value={value}>
183
+ {rangeLabel(value, t)}
184
+ </SelectItem>
185
+ ))}
186
+ </SelectContent>
187
+ </Select>
188
+ </div>
189
+ </CardContent>
190
+ </Card>
191
+
192
+ <Card>
193
+ <CardContent className="p-0">
194
+ {error ? (
195
+ <div className="p-6 text-sm text-destructive">
196
+ {t("sessions.loadFailed", { message: error.message })}
197
+ </div>
198
+ ) : isLoading ? (
199
+ <SessionSkeleton />
200
+ ) : recordings.length === 0 ? (
201
+ <EmptySessionsState />
202
+ ) : (
203
+ <div className="overflow-x-auto">
204
+ <Table>
205
+ <TableHeader>
206
+ <TableRow>
207
+ <TableHead>{t("sessions.session")}</TableHead>
208
+ <TableHead>{t("sessions.app")}</TableHead>
209
+ <TableHead>{t("sessions.visitor")}</TableHead>
210
+ <TableHead>{t("sessions.lastSeen")}</TableHead>
211
+ <TableHead>{t("sessions.duration")}</TableHead>
212
+ <TableHead className="text-end">
213
+ {t("sessions.events")}
214
+ </TableHead>
215
+ <TableHead className="text-end">
216
+ {t("sessions.chunks")}
217
+ </TableHead>
218
+ <TableHead className="text-end">
219
+ {t("sessions.pages")}
220
+ </TableHead>
221
+ </TableRow>
222
+ </TableHeader>
223
+ <TableBody>
224
+ {recordings.map((recording) => (
225
+ <TableRow key={recording.id}>
226
+ <TableCell className="min-w-[240px]">
227
+ <Link
228
+ to={`/sessions/${encodeURIComponent(recording.id)}`}
229
+ className="font-mono text-xs font-medium text-primary hover:underline"
230
+ >
231
+ {shortId(recording.sessionId)}
232
+ </Link>
233
+ <div className="mt-1 flex flex-wrap gap-1.5">
234
+ <Badge className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/20 dark:text-emerald-300">
235
+ {t("sessions.replayReady")}
236
+ </Badge>
237
+ {recording.errorCount > 0 ? (
238
+ <Badge variant="destructive">
239
+ {t("sessions.errorCount", {
240
+ count: String(recording.errorCount),
241
+ })}
242
+ </Badge>
243
+ ) : null}
244
+ {recording.rageClickCount > 0 ? (
245
+ <Badge variant="secondary">
246
+ {t("sessions.rageClicks", {
247
+ count: String(recording.rageClickCount),
248
+ })}
249
+ </Badge>
250
+ ) : null}
251
+ </div>
252
+ </TableCell>
253
+ <TableCell className="min-w-[130px]">
254
+ {recording.app || recording.template || "-"}
255
+ </TableCell>
256
+ <TableCell className="min-w-[160px]">
257
+ <div className="truncate">
258
+ {visitorLabel(recording, t)}
259
+ </div>
260
+ </TableCell>
261
+ <TableCell className="min-w-[140px]">
262
+ {formatDateTime(
263
+ recording.endedAt ??
264
+ recording.lastIngestedAt ??
265
+ recording.startedAt,
266
+ )}
267
+ </TableCell>
268
+ <TableCell>
269
+ {formatDuration(recording.durationMs)}
270
+ </TableCell>
271
+ <TableCell className="text-end">
272
+ {formatNumber(recording.eventCount)}
273
+ </TableCell>
274
+ <TableCell className="text-end">
275
+ {formatNumber(recording.chunkCount)}
276
+ </TableCell>
277
+ <TableCell className="text-end">
278
+ {formatNumber(recording.pageCount)}
279
+ </TableCell>
280
+ </TableRow>
281
+ ))}
282
+ </TableBody>
283
+ </Table>
284
+ </div>
285
+ )}
286
+ </CardContent>
287
+ </Card>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ function EmptySessionsState() {
293
+ const t = useT();
294
+ return (
295
+ <div className="grid min-h-[380px] gap-6 p-6 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:p-8">
296
+ <div className="flex flex-col justify-center gap-3">
297
+ <div className="flex h-10 w-10 items-center justify-center rounded-md border bg-muted/40">
298
+ <IconPlayerPlay className="h-5 w-5 text-primary" />
299
+ </div>
300
+ <div className="space-y-2">
301
+ <h2 className="text-lg font-semibold">{t("sessions.noSessions")}</h2>
302
+ <p className="max-w-xl text-sm text-muted-foreground">
303
+ {t("sessions.noSessionsDescription")}
304
+ </p>
305
+ </div>
306
+ <div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
307
+ <Badge variant="outline">{t("sessions.emptyStepKey")}</Badge>
308
+ <Badge variant="outline">{t("sessions.emptyStepEnv")}</Badge>
309
+ <Badge variant="outline">{t("sessions.emptyStepVisit")}</Badge>
310
+ </div>
311
+ </div>
312
+ <div className="overflow-hidden rounded-md border bg-muted/30">
313
+ <div className="flex items-center gap-2 border-b px-4 py-3 text-sm font-medium">
314
+ <IconCode className="h-4 w-4" />
315
+ {t("sessions.installSnippetTitle")}
316
+ </div>
317
+ <CodeSurface
318
+ code={SESSION_REPLAY_SNIPPET}
319
+ language="typescript"
320
+ maxLines={null}
321
+ className="mt-0 rounded-none border-0 bg-transparent"
322
+ />
323
+ </div>
324
+ </div>
325
+ );
326
+ }
327
+
328
+ function SessionSkeleton() {
329
+ return (
330
+ <div className="space-y-3 p-6">
331
+ {Array.from({ length: 7 }).map((_, index) => (
332
+ <Skeleton key={index} className="h-12 w-full" />
333
+ ))}
334
+ </div>
335
+ );
336
+ }
337
+
338
+ function readRange(value: string | null): ReplayRange {
339
+ return RANGE_OPTIONS.includes(value as ReplayRange)
340
+ ? (value as ReplayRange)
341
+ : "30d";
342
+ }
343
+
344
+ function rangeToFrom(range: ReplayRange): string | null {
345
+ if (range === "all") return null;
346
+ const hours =
347
+ range === "24h" ? 24 : range === "7d" ? 168 : range === "90d" ? 2160 : 720;
348
+ return new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
349
+ }
350
+
351
+ function rangeLabel(value: ReplayRange, t: ReturnType<typeof useT>): string {
352
+ if (value === "24h") return t("sessions.last24h");
353
+ if (value === "7d") return t("sessions.last7d");
354
+ if (value === "30d") return t("sessions.last30d");
355
+ if (value === "90d") return t("sessions.last90d");
356
+ return t("sessions.allTime");
357
+ }
358
+
359
+ function visitorLabel(
360
+ recording: SessionRecordingSummary,
361
+ t: ReturnType<typeof useT>,
362
+ ): string {
363
+ return (
364
+ recording.userId ||
365
+ recording.userKey ||
366
+ recording.anonymousId ||
367
+ t("sessions.anonymous")
368
+ );
369
+ }
370
+
371
+ function shortId(value: string): string {
372
+ return value.length > 22
373
+ ? `${value.slice(0, 10)}...${value.slice(-8)}`
374
+ : value;
375
+ }
376
+
377
+ function formatDateTime(value: string): string {
378
+ const date = new Date(value);
379
+ if (!Number.isFinite(date.getTime())) return value;
380
+ return new Intl.DateTimeFormat(undefined, {
381
+ month: "short",
382
+ day: "numeric",
383
+ hour: "numeric",
384
+ minute: "2-digit",
385
+ }).format(date);
386
+ }
387
+
388
+ function formatDuration(ms: number | null): string {
389
+ if (!ms || !Number.isFinite(ms) || ms <= 0) return "0s";
390
+ const seconds = Math.round(ms / 1000);
391
+ if (seconds < 60) return `${seconds}s`;
392
+ const minutes = Math.floor(seconds / 60);
393
+ const remainingSeconds = seconds % 60;
394
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
395
+ const hours = Math.floor(minutes / 60);
396
+ const remainingMinutes = minutes % 60;
397
+ return `${hours}h ${remainingMinutes}m`;
398
+ }
399
+
400
+ function formatNumber(value: number): string {
401
+ return new Intl.NumberFormat().format(value);
402
+ }
403
+
404
+ const SESSION_REPLAY_SNIPPET = `// Agent Native templates already call configureTracking().
405
+ import { configureTracking } from "@agent-native/core/client";
406
+
407
+ configureTracking({
408
+ key: "anpk_...",
409
+ endpoint: "https://analytics.example.com/api/analytics/track",
410
+ sessionReplay: {
411
+ enabled: true,
412
+ sampleRate: 1,
413
+ },
414
+ getDefaultProps: (_event, props) => ({
415
+ ...props,
416
+ app: "my-app",
417
+ template: "my-template",
418
+ }),
419
+ });`;
@@ -1,10 +1,17 @@
1
- import { messagesByLocale } from "@/i18n-data";
2
- import OverviewPage from "@/pages/overview/OverviewPage";
1
+ import { redirect, type LoaderFunctionArgs } from "react-router";
3
2
 
4
- export function meta() {
5
- return [{ title: messagesByLocale["en-US"].routeTitles.overview }];
3
+ function target(url: URL): string {
4
+ return `/ask${url.search}${url.hash}`;
6
5
  }
7
6
 
8
- export default function OverviewRoute() {
9
- return <OverviewPage />;
7
+ export function loader({ url }: LoaderFunctionArgs) {
8
+ throw redirect(target(url));
9
+ }
10
+
11
+ export function clientLoader({ url }: LoaderFunctionArgs) {
12
+ throw redirect(target(url));
13
+ }
14
+
15
+ export default function OverviewAliasRoute() {
16
+ return null;
10
17
  }
@@ -0,0 +1,10 @@
1
+ import { messagesByLocale } from "@/i18n-data";
2
+ import SessionDetailPage from "@/pages/sessions/SessionDetailPage";
3
+
4
+ export function meta() {
5
+ return [{ title: messagesByLocale["en-US"].routeTitles.session }];
6
+ }
7
+
8
+ export default function SessionDetailRoute() {
9
+ return <SessionDetailPage />;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { messagesByLocale } from "@/i18n-data";
2
+ import SessionsPage from "@/pages/sessions/SessionsPage";
3
+
4
+ export function meta() {
5
+ return [{ title: messagesByLocale["en-US"].routeTitles.sessions }];
6
+ }
7
+
8
+ export default function SessionsRoute() {
9
+ return <SessionsPage />;
10
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: removed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Analytics now opens directly to Ask instead of the old Overview page.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Dashboard charts now show reliable blue placement lines while dragging.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Dashboards can now send scheduled daily email reports with inline screenshots of the live dashboard.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Session replay browser sessions can now be reviewed from a Sessions page and opened from first-party dashboard rows.
@@ -4,8 +4,8 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "agent-native dev",
8
- "build": "agent-native build",
7
+ "dev": "DATABASE_URL=${ANALYTICS_DATABASE_URL:-file:./data/app.db} agent-native dev",
8
+ "build": "agent-native build && tsx scripts/emit-netlify-dashboard-report-cron.ts",
9
9
  "start": "agent-native start",
10
10
  "test": "vitest --run",
11
11
  "format.fix": "oxfmt --write .",
@@ -20,10 +20,13 @@
20
20
  "@dnd-kit/utilities": "^3.2.2",
21
21
  "@fontsource-variable/inter": "^5.2.8",
22
22
  "@libsql/client": "^0.15.8",
23
+ "@rrweb/replay": "2.0.1",
24
+ "@sparticuz/chromium-min": "^149.0.0",
23
25
  "@tabler/icons-react": "catalog:",
24
26
  "drizzle-orm": "^0.45.2",
25
27
  "h3": "^2.0.1-rc.20",
26
28
  "isbot": "^5",
29
+ "playwright-core": "^1.61.1",
27
30
  "postgres": "^3.4.9",
28
31
  "sql-formatter": "15.7.3",
29
32
  "zod": "^4.3.6"
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { randomBytes } from "node:crypto";
4
+ import { existsSync, rmSync, cpSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const FUNCTIONS_DIR = path.join(ROOT, ".netlify", "functions-internal");
10
+ const SERVER_DIR = path.join(FUNCTIONS_DIR, "server");
11
+ const SCHEDULED_NAME = "dashboard-report-cron";
12
+ const WORKER_NAME = "dashboard-report-sweep-background";
13
+ const ROUTE_PATH = "/api/dashboard-reports/run";
14
+ const SCHEDULE = "*/15 * * * *";
15
+
16
+ function ensureDir(dir: string) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+
20
+ function emitScheduledTrigger(token: string) {
21
+ const dest = path.join(FUNCTIONS_DIR, SCHEDULED_NAME);
22
+ rmSync(dest, { recursive: true, force: true });
23
+ ensureDir(dest);
24
+
25
+ const source = `const WORKER_PATH = "/.netlify/functions/${WORKER_NAME}";
26
+ const CRON_TOKEN = ${JSON.stringify(token)};
27
+
28
+ function siteOrigin(request) {
29
+ const configured = process.env.URL || process.env.DEPLOY_URL;
30
+ if (configured) return configured;
31
+ const url = new URL(request.url);
32
+ return url.origin;
33
+ }
34
+
35
+ export default async function handler(request) {
36
+ const url = new URL(WORKER_PATH, siteOrigin(request));
37
+ const response = await fetch(url.toString(), {
38
+ method: "POST",
39
+ headers: {
40
+ "content-type": "application/json",
41
+ "x-agent-native-dashboard-report-cron": CRON_TOKEN,
42
+ },
43
+ body: JSON.stringify({ scheduled: true }),
44
+ });
45
+
46
+ if (!response.ok && response.status !== 202) {
47
+ console.error(
48
+ "[dashboard-report-cron] Background sweep trigger failed:",
49
+ response.status,
50
+ await response.text().catch(() => ""),
51
+ );
52
+ }
53
+ }
54
+
55
+ export const config = {
56
+ name: "dashboard report cron trigger",
57
+ generator: "agent-native analytics build",
58
+ schedule: ${JSON.stringify(SCHEDULE)},
59
+ };
60
+ `;
61
+
62
+ writeFileSync(path.join(dest, `${SCHEDULED_NAME}.mjs`), source);
63
+ }
64
+
65
+ function emitBackgroundWorker(token: string) {
66
+ const dest = path.join(FUNCTIONS_DIR, WORKER_NAME);
67
+ rmSync(dest, { recursive: true, force: true });
68
+ cpSync(SERVER_DIR, dest, { recursive: true });
69
+ rmSync(path.join(dest, "server.mjs"), { force: true });
70
+
71
+ const source = `globalThis.__AGENT_NATIVE_DASHBOARD_REPORT_SCHEDULED_RUNTIME__ = true;
72
+
73
+ const CRON_TOKEN = ${JSON.stringify(token)};
74
+ const ROUTE_PATH = ${JSON.stringify(ROUTE_PATH)};
75
+ let cachedHandler;
76
+
77
+ function timingSafeEquals(a, b) {
78
+ if (a.length !== b.length) return false;
79
+ let diff = 0;
80
+ for (let i = 0; i < a.length; i += 1) {
81
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
82
+ }
83
+ return diff === 0;
84
+ }
85
+
86
+ export default async function handler(request, context) {
87
+ const token = request.headers.get("x-agent-native-dashboard-report-cron") || "";
88
+ if (!timingSafeEquals(token, CRON_TOKEN)) {
89
+ return new Response("Unauthorized", { status: 401 });
90
+ }
91
+
92
+ cachedHandler ??= (await import("./main.mjs")).default;
93
+ const url = new URL(request.url);
94
+ url.pathname = ROUTE_PATH;
95
+ url.search = "";
96
+
97
+ const rewritten = new Request(url.toString(), {
98
+ method: "POST",
99
+ headers: new Headers({ "content-type": "application/json" }),
100
+ body: JSON.stringify({ scheduled: true }),
101
+ });
102
+
103
+ return await cachedHandler(rewritten, context);
104
+ }
105
+
106
+ export const config = {
107
+ name: "dashboard report background sweep",
108
+ generator: "agent-native analytics build",
109
+ background: true,
110
+ nodeBundler: "none",
111
+ includedFiles: ["**"],
112
+ preferStatic: false,
113
+ };
114
+ `;
115
+
116
+ writeFileSync(path.join(dest, `${WORKER_NAME}.mjs`), source);
117
+ }
118
+
119
+ if (!existsSync(path.join(SERVER_DIR, "main.mjs"))) {
120
+ console.log(
121
+ "[dashboard-report-cron] Skipped Netlify cron emit: Nitro Netlify server output not found.",
122
+ );
123
+ process.exit(0);
124
+ }
125
+
126
+ const token = randomBytes(32).toString("hex");
127
+ emitScheduledTrigger(token);
128
+ emitBackgroundWorker(token);
129
+ console.log(
130
+ `[dashboard-report-cron] Emitted Netlify scheduled trigger "${SCHEDULED_NAME}" (${SCHEDULE}) and background worker "${WORKER_NAME}".`,
131
+ );