@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/docs/content/template-analytics.md +5 -0
- package/corpus/core/docs/content/tracking.md +45 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/agent/types.ts +22 -0
- package/corpus/core/src/client/AgentPanel.tsx +34 -18
- package/corpus/core/src/client/AssistantChat.tsx +40 -0
- package/corpus/core/src/client/agent-chat.ts +173 -0
- package/corpus/core/src/client/analytics.ts +141 -5
- package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
- package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
- package/corpus/core/src/client/composer/types.ts +22 -0
- package/corpus/core/src/client/index.ts +14 -0
- package/corpus/core/src/client/session-replay.ts +800 -0
- package/corpus/core/src/db/schema.ts +12 -0
- package/corpus/core/src/deploy/build.ts +75 -0
- package/corpus/core/src/private-blob/index.ts +18 -0
- package/corpus/core/src/private-blob/registry.ts +241 -0
- package/corpus/core/src/private-blob/types.ts +55 -0
- package/corpus/core/src/scripts/utils.ts +34 -6
- package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
- package/corpus/core/src/server/email.ts +5 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/styles/agent-native.css +24 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
- package/corpus/templates/analytics/AGENTS.md +32 -1
- package/corpus/templates/analytics/DEVELOPING.md +9 -1
- package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
- package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
- package/corpus/templates/analytics/actions/navigate.ts +15 -5
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
- package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
- package/corpus/templates/analytics/actions/view-screen.ts +50 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
- package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
- package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
- package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
- package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
- package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
- package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
- package/corpus/templates/analytics/package.json +5 -2
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
- package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
- package/corpus/templates/analytics/server/db/index.ts +12 -0
- package/corpus/templates/analytics/server/db/schema.ts +131 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
- package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
- package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
- package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
- package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
- package/corpus/templates/analytics/server/plugins/db.ts +200 -0
- package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
- package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
- package/corpus/templates/assets/AGENTS.md +18 -3
- package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
- package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
- package/corpus/templates/assets/actions/generate-asset.ts +30 -23
- package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
- package/corpus/templates/assets/actions/generate-image.ts +46 -30
- package/corpus/templates/assets/actions/generate-video.ts +18 -3
- package/corpus/templates/assets/actions/list-assets.ts +39 -8
- package/corpus/templates/assets/actions/list-libraries.ts +24 -2
- package/corpus/templates/assets/actions/navigate.ts +1 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
- package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
- package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
- package/corpus/templates/assets/actions/variant-slots.ts +131 -28
- package/corpus/templates/assets/actions/view-screen.ts +9 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
- package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
- package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
- package/corpus/templates/assets/app/i18n-data.ts +1815 -0
- package/corpus/templates/assets/app/lib/libraries.ts +6 -0
- package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
- package/corpus/templates/assets/app/routes/audit.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
- package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
- package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
- package/corpus/templates/assets/app/routes/library.tsx +1454 -175
- package/corpus/templates/assets/server/db/index.ts +1 -1
- package/corpus/templates/assets/server/lib/generation.ts +86 -15
- package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
- package/corpus/templates/assets/server/plugins/db.ts +5 -0
- package/corpus/templates/assets/shared/api.ts +6 -3
- package/dist/agent/types.d.ts +21 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +6 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +15 -3
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +32 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +113 -0
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/analytics.d.ts +22 -3
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +96 -4
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +2 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +228 -56
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
- package/dist/client/composer/extensions/MentionReference.js +3 -0
- package/dist/client/composer/extensions/MentionReference.js.map +1 -1
- package/dist/client/composer/types.d.ts +21 -0
- package/dist/client/composer/types.d.ts.map +1 -1
- package/dist/client/composer/types.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +41 -0
- package/dist/client/session-replay.d.ts.map +1 -0
- package/dist/client/session-replay.js +595 -0
- package/dist/client/session-replay.js.map +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/schema.d.ts +3 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +6 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +66 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/private-blob/index.d.ts +3 -0
- package/dist/private-blob/index.d.ts.map +1 -0
- package/dist/private-blob/index.js +2 -0
- package/dist/private-blob/index.js.map +1 -0
- package/dist/private-blob/registry.d.ts +10 -0
- package/dist/private-blob/registry.d.ts.map +1 -0
- package/dist/private-blob/registry.js +152 -0
- package/dist/private-blob/registry.js.map +1 -0
- package/dist/private-blob/types.d.ts +51 -0
- package/dist/private-blob/types.d.ts.map +1 -0
- package/dist/private-blob/types.js +2 -0
- package/dist/private-blob/types.js.map +1 -0
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/scripts/utils.d.ts +4 -4
- package/dist/scripts/utils.d.ts.map +1 -1
- package/dist/scripts/utils.js +30 -6
- package/dist/scripts/utils.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +5 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/email.d.ts +2 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +3 -0
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +24 -0
- package/docs/content/template-analytics.md +5 -0
- package/docs/content/tracking.md +45 -0
- package/package.json +3 -1
- 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 {
|
|
2
|
-
import OverviewPage from "@/pages/overview/OverviewPage";
|
|
1
|
+
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
return
|
|
3
|
+
function target(url: URL): string {
|
|
4
|
+
return `/ask${url.search}${url.hash}`;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
export
|
|
9
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
);
|