@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,405 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getAppProductionUrl,
|
|
5
|
+
sendEmail,
|
|
6
|
+
signEmbedSessionToken,
|
|
7
|
+
} from "@agent-native/core/server";
|
|
8
|
+
import {
|
|
9
|
+
EMBED_MODE_QUERY_PARAM,
|
|
10
|
+
EMBED_TOKEN_QUERY_PARAM,
|
|
11
|
+
} from "@agent-native/core/shared";
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
DashboardFilter,
|
|
15
|
+
FilterType,
|
|
16
|
+
SqlDashboardConfig,
|
|
17
|
+
} from "../../app/pages/adhoc/sql-dashboard/types";
|
|
18
|
+
import {
|
|
19
|
+
getReportDashboard,
|
|
20
|
+
type AccessCtx,
|
|
21
|
+
type DashboardReportSubscription,
|
|
22
|
+
} from "./dashboard-report-subscriptions";
|
|
23
|
+
|
|
24
|
+
type ReportSnapshot = {
|
|
25
|
+
dashboardId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
filters: Record<string, string>;
|
|
29
|
+
dashboardUrl: string;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
|
|
34
|
+
"date",
|
|
35
|
+
"date-range",
|
|
36
|
+
"toggle-date",
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_SERVERLESS_CHROMIUM_PACK_URL =
|
|
39
|
+
"https://github.com/Sparticuz/chromium/releases/download/v149.0.0/chromium-v149.0.0-pack.x64.tar";
|
|
40
|
+
const DASHBOARD_REPORT_SCREENSHOT_PARAM = "reportScreenshot";
|
|
41
|
+
const DASHBOARD_REPORT_CID = "dashboard-report-snapshot";
|
|
42
|
+
|
|
43
|
+
function daysAgo(n: number): string {
|
|
44
|
+
const d = new Date();
|
|
45
|
+
d.setDate(d.getDate() - n);
|
|
46
|
+
return d.toISOString().slice(0, 10);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveDefault(raw: string | undefined, type: FilterType): string {
|
|
50
|
+
if (!raw) return "";
|
|
51
|
+
if (DATE_FILTER_TYPES.has(type)) {
|
|
52
|
+
const m = /^(\d+)d$/.exec(raw);
|
|
53
|
+
if (m) return daysAgo(parseInt(m[1], 10));
|
|
54
|
+
if (raw === "today") return daysAgo(0);
|
|
55
|
+
}
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function defaultFilterValues(
|
|
60
|
+
config: SqlDashboardConfig,
|
|
61
|
+
): Record<string, string> {
|
|
62
|
+
const values: Record<string, string> = {};
|
|
63
|
+
const filters = Array.isArray(config.filters) ? config.filters : [];
|
|
64
|
+
for (const f of filters as DashboardFilter[]) {
|
|
65
|
+
if (f.type === "date-range") {
|
|
66
|
+
values[`f_${f.id}Start`] = resolveDefault(f.default, f.type);
|
|
67
|
+
values[`f_${f.id}End`] = daysAgo(0);
|
|
68
|
+
} else if (f.type !== "toggle" && f.type !== "toggle-date") {
|
|
69
|
+
values[`f_${f.id}`] = resolveDefault(f.default, f.type);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Object.entries(values).filter(([, value]) => Boolean(value)),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dashboardConfigFromRecord(raw: Record<string, unknown>) {
|
|
78
|
+
return {
|
|
79
|
+
name:
|
|
80
|
+
typeof raw.name === "string" && raw.name.trim()
|
|
81
|
+
? raw.name.trim()
|
|
82
|
+
: "Untitled Dashboard",
|
|
83
|
+
description:
|
|
84
|
+
typeof raw.description === "string" ? raw.description : undefined,
|
|
85
|
+
filters: Array.isArray(raw.filters)
|
|
86
|
+
? (raw.filters as DashboardFilter[])
|
|
87
|
+
: undefined,
|
|
88
|
+
variables:
|
|
89
|
+
raw.variables && typeof raw.variables === "object"
|
|
90
|
+
? (raw.variables as Record<string, string>)
|
|
91
|
+
: undefined,
|
|
92
|
+
columns: typeof raw.columns === "number" ? raw.columns : undefined,
|
|
93
|
+
panels: Array.isArray(raw.panels) ? (raw.panels as any[]) : [],
|
|
94
|
+
} satisfies SqlDashboardConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function dashboardBaseUrl(): string {
|
|
98
|
+
return (
|
|
99
|
+
process.env.DASHBOARD_REPORT_BASE_URL?.trim() ||
|
|
100
|
+
getAppProductionUrl().replace(/\/+$/, "")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildDashboardPath(
|
|
105
|
+
dashboardId: string,
|
|
106
|
+
filters: Record<string, string>,
|
|
107
|
+
options?: { reportScreenshot?: boolean },
|
|
108
|
+
): string {
|
|
109
|
+
const url = new URL(
|
|
110
|
+
`/dashboards/${encodeURIComponent(dashboardId)}`,
|
|
111
|
+
"https://agent-native.invalid/",
|
|
112
|
+
);
|
|
113
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
114
|
+
if (value) url.searchParams.set(key, value);
|
|
115
|
+
}
|
|
116
|
+
if (options?.reportScreenshot) {
|
|
117
|
+
url.searchParams.set(DASHBOARD_REPORT_SCREENSHOT_PARAM, "1");
|
|
118
|
+
}
|
|
119
|
+
return `${url.pathname}${url.search}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildDashboardUrl(
|
|
123
|
+
dashboardId: string,
|
|
124
|
+
filters: Record<string, string>,
|
|
125
|
+
options?: { reportScreenshot?: boolean },
|
|
126
|
+
): string {
|
|
127
|
+
const path = buildDashboardPath(dashboardId, filters, options);
|
|
128
|
+
const url = new URL(path, `${dashboardBaseUrl()}/`);
|
|
129
|
+
return url.toString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function escapeHtml(value: unknown): string {
|
|
133
|
+
return String(value ?? "")
|
|
134
|
+
.replace(/&/g, "&")
|
|
135
|
+
.replace(/</g, "<")
|
|
136
|
+
.replace(/>/g, ">")
|
|
137
|
+
.replace(/"/g, """)
|
|
138
|
+
.replace(/'/g, "'");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function collectReportSnapshot(
|
|
142
|
+
sub: DashboardReportSubscription,
|
|
143
|
+
): Promise<ReportSnapshot> {
|
|
144
|
+
const accessCtx: AccessCtx = {
|
|
145
|
+
email: sub.ownerEmail,
|
|
146
|
+
orgId: sub.orgId,
|
|
147
|
+
};
|
|
148
|
+
const dashboard = await getReportDashboard(sub.dashboardId, accessCtx);
|
|
149
|
+
if (!dashboard) {
|
|
150
|
+
throw Object.assign(new Error("Dashboard not found"), { statusCode: 404 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const config = dashboardConfigFromRecord(dashboard.config);
|
|
154
|
+
const filters = {
|
|
155
|
+
...defaultFilterValues(config),
|
|
156
|
+
...sub.filters,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
dashboardId: sub.dashboardId,
|
|
161
|
+
title: config.name || dashboard.title,
|
|
162
|
+
description: config.description,
|
|
163
|
+
filters,
|
|
164
|
+
dashboardUrl: buildDashboardUrl(sub.dashboardId, filters),
|
|
165
|
+
generatedAt: new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isServerlessBrowserRuntime(): boolean {
|
|
170
|
+
return (
|
|
171
|
+
process.env.NETLIFY === "true" ||
|
|
172
|
+
Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME) ||
|
|
173
|
+
Boolean(process.env.AWS_EXECUTION_ENV)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function localChromiumExecutablePath(): string | null {
|
|
178
|
+
const configured =
|
|
179
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
|
|
180
|
+
process.env.CHROME_BIN ||
|
|
181
|
+
process.env.CHROMIUM_PATH;
|
|
182
|
+
if (configured && existsSync(configured)) return configured;
|
|
183
|
+
|
|
184
|
+
const candidates = [
|
|
185
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
186
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
187
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
188
|
+
"/usr/bin/google-chrome",
|
|
189
|
+
"/usr/bin/google-chrome-stable",
|
|
190
|
+
"/usr/bin/chromium",
|
|
191
|
+
"/usr/bin/chromium-browser",
|
|
192
|
+
];
|
|
193
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function launchScreenshotBrowser() {
|
|
197
|
+
const { chromium: playwright } = await import("playwright-core");
|
|
198
|
+
const localExecutablePath = localChromiumExecutablePath();
|
|
199
|
+
if (localExecutablePath) {
|
|
200
|
+
return playwright.launch({
|
|
201
|
+
executablePath: localExecutablePath,
|
|
202
|
+
headless: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isServerlessBrowserRuntime()) {
|
|
207
|
+
const { default: chromium } = await import("@sparticuz/chromium-min");
|
|
208
|
+
const packUrl =
|
|
209
|
+
process.env.DASHBOARD_REPORT_CHROMIUM_PACK_URL?.trim() ||
|
|
210
|
+
DEFAULT_SERVERLESS_CHROMIUM_PACK_URL;
|
|
211
|
+
return playwright.launch({
|
|
212
|
+
args: chromium.args,
|
|
213
|
+
executablePath: await chromium.executablePath(packUrl),
|
|
214
|
+
headless: true,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return playwright.launch({ headless: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function waitForDashboardReportReady(page: any): Promise<void> {
|
|
222
|
+
await page.waitForFunction(
|
|
223
|
+
`(() => {
|
|
224
|
+
const root = document.querySelector("[data-dashboard-report-capture]");
|
|
225
|
+
if (!root) return false;
|
|
226
|
+
if (root.getAttribute("data-dashboard-report-ready") !== "true") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return !root.querySelector("[data-dashboard-report-loading='true']");
|
|
230
|
+
})()`,
|
|
231
|
+
undefined,
|
|
232
|
+
{ timeout: 90_000 },
|
|
233
|
+
);
|
|
234
|
+
await page.evaluate(`(async () => {
|
|
235
|
+
await document.fonts?.ready;
|
|
236
|
+
})()`);
|
|
237
|
+
await page.waitForTimeout(750);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function scrollDashboardForLazyRendering(page: any): Promise<void> {
|
|
241
|
+
await page.evaluate(`(async () => {
|
|
242
|
+
const wait = (ms) =>
|
|
243
|
+
new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
244
|
+
const maxY = Math.max(
|
|
245
|
+
document.body.scrollHeight,
|
|
246
|
+
document.documentElement.scrollHeight,
|
|
247
|
+
);
|
|
248
|
+
const step = Math.max(600, Math.floor(window.innerHeight * 0.75));
|
|
249
|
+
for (let y = 0; y < maxY; y += step) {
|
|
250
|
+
window.scrollTo(0, y);
|
|
251
|
+
await wait(120);
|
|
252
|
+
}
|
|
253
|
+
window.scrollTo(0, 0);
|
|
254
|
+
})()`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function captureDashboardPng(
|
|
258
|
+
sub: DashboardReportSubscription,
|
|
259
|
+
snapshot: ReportSnapshot,
|
|
260
|
+
): Promise<Buffer> {
|
|
261
|
+
const targetPath = buildDashboardPath(
|
|
262
|
+
snapshot.dashboardId,
|
|
263
|
+
snapshot.filters,
|
|
264
|
+
{
|
|
265
|
+
reportScreenshot: true,
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
const token = signEmbedSessionToken({
|
|
269
|
+
ownerEmail: sub.ownerEmail,
|
|
270
|
+
orgId: sub.orgId,
|
|
271
|
+
targetPath,
|
|
272
|
+
scope: `dashboard-report-screenshot:${sub.id}`,
|
|
273
|
+
ttlSeconds: 5 * 60,
|
|
274
|
+
});
|
|
275
|
+
const screenshotUrl = new URL(targetPath, `${dashboardBaseUrl()}/`);
|
|
276
|
+
screenshotUrl.searchParams.set(EMBED_MODE_QUERY_PARAM, "1");
|
|
277
|
+
screenshotUrl.searchParams.set(EMBED_TOKEN_QUERY_PARAM, token);
|
|
278
|
+
|
|
279
|
+
const browser = await launchScreenshotBrowser();
|
|
280
|
+
try {
|
|
281
|
+
const page = await browser.newPage({
|
|
282
|
+
viewport: { width: 1440, height: 1800 },
|
|
283
|
+
deviceScaleFactor: 1,
|
|
284
|
+
});
|
|
285
|
+
page.setDefaultTimeout(90_000);
|
|
286
|
+
await page.emulateMedia({ media: "screen" });
|
|
287
|
+
await page.goto(screenshotUrl.toString(), {
|
|
288
|
+
waitUntil: "domcontentloaded",
|
|
289
|
+
timeout: 90_000,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const capture = page.locator("[data-dashboard-report-capture]");
|
|
293
|
+
await capture.waitFor({ state: "visible", timeout: 90_000 });
|
|
294
|
+
await waitForDashboardReportReady(page);
|
|
295
|
+
await scrollDashboardForLazyRendering(page);
|
|
296
|
+
await waitForDashboardReportReady(page);
|
|
297
|
+
|
|
298
|
+
const box = await capture.boundingBox();
|
|
299
|
+
if (box) {
|
|
300
|
+
await page.setViewportSize({
|
|
301
|
+
width: Math.max(1200, Math.min(1800, Math.ceil(box.width + 64))),
|
|
302
|
+
height: Math.max(1000, Math.min(7000, Math.ceil(box.height + 64))),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
await capture.scrollIntoViewIfNeeded();
|
|
306
|
+
const image = await capture.screenshot({
|
|
307
|
+
type: "png",
|
|
308
|
+
animations: "disabled",
|
|
309
|
+
});
|
|
310
|
+
if (!image?.length) {
|
|
311
|
+
throw new Error("Dashboard screenshot was empty");
|
|
312
|
+
}
|
|
313
|
+
return Buffer.from(image);
|
|
314
|
+
} finally {
|
|
315
|
+
await browser.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function reportDate(snapshot: ReportSnapshot): string {
|
|
320
|
+
return new Date(snapshot.generatedAt).toLocaleDateString("en-US", {
|
|
321
|
+
year: "numeric",
|
|
322
|
+
month: "numeric",
|
|
323
|
+
day: "numeric",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderReportEmailHtml(snapshot: ReportSnapshot): string {
|
|
328
|
+
const title = escapeHtml(snapshot.title);
|
|
329
|
+
const dashboardUrl = escapeHtml(snapshot.dashboardUrl);
|
|
330
|
+
const date = escapeHtml(reportDate(snapshot));
|
|
331
|
+
const description = snapshot.description
|
|
332
|
+
? `<p style="margin:8px 0 18px;color:#525866;font-size:14px;line-height:1.5;">${escapeHtml(snapshot.description)}</p>`
|
|
333
|
+
: "";
|
|
334
|
+
|
|
335
|
+
return `<!doctype html>
|
|
336
|
+
<html>
|
|
337
|
+
<body style="margin:0;padding:24px;background:#ffffff;color:#171717;font-family:Inter,Arial,sans-serif;">
|
|
338
|
+
<h3 style="margin:0 0 8px;font-size:18px;line-height:1.35;font-weight:600;">
|
|
339
|
+
Here's the report of <a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">${title}</a> for ${date}.
|
|
340
|
+
</h3>
|
|
341
|
+
${description}
|
|
342
|
+
<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
|
|
343
|
+
<img src="cid:${DASHBOARD_REPORT_CID}" alt="${title}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:1px solid #e5e7eb;border-radius:8px;" />
|
|
344
|
+
</a>
|
|
345
|
+
<p style="margin:18px 0 0;color:#525866;font-size:13px;line-height:1.45;">
|
|
346
|
+
<a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">Open dashboard</a>
|
|
347
|
+
</p>
|
|
348
|
+
</body>
|
|
349
|
+
</html>`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderReportText(snapshot: ReportSnapshot): string {
|
|
353
|
+
return [
|
|
354
|
+
`Daily dashboard report: ${snapshot.title}`,
|
|
355
|
+
`Date: ${reportDate(snapshot)}`,
|
|
356
|
+
`Open dashboard: ${snapshot.dashboardUrl}`,
|
|
357
|
+
].join("\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function reportFilename(title: string): string {
|
|
361
|
+
const slug = title
|
|
362
|
+
.toLowerCase()
|
|
363
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
364
|
+
.replace(/^-|-$/g, "")
|
|
365
|
+
.slice(0, 64);
|
|
366
|
+
return `${slug || "dashboard"}-report.png`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function sendDashboardReportSubscription(
|
|
370
|
+
sub: DashboardReportSubscription,
|
|
371
|
+
): Promise<{
|
|
372
|
+
dashboardUrl: string;
|
|
373
|
+
recipientCount: number;
|
|
374
|
+
screenshotAttached: boolean;
|
|
375
|
+
}> {
|
|
376
|
+
const snapshot = await collectReportSnapshot(sub);
|
|
377
|
+
const png = await captureDashboardPng(sub, snapshot);
|
|
378
|
+
const html = renderReportEmailHtml(snapshot);
|
|
379
|
+
const text = renderReportText(snapshot);
|
|
380
|
+
const subject = `Daily dashboard: ${snapshot.title}`;
|
|
381
|
+
|
|
382
|
+
for (const to of sub.recipients) {
|
|
383
|
+
await sendEmail({
|
|
384
|
+
to,
|
|
385
|
+
subject,
|
|
386
|
+
html,
|
|
387
|
+
text,
|
|
388
|
+
attachments: [
|
|
389
|
+
{
|
|
390
|
+
filename: reportFilename(snapshot.title),
|
|
391
|
+
content: png,
|
|
392
|
+
contentType: "image/png",
|
|
393
|
+
contentId: DASHBOARD_REPORT_CID,
|
|
394
|
+
disposition: "inline",
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
dashboardUrl: snapshot.dashboardUrl,
|
|
402
|
+
recipientCount: sub.recipients.length,
|
|
403
|
+
screenshotAttached: true,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -25,6 +25,10 @@ export interface AnalyticsQueryResult {
|
|
|
25
25
|
|
|
26
26
|
const MAX_EVENTS_PER_REQUEST = 100;
|
|
27
27
|
const MAX_QUERY_ROWS = 5_000;
|
|
28
|
+
const FIRST_PARTY_QUERY_TABLES = new Set([
|
|
29
|
+
"analytics_events",
|
|
30
|
+
"session_recordings",
|
|
31
|
+
]);
|
|
28
32
|
const RESERVED_ALIAS_WORDS = new Set([
|
|
29
33
|
"where",
|
|
30
34
|
"on",
|
|
@@ -75,6 +79,9 @@ export async function createAnalyticsPublicKey(
|
|
|
75
79
|
name: name.trim() || "Default key",
|
|
76
80
|
publicKey,
|
|
77
81
|
publicKeyPrefix: publicKey.slice(0, 13),
|
|
82
|
+
replayAllowedOrigins: "[]",
|
|
83
|
+
replayMaxBytesPerDay: 100 * 1024 * 1024,
|
|
84
|
+
replayMaxRequestsPerMinute: 120,
|
|
78
85
|
createdAt,
|
|
79
86
|
ownerEmail: scope.userEmail,
|
|
80
87
|
orgId: scope.orgId,
|
|
@@ -85,6 +92,9 @@ export async function createAnalyticsPublicKey(
|
|
|
85
92
|
name: row.name,
|
|
86
93
|
publicKey,
|
|
87
94
|
publicKeyPrefix: row.publicKeyPrefix,
|
|
95
|
+
replayAllowedOrigins: [],
|
|
96
|
+
replayMaxBytesPerDay: row.replayMaxBytesPerDay,
|
|
97
|
+
replayMaxRequestsPerMinute: row.replayMaxRequestsPerMinute,
|
|
88
98
|
createdAt,
|
|
89
99
|
orgId: row.orgId,
|
|
90
100
|
revokedAt: null,
|
|
@@ -118,6 +128,9 @@ export async function listAnalyticsPublicKeys(
|
|
|
118
128
|
id: row.id,
|
|
119
129
|
name: row.name,
|
|
120
130
|
publicKeyPrefix: row.publicKeyPrefix,
|
|
131
|
+
replayAllowedOrigins: parseReplayAllowedOrigins(row.replayAllowedOrigins),
|
|
132
|
+
replayMaxBytesPerDay: row.replayMaxBytesPerDay ?? 100 * 1024 * 1024,
|
|
133
|
+
replayMaxRequestsPerMinute: row.replayMaxRequestsPerMinute ?? 120,
|
|
121
134
|
createdAt: row.createdAt,
|
|
122
135
|
lastUsedAt: row.lastUsedAt ?? null,
|
|
123
136
|
revokedAt: row.revokedAt ?? null,
|
|
@@ -125,6 +138,25 @@ export async function listAnalyticsPublicKeys(
|
|
|
125
138
|
}));
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
function parseReplayAllowedOrigins(value: unknown): string[] {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
144
|
+
}
|
|
145
|
+
if (typeof value !== "string" || !value.trim()) return [];
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(value);
|
|
148
|
+
if (Array.isArray(parsed)) {
|
|
149
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
return value
|
|
153
|
+
.split(/[\n,]/)
|
|
154
|
+
.map((item) => item.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
export async function revokeAnalyticsPublicKey(
|
|
129
161
|
scope: AnalyticsScope,
|
|
130
162
|
keyId: string,
|
|
@@ -435,6 +467,11 @@ export function validateFirstPartyAnalyticsSql(sql: string): void {
|
|
|
435
467
|
if (/\$\d+\b/.test(stripped)) {
|
|
436
468
|
throw new Error("Bind placeholders are not supported in dashboard SQL");
|
|
437
469
|
}
|
|
470
|
+
if (/\bsession_replay_chunks\b/i.test(stripped)) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
"First-party analytics queries cannot read session replay chunks",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
438
475
|
|
|
439
476
|
const cteNames = new Set<string>();
|
|
440
477
|
const cteRe = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s+as\s*\(/gi;
|
|
@@ -442,22 +479,24 @@ export function validateFirstPartyAnalyticsSql(sql: string): void {
|
|
|
442
479
|
cteNames.add(match[1].toLowerCase());
|
|
443
480
|
}
|
|
444
481
|
|
|
445
|
-
let
|
|
482
|
+
let usesAllowedTable = false;
|
|
446
483
|
const tableRe =
|
|
447
484
|
/\b(?:from|join)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi;
|
|
448
485
|
for (const match of stripped.matchAll(tableRe)) {
|
|
449
486
|
const ref = match[1].toLowerCase();
|
|
450
|
-
if (ref
|
|
451
|
-
|
|
487
|
+
if (FIRST_PARTY_QUERY_TABLES.has(ref)) {
|
|
488
|
+
usesAllowedTable = true;
|
|
452
489
|
continue;
|
|
453
490
|
}
|
|
454
491
|
if (cteNames.has(ref)) continue;
|
|
455
492
|
throw new Error(
|
|
456
|
-
`First-party analytics queries can only read analytics_events (found ${match[1]})`,
|
|
493
|
+
`First-party analytics queries can only read analytics_events or session_recordings (found ${match[1]})`,
|
|
457
494
|
);
|
|
458
495
|
}
|
|
459
|
-
if (!
|
|
460
|
-
throw new Error(
|
|
496
|
+
if (!usesAllowedTable) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"Query must read from analytics_events or session_recordings",
|
|
499
|
+
);
|
|
461
500
|
}
|
|
462
501
|
}
|
|
463
502
|
|
|
@@ -483,18 +522,24 @@ function scopedAnalyticsSql(
|
|
|
483
522
|
): { sql: string; args: Array<string | null> } {
|
|
484
523
|
const args: Array<string | null> = [];
|
|
485
524
|
const aliasRe =
|
|
486
|
-
/\b(from|join)\s+analytics_events\b(\s+(?:as\s+)?(?!where\b|on\b|group\b|order\b|limit\b|join\b|left\b|right\b|inner\b|outer\b|cross\b|full\b|having\b|union\b)([a-zA-Z_][a-zA-Z0-9_]*))?/gi;
|
|
487
|
-
const rewritten = sql.replace(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
?
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
525
|
+
/\b(from|join)\s+(analytics_events|session_recordings)\b(\s+(?:as\s+)?(?!where\b|on\b|group\b|order\b|limit\b|join\b|left\b|right\b|inner\b|outer\b|cross\b|full\b|having\b|union\b)([a-zA-Z_][a-zA-Z0-9_]*))?/gi;
|
|
526
|
+
const rewritten = sql.replace(
|
|
527
|
+
aliasRe,
|
|
528
|
+
(full, keyword, tableName, aliasPart, alias) => {
|
|
529
|
+
const normalizedTable = String(tableName).toLowerCase();
|
|
530
|
+
const normalizedAlias =
|
|
531
|
+
typeof alias === "string" ? alias.toLowerCase() : "";
|
|
532
|
+
const usableAlias =
|
|
533
|
+
aliasPart &&
|
|
534
|
+
normalizedAlias &&
|
|
535
|
+
!RESERVED_ALIAS_WORDS.has(normalizedAlias)
|
|
536
|
+
? aliasPart
|
|
537
|
+
: ` AS ${normalizedTable}`;
|
|
538
|
+
const scopeDef = scopeClause(scope);
|
|
539
|
+
args.push(...scopeDef.args);
|
|
540
|
+
return `${keyword} (SELECT * FROM ${normalizedTable} WHERE ${scopeDef.sql})${usableAlias}`;
|
|
541
|
+
},
|
|
542
|
+
);
|
|
498
543
|
return { sql: rewritten, args };
|
|
499
544
|
}
|
|
500
545
|
|