@agent-native/core 0.122.0 → 0.122.2
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/README.md +5 -25
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +57 -4
- package/corpus/core/src/agent/production-agent.ts +65 -1
- package/corpus/core/src/agent/run-store.ts +61 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +9 -9
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/corpus/core/src/extensions/url-safety.ts +67 -4
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/core/src/templates/default/app/root.tsx +1 -1
- package/corpus/core/src/templates/default/public/manifest.json +2 -2
- package/corpus/core/src/vite/client.ts +64 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +15 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -3
- package/corpus/templates/analytics/app/routes/_index.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-25-analytics-dashboards-and-scheduled-reports-are-faster-and-mo.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/analytics/package.json +1 -1
- package/corpus/templates/analytics/public/manifest.json +1 -1
- package/corpus/templates/analytics/server/db/schema.ts +12 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +8 -4
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +136 -5
- package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +250 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +167 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +34 -9
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +105 -23
- package/corpus/templates/analytics/server/lib/first-party-unbounded-panel-repair.ts +169 -0
- package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
- package/corpus/templates/analytics/server/plugins/db.ts +41 -1
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +6 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/assets/app/routes/_index.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/assets/server/plugins/auth.ts +1 -1
- package/corpus/templates/brain/app/lib/brain.ts +0 -7
- package/corpus/templates/brain/app/routes/_index.tsx +1 -2
- package/corpus/templates/brain/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/brain/package.json +1 -1
- package/corpus/templates/brain/public/manifest.json +1 -1
- package/corpus/templates/brain/server/plugins/auth.ts +1 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -6
- package/corpus/templates/calendar/app/routes/_app._index.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/calendar/package.json +1 -1
- package/corpus/templates/calendar/public/manifest.json +1 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +1 -1
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/corpus/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +53 -98
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +260 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +0 -7
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_index.tsx +1 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +6 -0
- package/corpus/templates/clips/app/routes/record.tsx +5 -0
- package/corpus/templates/clips/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/clips/server/db/schema.ts +2 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +2 -1
- package/corpus/templates/clips/server/plugins/auth.ts +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +8 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +56 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -39
- package/corpus/templates/content/app/routes/_app._index.tsx +1 -2
- package/corpus/templates/content/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/public/manifest.json +1 -1
- package/corpus/templates/content/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/actions/edit-design.ts +66 -27
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/design/app/routes/_index.tsx +1 -1
- package/corpus/templates/design/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-design-edits-retry-concurrent-changes.md +6 -0
- package/corpus/templates/design/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/design/server/plugins/auth.ts +1 -1
- package/corpus/templates/design/server/source-workspace.ts +5 -1
- package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -10
- package/corpus/templates/dispatch/app/routes/overview.tsx +1 -1
- package/corpus/templates/dispatch/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/dispatch/package.json +1 -1
- package/corpus/templates/dispatch/public/manifest.json +1 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +6 -4
- package/corpus/templates/forms/actions/create-form.ts +5 -1
- package/corpus/templates/forms/actions/export-responses.ts +22 -11
- package/corpus/templates/forms/actions/update-form.ts +5 -1
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +0 -35
- package/corpus/templates/forms/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-fields-can-be-created-without-ids.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-response-exports-use-file-storage.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/forms/package.json +1 -1
- package/corpus/templates/forms/public/manifest.json +1 -1
- package/corpus/templates/forms/server/lib/validate-fields.ts +43 -0
- package/corpus/templates/forms/server/plugins/auth.ts +1 -1
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -2
- package/corpus/templates/macros/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/macros/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/macros/package.json +1 -1
- package/corpus/templates/macros/server/plugins/auth.ts +2 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +0 -39
- package/corpus/templates/mail/app/routes/$view.$threadId.tsx +1 -1
- package/corpus/templates/mail/app/routes/$view.tsx +1 -1
- package/corpus/templates/mail/app/routes/_index.tsx +1 -1
- package/corpus/templates/mail/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/mail/package.json +1 -1
- package/corpus/templates/mail/public/manifest.json +1 -1
- package/corpus/templates/mail/server/plugins/auth.ts +1 -1
- package/corpus/templates/plan/agent-native.app-skill.json +1 -1
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +4 -9
- package/corpus/templates/plan/app/lib/app-config.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/plan/package.json +1 -1
- package/corpus/templates/plan/public/manifest.json +1 -1
- package/corpus/templates/plan/server/plugins/auth.ts +1 -1
- package/corpus/templates/slides/actions/extract-pdf.ts +4 -4
- package/corpus/templates/slides/actions/import-file.ts +3 -4
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/slides/app/routes/_index.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-07-25-app-branding-now-uses-the-product-name-without-the-agent-nat.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-gemini-image-models-updated.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-pdf-imports-tolerate-missing-canvas.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/corpus/templates/slides/package.json +1 -1
- package/corpus/templates/slides/public/manifest.json +1 -1
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +5 -2
- package/corpus/templates/slides/server/lib/pdf-parse-setup.ts +150 -0
- package/corpus/templates/slides/server/plugins/auth.ts +1 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +58 -4
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +63 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +49 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +9 -9
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +7 -4
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +9 -16
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +48 -4
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/dist/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/dist/templates/default/app/root.tsx +1 -1
- package/dist/templates/default/public/manifest.json +2 -2
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +64 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
- package/src/a2a/client.ts +57 -4
- package/src/agent/production-agent.ts +65 -1
- package/src/agent/run-store.ts +61 -1
- package/src/client/settings/SettingsPanel.tsx +9 -9
- package/src/client/settings/SettingsTabsPage.tsx +12 -2
- package/src/extensions/url-safety.ts +67 -4
- package/src/server/builder-design-systems.ts +1 -1
- package/src/templates/chat/app/components/layout/Sidebar.tsx +0 -7
- package/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +6 -0
- package/src/templates/default/app/root.tsx +1 -1
- package/src/templates/default/public/manifest.json +2 -2
- package/src/vite/client.ts +64 -3
|
@@ -2414,6 +2414,10 @@ type NitroModuleGraph = {
|
|
|
2414
2414
|
|
|
2415
2415
|
const NITRO_STARTUP_SETTLE_MS = 1_000;
|
|
2416
2416
|
const NITRO_STARTUP_TIMEOUT_MS = 30_000;
|
|
2417
|
+
const NITRO_STARTUP_RETRY_KEY = "__agent_native_nitro_startup_retry";
|
|
2418
|
+
const NITRO_STARTUP_RETRY_MAX = 5;
|
|
2419
|
+
const NITRO_STARTUP_RETRY_DELAY_MS = 1_000;
|
|
2420
|
+
const NITRO_STARTUP_RETRY_RESET_MS = 15_000;
|
|
2417
2421
|
|
|
2418
2422
|
function nitroModuleGraphSignature(environment: unknown): string | null {
|
|
2419
2423
|
const graph = (environment as { moduleGraph?: NitroModuleGraph } | undefined)
|
|
@@ -2456,9 +2460,66 @@ function sendNitroStartingResponse(
|
|
|
2456
2460
|
res.end();
|
|
2457
2461
|
return;
|
|
2458
2462
|
}
|
|
2459
|
-
res.end(
|
|
2460
|
-
|
|
2461
|
-
|
|
2463
|
+
res.end(`<!doctype html>
|
|
2464
|
+
<html>
|
|
2465
|
+
<head>
|
|
2466
|
+
<meta charset="utf-8">
|
|
2467
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2468
|
+
<title>Dev server restarting…</title>
|
|
2469
|
+
<style>
|
|
2470
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; font: 16px/1.5 system-ui, sans-serif; color: #171717; background: #fafafa; }
|
|
2471
|
+
main { width: min(560px, calc(100vw - 48px)); }
|
|
2472
|
+
h1 { margin: 0 0 8px; font-size: 1.25rem; }
|
|
2473
|
+
p { margin: 0; color: #737373; }
|
|
2474
|
+
</style>
|
|
2475
|
+
</head>
|
|
2476
|
+
<body>
|
|
2477
|
+
<main>
|
|
2478
|
+
<h1>Dev server is restarting…</h1>
|
|
2479
|
+
<p id="agent-native-nitro-retry-status">Checking again shortly.</p>
|
|
2480
|
+
</main>
|
|
2481
|
+
<script>
|
|
2482
|
+
(() => {
|
|
2483
|
+
const key = ${JSON.stringify(NITRO_STARTUP_RETRY_KEY)};
|
|
2484
|
+
const maxRetries = ${NITRO_STARTUP_RETRY_MAX};
|
|
2485
|
+
const resetAfterMs = ${NITRO_STARTUP_RETRY_RESET_MS};
|
|
2486
|
+
const retryDelayMs = ${NITRO_STARTUP_RETRY_DELAY_MS};
|
|
2487
|
+
const status = document.getElementById("agent-native-nitro-retry-status");
|
|
2488
|
+
const now = Date.now();
|
|
2489
|
+
let count = 0;
|
|
2490
|
+
let lastAttemptAt = 0;
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
const stored = JSON.parse(sessionStorage.getItem(key) || "null");
|
|
2494
|
+
if (stored && typeof stored === "object") {
|
|
2495
|
+
count = Number.isFinite(stored.count) ? stored.count : 0;
|
|
2496
|
+
lastAttemptAt = Number.isFinite(stored.at) ? stored.at : 0;
|
|
2497
|
+
}
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
// A blocked session store is handled below by showing a manual retry.
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (now - lastAttemptAt > resetAfterMs) count = 0;
|
|
2503
|
+
if (count >= maxRetries) {
|
|
2504
|
+
if (status) status.textContent = "The server is still unavailable. Refresh when it is ready.";
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
const nextState = JSON.stringify({ count: count + 1, at: now });
|
|
2509
|
+
try {
|
|
2510
|
+
sessionStorage.setItem(key, nextState);
|
|
2511
|
+
if (sessionStorage.getItem(key) !== nextState) throw new Error("unavailable");
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (status) status.textContent = "Refresh manually when the server is ready.";
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
if (status) status.textContent = "Retrying in one second…";
|
|
2518
|
+
setTimeout(() => window.location.reload(), retryDelayMs);
|
|
2519
|
+
})();
|
|
2520
|
+
</script>
|
|
2521
|
+
</body>
|
|
2522
|
+
</html>`);
|
|
2462
2523
|
}
|
|
2463
2524
|
|
|
2464
2525
|
function nitroStartupGate(
|
|
@@ -84,6 +84,21 @@ Do not rely on undeclared time variables. Server validation rejects unbound
|
|
|
84
84
|
first-party SQL, so declare the filter or choose an explicit non-dashboard
|
|
85
85
|
scope before saving.
|
|
86
86
|
|
|
87
|
+
**A bound anywhere in the SQL is not the same as every CTE having its own
|
|
88
|
+
bound.** If a panel has multiple top-level CTEs (`WITH a AS (...), b AS
|
|
89
|
+
(...)`) and more than one of them reads `analytics_events`, EVERY one of
|
|
90
|
+
those CTEs needs its own `{{timeRange}}`/`{{<id>Start}}`/`{{<id>End}}`
|
|
91
|
+
reference or literal date bound — not just the final `SELECT` or one sibling
|
|
92
|
+
CTE. A CTE that computes something like "this user's first-ever active day"
|
|
93
|
+
by scanning `analytics_events` with no bound at all will full-table-scan on
|
|
94
|
+
every render even though the panel *looks* time-bound overall (root cause of
|
|
95
|
+
a 2026-07-25 production incident: several dashboards had exactly this shape).
|
|
96
|
+
Server validation checks each top-level CTE independently now, so this fails
|
|
97
|
+
at save time — but write it right the first time: bound every CTE, or use
|
|
98
|
+
`config.timeScope: "cohort-history"` only for a CTE that is genuinely
|
|
99
|
+
defining a cohort (e.g. a first-seen date), never as a way to skip bounding
|
|
100
|
+
an ordinary activity scan.
|
|
101
|
+
|
|
87
102
|
## Creating A Dashboard
|
|
88
103
|
|
|
89
104
|
When the user asks for a dashboard:
|
|
@@ -19,14 +19,12 @@ import {
|
|
|
19
19
|
IconLock,
|
|
20
20
|
IconLink,
|
|
21
21
|
IconMessageCircle,
|
|
22
|
-
IconHierarchy2,
|
|
23
22
|
IconUsersGroup,
|
|
24
23
|
IconEye,
|
|
25
24
|
IconEyeOff,
|
|
26
25
|
IconPlayerPlay,
|
|
27
26
|
IconLayoutSidebarLeftCollapse,
|
|
28
27
|
IconLayoutSidebarLeftExpand,
|
|
29
|
-
IconArrowUpRight,
|
|
30
28
|
} from "@tabler/icons-react";
|
|
31
29
|
import {
|
|
32
30
|
useQuery,
|
|
@@ -183,12 +181,6 @@ import {
|
|
|
183
181
|
import { CSS } from "@dnd-kit/utilities";
|
|
184
182
|
|
|
185
183
|
const bottomItems = [
|
|
186
|
-
{
|
|
187
|
-
icon: IconHierarchy2,
|
|
188
|
-
labelKey: "settings.agentTitle",
|
|
189
|
-
href: "/agent",
|
|
190
|
-
showExternalLinkIcon: true,
|
|
191
|
-
},
|
|
192
184
|
{ icon: IconSettings, labelKey: "navigation.settings", href: "/settings" },
|
|
193
185
|
];
|
|
194
186
|
|
|
@@ -2098,12 +2090,6 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
2098
2090
|
href: "/data-dictionary",
|
|
2099
2091
|
active: location.pathname.startsWith("/data-dictionary"),
|
|
2100
2092
|
},
|
|
2101
|
-
{
|
|
2102
|
-
icon: IconHierarchy2,
|
|
2103
|
-
label: t("settings.agentTitle"),
|
|
2104
|
-
href: "/agent",
|
|
2105
|
-
active: location.pathname === "/agent",
|
|
2106
|
-
},
|
|
2107
2093
|
{
|
|
2108
2094
|
icon: IconSettings,
|
|
2109
2095
|
label: t("navigation.settings"),
|
|
@@ -2572,15 +2558,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
2572
2558
|
)}
|
|
2573
2559
|
>
|
|
2574
2560
|
<Icon className="h-4 w-4" />
|
|
2575
|
-
<span className="
|
|
2576
|
-
<span className="truncate">{t(item.labelKey)}</span>
|
|
2577
|
-
{item.showExternalLinkIcon && (
|
|
2578
|
-
<IconArrowUpRight
|
|
2579
|
-
aria-hidden="true"
|
|
2580
|
-
className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70"
|
|
2581
|
-
/>
|
|
2582
|
-
)}
|
|
2583
|
-
</span>
|
|
2561
|
+
<span className="truncate">{t(item.labelKey)}</span>
|
|
2584
2562
|
</Link>
|
|
2585
2563
|
);
|
|
2586
2564
|
})}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { MAX_CONCURRENT_SQL_QUERIES } from "@shared/sql-query-limits";
|
|
2
|
-
|
|
3
1
|
import type { SqlPanel } from "./types";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
// Keep each report window within the browser-side SQL query concurrency limit
|
|
4
|
+
// so a cold window does not serialize into multiple waves before readiness.
|
|
5
|
+
export const REPORT_PANEL_CHUNK_SIZE = 4;
|
|
6
6
|
|
|
7
7
|
export function listReportablePanelIds(panels: SqlPanel[]): string[] {
|
|
8
8
|
return panels
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { redirect, type LoaderFunctionArgs } from "react-router";
|
|
2
2
|
|
|
3
3
|
const SEO_TITLE =
|
|
4
|
-
"
|
|
4
|
+
"Analytics - Open Source Alternative to Amplitude & FullStory";
|
|
5
5
|
const SEO_DESCRIPTION =
|
|
6
6
|
"Open Source analytics app and alternative to Amplitude and FullStory where AI agents connect to warehouses, product analytics, and CRM data to answer questions and build dashboards.";
|
|
7
7
|
|
|
@@ -193,6 +193,18 @@ export const bigqueryCache = table("bigquery_cache", {
|
|
|
193
193
|
expiresAt: text("expires_at").notNull(),
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* First-party dashboard panel result cache — see
|
|
198
|
+
* server/lib/first-party-analytics-cache.ts.
|
|
199
|
+
*/
|
|
200
|
+
export const firstPartyAnalyticsCache = table("first_party_analytics_cache", {
|
|
201
|
+
key: text("key").primaryKey(),
|
|
202
|
+
sql: text("sql").notNull(),
|
|
203
|
+
result: text("result").notNull(),
|
|
204
|
+
createdAt: text("created_at").notNull(),
|
|
205
|
+
expiresAt: text("expires_at").notNull(),
|
|
206
|
+
});
|
|
207
|
+
|
|
196
208
|
/**
|
|
197
209
|
* Public write keys for the first-party analytics ingestion endpoint.
|
|
198
210
|
* The key is intentionally public/write-only: it can create events for the
|
|
@@ -446,10 +446,14 @@ export async function runDashboardPanelQuery(args: {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
if (source === "first-party") {
|
|
449
|
-
return await queryFirstPartyAnalytics(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
449
|
+
return await queryFirstPartyAnalytics(
|
|
450
|
+
query,
|
|
451
|
+
{
|
|
452
|
+
userEmail: ctx.userEmail,
|
|
453
|
+
orgId: ctx.orgId ?? null,
|
|
454
|
+
},
|
|
455
|
+
{ cache: true },
|
|
456
|
+
);
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
if (source === "demo") {
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
EMBED_TOKEN_QUERY_PARAM,
|
|
16
16
|
} from "@agent-native/core/shared";
|
|
17
17
|
|
|
18
|
+
import { interpolate } from "../../app/pages/adhoc/sql-dashboard/interpolate";
|
|
19
|
+
import { serializePanelSql } from "../../app/pages/adhoc/sql-dashboard/panel-sql";
|
|
18
20
|
import {
|
|
19
21
|
listReportablePanelIds,
|
|
20
22
|
REPORT_PANEL_CHUNK_SIZE,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
type DashboardReportCaptureOutcome,
|
|
33
35
|
type DashboardReportSubscription,
|
|
34
36
|
} from "./dashboard-report-subscriptions";
|
|
37
|
+
import { queryFirstPartyAnalytics } from "./first-party-analytics.js";
|
|
35
38
|
|
|
36
39
|
type ReportSnapshot = {
|
|
37
40
|
dashboardId: string;
|
|
@@ -42,6 +45,8 @@ type ReportSnapshot = {
|
|
|
42
45
|
reportSettingsUrl: string;
|
|
43
46
|
generatedAt: string;
|
|
44
47
|
panelIds: string[];
|
|
48
|
+
panels: SqlDashboardConfig["panels"];
|
|
49
|
+
variables?: Record<string, string>;
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
|
|
@@ -58,7 +63,9 @@ const LOCAL_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
|
58
63
|
const SERVERLESS_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
59
64
|
const SERVERLESS_SECOND_READY_TIMEOUT_MS = 45_000;
|
|
60
65
|
// Cap browser work separately from the sweep-level delivery deadline.
|
|
61
|
-
|
|
66
|
+
// Reserve time under Netlify's 300s background-function limit for cleanup and
|
|
67
|
+
// email delivery (see MAX_CAPTURE_CLEANUP_RESERVE_MS / delivery reserves below).
|
|
68
|
+
const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 210_000;
|
|
62
69
|
const BROWSER_CLEANUP_TIMEOUT_MS = 10_000;
|
|
63
70
|
const DASHBOARD_REPORT_EMAIL_TIMEOUT_MS = 10_000;
|
|
64
71
|
const DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS = 5_000;
|
|
@@ -226,6 +233,8 @@ async function collectReportSnapshot(
|
|
|
226
233
|
}),
|
|
227
234
|
generatedAt: new Date().toISOString(),
|
|
228
235
|
panelIds: listReportablePanelIds(config.panels),
|
|
236
|
+
panels: config.panels,
|
|
237
|
+
variables: config.variables,
|
|
229
238
|
};
|
|
230
239
|
}
|
|
231
240
|
|
|
@@ -375,6 +384,14 @@ function boundedStageTimeout(capMs: number, deadlineAt?: number): number {
|
|
|
375
384
|
return Math.max(1, Math.min(capMs, remainingMs));
|
|
376
385
|
}
|
|
377
386
|
|
|
387
|
+
function reportDeliveryReserveMs(recipientCount: number): number {
|
|
388
|
+
return (
|
|
389
|
+
MAX_CAPTURE_CLEANUP_RESERVE_MS +
|
|
390
|
+
recipientCount * DASHBOARD_REPORT_EMAIL_TIMEOUT_MS +
|
|
391
|
+
DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
378
395
|
async function runWithinCaptureDeadline<T>(
|
|
379
396
|
operation: () => Promise<T>,
|
|
380
397
|
deadlineAt?: number,
|
|
@@ -448,7 +465,15 @@ async function waitForDashboardReportReady(
|
|
|
448
465
|
if (root.getAttribute("data-dashboard-report-ready") !== "true") {
|
|
449
466
|
return false;
|
|
450
467
|
}
|
|
451
|
-
|
|
468
|
+
if (root.querySelector("[data-dashboard-report-loading='true']")) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
// A panel that errored (including its own query-call timeout) clears
|
|
472
|
+
// its loading marker just like a successful panel, so without this
|
|
473
|
+
// check the capture would treat a broken panel as "ready" and bake
|
|
474
|
+
// its raw error text into the delivered screenshot instead of
|
|
475
|
+
// failing the attempt so the retry sweep can pick it up.
|
|
476
|
+
return !root.querySelector("[role='alert']");
|
|
452
477
|
})()`,
|
|
453
478
|
undefined,
|
|
454
479
|
{ timeout },
|
|
@@ -505,6 +530,101 @@ async function waitForDashboardReportReady(
|
|
|
505
530
|
}
|
|
506
531
|
}
|
|
507
532
|
|
|
533
|
+
const PREWARM_CONCURRENCY = 3;
|
|
534
|
+
const PREWARM_PANEL_TIMEOUT_MS = 20_000;
|
|
535
|
+
const PREWARM_MAX_BUDGET_MS = 45_000;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Runs first-party panel queries once, directly, before the browser capture
|
|
539
|
+
* starts, so the capture's own query-dashboard-panel calls hit a warm cache
|
|
540
|
+
* instead of several panels racing each other cold for database time inside
|
|
541
|
+
* a single capture window — that contention, not any one query alone, is
|
|
542
|
+
* what was pushing panels past their capture timeout. Best-effort: a panel
|
|
543
|
+
* that fails or times out here is simply computed cold by the capture later,
|
|
544
|
+
* exactly as it always was, so this can never make a report worse than
|
|
545
|
+
* before it existed. Bounded to time left after reserving the full chunked
|
|
546
|
+
* capture attempt and delivery tail, so prewarming cannot starve capture.
|
|
547
|
+
* The per-panel timeout only bounds how long a worker waits before logging;
|
|
548
|
+
* it is also passed to the database/cache layers, and the original query is
|
|
549
|
+
* still awaited before this function returns so it cannot overlap capture.
|
|
550
|
+
*/
|
|
551
|
+
async function prewarmFirstPartyPanelCache(
|
|
552
|
+
sub: DashboardReportSubscription,
|
|
553
|
+
snapshot: ReportSnapshot,
|
|
554
|
+
deadlineAt?: number,
|
|
555
|
+
recipientCount = 0,
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
const panels = (snapshot.panels ?? []).filter(
|
|
558
|
+
(panel) => panel.source === "first-party" && panel.sql != null,
|
|
559
|
+
);
|
|
560
|
+
if (!panels.length) return;
|
|
561
|
+
|
|
562
|
+
const remaining = deadlineAt ? deadlineAt - Date.now() : Infinity;
|
|
563
|
+
const budgetMs = Math.max(
|
|
564
|
+
0,
|
|
565
|
+
Math.min(
|
|
566
|
+
PREWARM_MAX_BUDGET_MS,
|
|
567
|
+
deadlineAt
|
|
568
|
+
? remaining -
|
|
569
|
+
SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS -
|
|
570
|
+
reportDeliveryReserveMs(recipientCount)
|
|
571
|
+
: PREWARM_MAX_BUDGET_MS,
|
|
572
|
+
),
|
|
573
|
+
);
|
|
574
|
+
if (budgetMs <= 0) return;
|
|
575
|
+
const prewarmDeadline = Date.now() + budgetMs;
|
|
576
|
+
|
|
577
|
+
const vars: Record<string, string> = { ...snapshot.variables };
|
|
578
|
+
for (const [key, value] of Object.entries(snapshot.filters)) {
|
|
579
|
+
if (key.startsWith("f_")) vars[key.slice(2)] = value;
|
|
580
|
+
}
|
|
581
|
+
const scope = { userEmail: sub.ownerEmail, orgId: sub.orgId ?? null };
|
|
582
|
+
|
|
583
|
+
let index = 0;
|
|
584
|
+
async function worker(): Promise<void> {
|
|
585
|
+
while (index < panels.length && Date.now() < prewarmDeadline) {
|
|
586
|
+
const panel = panels[index++];
|
|
587
|
+
const sql = interpolate(serializePanelSql(panel.sql), vars, {
|
|
588
|
+
failClosedTimeVariables: true,
|
|
589
|
+
});
|
|
590
|
+
if (!sql.trim()) continue;
|
|
591
|
+
const panelTimeoutMs = Math.max(
|
|
592
|
+
1,
|
|
593
|
+
Math.min(PREWARM_PANEL_TIMEOUT_MS, prewarmDeadline - Date.now()),
|
|
594
|
+
);
|
|
595
|
+
const query = queryFirstPartyAnalytics(sql, scope, {
|
|
596
|
+
cache: true,
|
|
597
|
+
timeoutMs: panelTimeoutMs,
|
|
598
|
+
});
|
|
599
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
600
|
+
try {
|
|
601
|
+
await Promise.race([
|
|
602
|
+
query,
|
|
603
|
+
new Promise<never>((_, reject) => {
|
|
604
|
+
timeout = setTimeout(
|
|
605
|
+
() => reject(new Error("prewarm panel timeout")),
|
|
606
|
+
panelTimeoutMs,
|
|
607
|
+
);
|
|
608
|
+
timeout.unref?.();
|
|
609
|
+
}),
|
|
610
|
+
]);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`[dashboard-report] prewarm failed for panel ${panel.id}:`,
|
|
614
|
+
err instanceof Error ? err.message : err,
|
|
615
|
+
);
|
|
616
|
+
} finally {
|
|
617
|
+
if (timeout) clearTimeout(timeout);
|
|
618
|
+
await query.catch(() => undefined);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await Promise.all(
|
|
624
|
+
Array.from({ length: PREWARM_CONCURRENCY }, () => worker()),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
508
628
|
async function assertDashboardReportPanelWindow(
|
|
509
629
|
page: any,
|
|
510
630
|
expectedPanelIds: string[],
|
|
@@ -1358,14 +1478,25 @@ export async function sendDashboardReportSubscription(
|
|
|
1358
1478
|
() => collectReportSnapshot(sub),
|
|
1359
1479
|
options.deadlineAt,
|
|
1360
1480
|
);
|
|
1481
|
+
try {
|
|
1482
|
+
await prewarmFirstPartyPanelCache(
|
|
1483
|
+
sub,
|
|
1484
|
+
snapshot,
|
|
1485
|
+
options.deadlineAt,
|
|
1486
|
+
recipients.length,
|
|
1487
|
+
);
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
console.warn(
|
|
1490
|
+
"[dashboard-report] panel cache prewarm failed (non-fatal):",
|
|
1491
|
+
err instanceof Error ? err.message : err,
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1361
1494
|
const captureTimeoutMs = options.deadlineAt
|
|
1362
1495
|
? Math.min(
|
|
1363
1496
|
SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
|
|
1364
1497
|
options.deadlineAt -
|
|
1365
1498
|
Date.now() -
|
|
1366
|
-
|
|
1367
|
-
recipients.length * DASHBOARD_REPORT_EMAIL_TIMEOUT_MS -
|
|
1368
|
-
DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS,
|
|
1499
|
+
reportDeliveryReserveMs(recipients.length),
|
|
1369
1500
|
)
|
|
1370
1501
|
: undefined;
|
|
1371
1502
|
const capture = await captureDashboardPngWithFallback(
|