@agent-native/core 0.84.66 → 0.85.0
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/locales/ar-SA/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +199 -38
- package/corpus/core/src/agent/run-manager.ts +79 -0
- package/corpus/core/src/agent/run-store.ts +117 -5
- package/corpus/core/src/agent/types.ts +13 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +475 -0
- package/corpus/core/src/client/analytics.ts +2 -0
- package/corpus/core/src/client/error-format.ts +4 -0
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +1 -1
- package/corpus/core/src/client/session-replay.ts +774 -2
- package/corpus/core/src/client/sse-event-processor.ts +65 -9
- package/corpus/core/src/client/use-run-stuck-detection.ts +31 -7
- package/corpus/core/src/db/runtime-diagnostics.ts +331 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +115 -2
- package/corpus/core/src/server/analytics.ts +1 -34
- package/corpus/core/src/server/core-routes-plugin.ts +91 -4
- package/corpus/core/src/server/http-response-telemetry.ts +189 -0
- package/corpus/core/src/server/self-dispatch.ts +29 -0
- package/corpus/core/src/server/ssr-handler.ts +18 -377
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +53 -1
- package/corpus/templates/analytics/AGENTS.md +13 -0
- package/corpus/templates/analytics/actions/view-screen.ts +25 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +20 -0
- package/corpus/templates/analytics/app/global.css +55 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +100 -1
- package/corpus/templates/analytics/app/i18n-data.ts +1226 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +182 -144
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +11 -18
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +11 -18
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +49 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +557 -0
- package/corpus/templates/analytics/app/pages/sessions/session-replay-devtools.ts +274 -0
- package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +974 -0
- package/corpus/templates/analytics/changelog/2026-07-02-alert-rules-can-now-be-viewed-and-managed-from-settings.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-recent-ask-chats-show-a-loading-placeholder-while-your-chat-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-session-replays-now-capture-console-logs-and-network-request.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-sharing-status-labels-use-neutral-icons-instead-of-colored-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-02-the-session-replay-viewer-has-a-dev-tools-panel-with-console.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +3 -0
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +4 -0
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +142 -1
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +369 -8
- package/corpus/templates/analytics/server/lib/session-replay.ts +76 -2
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +101 -0
- package/corpus/templates/analytics/shared/session-replay-diagnostics.ts +78 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +0 -2
- package/corpus/templates/design/app/pages/DesignEditor.tsx +24 -12
- package/corpus/templates/design/changelog/2026-07-02-the-temporary-code-tab-is-hidden-from-the-design-editor-side.md +6 -0
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +0 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/update-local-plan-folder.ts +3 -1
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-02-trying-to-edit-a-recap-or-plan-you-can-only-view-now-explain.md +6 -0
- package/corpus/templates/plan/server/plans.ts +26 -9
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +155 -34
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +29 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +73 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +41 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +100 -4
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +13 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +13 -0
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +410 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +2 -0
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/session-replay.d.ts +36 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +579 -1
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +20 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +42 -11
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +14 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +18 -4
- package/dist/client/use-run-stuck-detection.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/db/runtime-diagnostics.d.ts +52 -0
- package/dist/db/runtime-diagnostics.d.ts.map +1 -0
- package/dist/db/runtime-diagnostics.js +250 -0
- package/dist/db/runtime-diagnostics.js.map +1 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +101 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/analytics.d.ts +1 -19
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +1 -31
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +17 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +65 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts +4 -0
- package/dist/server/http-response-telemetry.d.ts.map +1 -0
- package/dist/server/http-response-telemetry.js +174 -0
- package/dist/server/http-response-telemetry.js.map +1 -0
- package/dist/server/self-dispatch.d.ts +17 -0
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +11 -0
- package/dist/server/self-dispatch.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +18 -271
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/locales/ar-SA/tracking.mdx +55 -0
- package/docs/content/locales/de-DE/tracking.mdx +55 -0
- package/docs/content/locales/es-ES/tracking.mdx +55 -0
- package/docs/content/locales/fr-FR/tracking.mdx +55 -0
- package/docs/content/locales/hi-IN/tracking.mdx +55 -0
- package/docs/content/locales/ja-JP/tracking.mdx +55 -0
- package/docs/content/locales/ko-KR/tracking.mdx +55 -0
- package/docs/content/locales/pt-BR/tracking.mdx +55 -0
- package/docs/content/locales/zh-CN/tracking.mdx +55 -0
- package/docs/content/locales/zh-TW/tracking.mdx +55 -0
- package/docs/content/tracking.mdx +2 -0
- package/package.json +1 -1
|
@@ -8088,6 +8088,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
8088
8088
|
stages: m.RUN_DIAG_STAGE,
|
|
8089
8089
|
}))
|
|
8090
8090
|
.catch(() => null);
|
|
8091
|
+
const runtimeDetail = await import("../db/runtime-diagnostics.js")
|
|
8092
|
+
.then((m) => m.formatRuntimeDebugFingerprint())
|
|
8093
|
+
.catch(() => "");
|
|
8091
8094
|
|
|
8092
8095
|
// Record "the route handler was entered" against the run after auth
|
|
8093
8096
|
// succeeds. This is the proof the bg-fn invocation actually reached
|
|
@@ -8097,14 +8100,74 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
8097
8100
|
.record(prepared.runId, diag.stages.routeEntered)
|
|
8098
8101
|
.catch(() => {});
|
|
8099
8102
|
await diag
|
|
8100
|
-
.record(prepared.runId, diag.stages.authPassed)
|
|
8103
|
+
.record(prepared.runId, diag.stages.authPassed, runtimeDetail)
|
|
8101
8104
|
.catch(() => {});
|
|
8102
8105
|
}
|
|
8103
8106
|
|
|
8107
|
+
// PAYLOAD REHYDRATION: a `payloadRef` marker means the dispatch
|
|
8108
|
+
// carried ONLY the marker (Netlify caps background-function request
|
|
8109
|
+
// bodies at 256KB — a large chat history silently exceeded it) and
|
|
8110
|
+
// the full request body is persisted on the run row. Rehydrate it
|
|
8111
|
+
// here. The payload is NOT cleared on read: a Netlify at-least-once
|
|
8112
|
+
// retry of a failed invocation must be able to rehydrate again (the
|
|
8113
|
+
// claim CAS still dedupes execution); terminal status writes clear it.
|
|
8114
|
+
let workerBody: Record<string, unknown> = prepared.body;
|
|
8115
|
+
const preparedMarkerRecord =
|
|
8116
|
+
preparedMarker && typeof preparedMarker === "object"
|
|
8117
|
+
? (preparedMarker as Record<string, unknown>)
|
|
8118
|
+
: null;
|
|
8119
|
+
if (preparedMarkerRecord?.payloadRef === true) {
|
|
8120
|
+
const runStore = await import("../agent/run-store.js");
|
|
8121
|
+
const rawPayload = await runStore
|
|
8122
|
+
.readRunDispatchPayload(prepared.runId)
|
|
8123
|
+
.catch(() => null);
|
|
8124
|
+
let parsedPayload: Record<string, unknown> | null = null;
|
|
8125
|
+
if (rawPayload) {
|
|
8126
|
+
try {
|
|
8127
|
+
const candidate = JSON.parse(rawPayload);
|
|
8128
|
+
if (candidate && typeof candidate === "object") {
|
|
8129
|
+
parsedPayload = candidate as Record<string, unknown>;
|
|
8130
|
+
}
|
|
8131
|
+
} catch {
|
|
8132
|
+
// Corrupt payload — treated as missing below.
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
if (!parsedPayload) {
|
|
8136
|
+
// Row missing / reaped / already terminal — there is nothing to
|
|
8137
|
+
// run. Fail the run loudly (if it is still running) and ack the
|
|
8138
|
+
// dispatch with a 200 so Netlify does not retry a dead handoff.
|
|
8139
|
+
if (diag) {
|
|
8140
|
+
await diag
|
|
8141
|
+
.record(
|
|
8142
|
+
prepared.runId,
|
|
8143
|
+
diag.stages.workerThrew,
|
|
8144
|
+
"dispatch payload missing — cannot rehydrate background run body",
|
|
8145
|
+
)
|
|
8146
|
+
.catch(() => {});
|
|
8147
|
+
}
|
|
8148
|
+
const statusUpdated = await runStore
|
|
8149
|
+
.updateRunStatusIfRunning(prepared.runId, "errored")
|
|
8150
|
+
.catch(() => false);
|
|
8151
|
+
if (statusUpdated) {
|
|
8152
|
+
await runStore
|
|
8153
|
+
.setRunTerminalReason(
|
|
8154
|
+
prepared.runId,
|
|
8155
|
+
"dispatch_payload_missing",
|
|
8156
|
+
)
|
|
8157
|
+
.catch(() => {});
|
|
8158
|
+
}
|
|
8159
|
+
return { ok: false, skipped: "dispatch-payload-missing" };
|
|
8160
|
+
}
|
|
8161
|
+
workerBody = {
|
|
8162
|
+
...parsedPayload,
|
|
8163
|
+
[AGENT_CHAT_BACKGROUND_RUN_FIELD]: preparedMarker,
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
|
|
8104
8167
|
// Stash the verified+augmented body for the handler — the body stream
|
|
8105
8168
|
// is already consumed, so the handler reads this instead.
|
|
8106
8169
|
(event as any).context = (event as any).context ?? {};
|
|
8107
|
-
(event as any).context.__agentChatBackgroundBody =
|
|
8170
|
+
(event as any).context.__agentChatBackgroundBody = workerBody;
|
|
8108
8171
|
|
|
8109
8172
|
// Durable owner context: this self-dispatch is cookieless (HMAC-only).
|
|
8110
8173
|
// Resolve the owner from the persisted run row, never the request
|
|
@@ -8287,6 +8350,56 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
8287
8350
|
}, 15_000); // Start 15s after init (after the scheduler)
|
|
8288
8351
|
})();
|
|
8289
8352
|
|
|
8353
|
+
// ─── Unclaimed background-run sweep ────────────────────────────────
|
|
8354
|
+
// Backstop for LOST background handoffs. The foreground circuit-breaker
|
|
8355
|
+
// covers the initial dispatch (a connected client is polling the claim),
|
|
8356
|
+
// but a server-chained CONTINUATION handoff has no foreground watching
|
|
8357
|
+
// it: if the dispatch is lost after the successor row was inserted, the
|
|
8358
|
+
// row would sit at dispatch_mode='background' forever and the turn hangs
|
|
8359
|
+
// silently. This sweep reaps such rows into a loud, attributable error
|
|
8360
|
+
// (`background_worker_never_started`) that the client renders, instead
|
|
8361
|
+
// of an idle spinner. Cheap: one indexed-ish query per 2-min window.
|
|
8362
|
+
(() => {
|
|
8363
|
+
let lastSweep = 0;
|
|
8364
|
+
const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
|
|
8365
|
+
|
|
8366
|
+
setTimeout(() => {
|
|
8367
|
+
setInterval(() => {
|
|
8368
|
+
const now = Date.now();
|
|
8369
|
+
if (now - lastSweep < SWEEP_INTERVAL_MS) return;
|
|
8370
|
+
lastSweep = now;
|
|
8371
|
+
|
|
8372
|
+
(async () => {
|
|
8373
|
+
const {
|
|
8374
|
+
listUnclaimedBackgroundRunIds,
|
|
8375
|
+
reapUnclaimedBackgroundRun,
|
|
8376
|
+
} = await import("../agent/run-store.js");
|
|
8377
|
+
let runIds: string[];
|
|
8378
|
+
try {
|
|
8379
|
+
runIds = await listUnclaimedBackgroundRunIds();
|
|
8380
|
+
} catch {
|
|
8381
|
+
return; // Table may not exist yet on first boot
|
|
8382
|
+
}
|
|
8383
|
+
for (const staleRunId of runIds) {
|
|
8384
|
+
try {
|
|
8385
|
+
const reaped = await reapUnclaimedBackgroundRun(staleRunId);
|
|
8386
|
+
if (reaped) {
|
|
8387
|
+
console.error(
|
|
8388
|
+
"[agent-chat] swept unclaimed background run (handoff lost):",
|
|
8389
|
+
staleRunId,
|
|
8390
|
+
);
|
|
8391
|
+
}
|
|
8392
|
+
} catch {
|
|
8393
|
+
// best-effort per run
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
})().catch(() => {
|
|
8397
|
+
// best-effort — never break the server
|
|
8398
|
+
});
|
|
8399
|
+
}, 30_000); // Check every 30s but only sweep once per 2min
|
|
8400
|
+
}, 20_000); // Start 20s after init (after the agent-teams sweep)
|
|
8401
|
+
})();
|
|
8402
|
+
|
|
8290
8403
|
// ─── Trigger Dispatcher (event-based automations) ─────────────────
|
|
8291
8404
|
if (disableRecurringJobsRuntime) {
|
|
8292
8405
|
if (process.env.DEBUG) {
|
|
@@ -42,42 +42,9 @@ function getGaMeasurementId(): string | null {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Script hosts the injected GA loader pulls executable code from. Google Tag
|
|
47
|
-
* Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
|
|
48
|
-
* `www.google-analytics.com`. These must be listed in the document `script-src`
|
|
49
|
-
* so the CSP reflects the code the framework itself injects (see
|
|
50
|
-
* `applyDocumentCsp` in `ssr-handler.ts`).
|
|
51
|
-
*/
|
|
52
|
-
export const GA_CSP_SCRIPT_HOSTS = [
|
|
53
|
-
"https://www.googletagmanager.com",
|
|
54
|
-
"https://www.google-analytics.com",
|
|
55
|
-
] as const;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Network/image hosts used by the GA4 loader when it sends page-view and event
|
|
59
|
-
* beacons. These are separate from `script-src`: a stricter deployment CSP with
|
|
60
|
-
* `connect-src 'self'` or `img-src 'self'` can load gtag.js but still drop all
|
|
61
|
-
* analytics events unless these hosts are present too.
|
|
62
|
-
*/
|
|
63
|
-
export const GA_CSP_CONNECT_HOSTS = [
|
|
64
|
-
"https://www.google-analytics.com",
|
|
65
|
-
"https://analytics.google.com",
|
|
66
|
-
"https://stats.g.doubleclick.net",
|
|
67
|
-
"https://region1.google-analytics.com",
|
|
68
|
-
] as const;
|
|
69
|
-
|
|
70
|
-
export const GA_CSP_IMG_HOSTS = [
|
|
71
|
-
"https://www.google-analytics.com",
|
|
72
|
-
"https://www.googletagmanager.com",
|
|
73
|
-
"https://stats.g.doubleclick.net",
|
|
74
|
-
] as const;
|
|
75
|
-
|
|
76
45
|
/**
|
|
77
46
|
* The exact JS body (no surrounding `<script>` tags) of the inline gtag config
|
|
78
|
-
* block injected next to the gtag.js loader.
|
|
79
|
-
* hash it for the `script-src` CSP directive — the hash must be computed from
|
|
80
|
-
* the identical string that `getGaScript()` embeds, so both call this helper.
|
|
47
|
+
* block injected next to the gtag.js loader.
|
|
81
48
|
* Returns `null` when GA is not configured.
|
|
82
49
|
*/
|
|
83
50
|
export function getGaInlineConfigScriptBody(): string | null {
|
|
@@ -45,6 +45,12 @@ import {
|
|
|
45
45
|
import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js";
|
|
46
46
|
import { mountDbAdminRoutes } from "../db-admin/routes.js";
|
|
47
47
|
import { getDbExec } from "../db/client.js";
|
|
48
|
+
import {
|
|
49
|
+
getDatabaseRuntimeFingerprint,
|
|
50
|
+
getRuntimeDebugFingerprint,
|
|
51
|
+
runDatabaseSchemaHealthCheck,
|
|
52
|
+
type DatabaseSchemaHealthResult,
|
|
53
|
+
} from "../db/runtime-diagnostics.js";
|
|
48
54
|
import {
|
|
49
55
|
uploadFile,
|
|
50
56
|
getActiveFileUploadProviderForRequest,
|
|
@@ -143,6 +149,7 @@ import {
|
|
|
143
149
|
DEFAULT_UPLOAD_MAX_FILE_BYTES,
|
|
144
150
|
isAllowedUploadMimeType,
|
|
145
151
|
} from "./h3-helpers.js";
|
|
152
|
+
import { createHttpResponseTelemetryMiddleware } from "./http-response-telemetry.js";
|
|
146
153
|
import { isIdentitySsoEnabled } from "./identity-sso-store.js";
|
|
147
154
|
import { handleIdentitySso } from "./identity-sso.js";
|
|
148
155
|
import { createOpenRouteHandler } from "./open-route.js";
|
|
@@ -204,10 +211,24 @@ export function getFrameworkEnvKeys(): EnvKeyConfig[] {
|
|
|
204
211
|
export interface DbHealthProbeResult {
|
|
205
212
|
/** The serverless function is live and served the request. */
|
|
206
213
|
ok: true;
|
|
214
|
+
/** Database + optional schema readiness for stricter production monitors. */
|
|
215
|
+
ready: boolean;
|
|
207
216
|
/** A trivial `SELECT 1` reached the database (false = no DB or unreachable). */
|
|
208
217
|
db: boolean;
|
|
209
218
|
/** Round-trip time of the probe in milliseconds. */
|
|
210
219
|
ms: number;
|
|
220
|
+
/** Redacted database routing details useful for deploy/runtime checks. */
|
|
221
|
+
database: {
|
|
222
|
+
configured: boolean;
|
|
223
|
+
source: string;
|
|
224
|
+
dialect: string;
|
|
225
|
+
urlHash?: string;
|
|
226
|
+
appName?: string;
|
|
227
|
+
authTokenConfigured: boolean;
|
|
228
|
+
netlifyDatabaseUrlConfigured: boolean;
|
|
229
|
+
};
|
|
230
|
+
/** Optional metadata-only schema compatibility check. */
|
|
231
|
+
schema?: DatabaseSchemaHealthResult;
|
|
211
232
|
}
|
|
212
233
|
|
|
213
234
|
/**
|
|
@@ -222,16 +243,40 @@ export interface DbHealthProbeResult {
|
|
|
222
243
|
*/
|
|
223
244
|
export async function runDbHealthProbe(
|
|
224
245
|
exec: () => { execute: (sql: string) => Promise<unknown> } = getDbExec,
|
|
246
|
+
options: { schema?: boolean } = {},
|
|
225
247
|
): Promise<DbHealthProbeResult> {
|
|
226
248
|
const startedAt = Date.now();
|
|
227
249
|
let db = false;
|
|
250
|
+
let schema: DatabaseSchemaHealthResult | undefined;
|
|
251
|
+
const dbExec = exec();
|
|
228
252
|
try {
|
|
229
|
-
await
|
|
253
|
+
await dbExec.execute("SELECT 1");
|
|
230
254
|
db = true;
|
|
231
255
|
} catch {
|
|
232
256
|
// Live even when the DB is unreachable or the app has no database.
|
|
233
257
|
}
|
|
234
|
-
|
|
258
|
+
if (db && options.schema) {
|
|
259
|
+
schema = await runDatabaseSchemaHealthCheck({
|
|
260
|
+
exec: dbExec as ReturnType<typeof getDbExec>,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const database = getDatabaseRuntimeFingerprint();
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
ready: db && (!schema || schema.ok),
|
|
267
|
+
db,
|
|
268
|
+
ms: Date.now() - startedAt,
|
|
269
|
+
database: {
|
|
270
|
+
configured: database.configured,
|
|
271
|
+
source: database.source,
|
|
272
|
+
dialect: database.dialect,
|
|
273
|
+
urlHash: database.urlHash,
|
|
274
|
+
appName: database.appName,
|
|
275
|
+
authTokenConfigured: database.authTokenConfigured,
|
|
276
|
+
netlifyDatabaseUrlConfigured: database.netlifyDatabaseUrlConfigured,
|
|
277
|
+
},
|
|
278
|
+
...(schema ? { schema } : {}),
|
|
279
|
+
};
|
|
235
280
|
}
|
|
236
281
|
const DEFAULT_BUILDER_WAITLIST_FORM_ID = "DYTHuM0jlV";
|
|
237
282
|
const DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN = "https://forms.agent-native.com";
|
|
@@ -863,6 +908,8 @@ export function createCoreRoutesPlugin(
|
|
|
863
908
|
|
|
864
909
|
const P = FRAMEWORK_ROUTE_PREFIX;
|
|
865
910
|
|
|
911
|
+
getH3App(nitroApp).use(createHttpResponseTelemetryMiddleware());
|
|
912
|
+
|
|
866
913
|
// Security response headers — emitted on every framework response.
|
|
867
914
|
// Mounted before route handlers so 4xx/5xx error pages also carry the
|
|
868
915
|
// headers. Routes that need to tighten a specific header override via
|
|
@@ -1071,17 +1118,57 @@ export function createCoreRoutesPlugin(
|
|
|
1071
1118
|
// Health + DB warmup — liveness probe that touches the database so
|
|
1072
1119
|
// uptime monitors and the keep-warm cron prevent a scale-to-zero
|
|
1073
1120
|
// serverless DB (e.g. Neon) from cold-starting on the next real
|
|
1074
|
-
// request. Public, side-effect free, and never cached.
|
|
1121
|
+
// request. Public, side-effect free, and never cached. Add ?schema=1
|
|
1122
|
+
// for metadata-only schema checks, and ?strict=1 to turn a not-ready
|
|
1123
|
+
// DB/schema probe into a failing HTTP status for monitors.
|
|
1075
1124
|
if (!options.disableHealth) {
|
|
1076
1125
|
getH3App(nitroApp).use(
|
|
1077
1126
|
`${P}/health`,
|
|
1078
1127
|
defineEventHandler(async (event) => {
|
|
1079
1128
|
setResponseHeader(event, "cache-control", "no-store");
|
|
1080
|
-
|
|
1129
|
+
const schema =
|
|
1130
|
+
event.url?.searchParams.get("schema") === "1" ||
|
|
1131
|
+
event.url?.searchParams.get("schema") === "true";
|
|
1132
|
+
const strict =
|
|
1133
|
+
event.url?.searchParams.get("strict") === "1" ||
|
|
1134
|
+
event.url?.searchParams.get("strict") === "true" ||
|
|
1135
|
+
process.env.AGENT_NATIVE_HEALTH_STRICT_SCHEMA === "true";
|
|
1136
|
+
const result = await runDbHealthProbe(getDbExec, { schema });
|
|
1137
|
+
if (strict && !result.ready) setResponseStatus(event, 503);
|
|
1138
|
+
return result;
|
|
1081
1139
|
}),
|
|
1082
1140
|
);
|
|
1083
1141
|
}
|
|
1084
1142
|
|
|
1143
|
+
getH3App(nitroApp).use(
|
|
1144
|
+
`${P}/debug/runtime`,
|
|
1145
|
+
defineEventHandler(async (event) => {
|
|
1146
|
+
setResponseHeader(event, "cache-control", "no-store");
|
|
1147
|
+
const session = await getSession(event).catch(() => null);
|
|
1148
|
+
const productionLike =
|
|
1149
|
+
process.env.NODE_ENV === "production" ||
|
|
1150
|
+
process.env.NETLIFY === "true" ||
|
|
1151
|
+
process.env.VERCEL === "1";
|
|
1152
|
+
if (!session?.email && productionLike) {
|
|
1153
|
+
setResponseStatus(event, 401);
|
|
1154
|
+
return { error: "Authentication required" };
|
|
1155
|
+
}
|
|
1156
|
+
const schema = await runDatabaseSchemaHealthCheck().catch((err) => ({
|
|
1157
|
+
ok: false,
|
|
1158
|
+
checked: false,
|
|
1159
|
+
dialect: getDatabaseRuntimeFingerprint().dialect,
|
|
1160
|
+
missingTables: [],
|
|
1161
|
+
missingColumns: [],
|
|
1162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1163
|
+
}));
|
|
1164
|
+
return {
|
|
1165
|
+
ok: true,
|
|
1166
|
+
runtime: getRuntimeDebugFingerprint(),
|
|
1167
|
+
schema,
|
|
1168
|
+
};
|
|
1169
|
+
}),
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1085
1172
|
getH3App(nitroApp).use(
|
|
1086
1173
|
`${P}/speculation-rules.json`,
|
|
1087
1174
|
defineEventHandler((event) => {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { defineEventHandler, getHeader, getMethod } from "h3";
|
|
2
|
+
import type { EventHandler, H3Event } from "h3";
|
|
3
|
+
|
|
4
|
+
import { getDatabaseRuntimeFingerprint } from "../db/runtime-diagnostics.js";
|
|
5
|
+
import { track } from "../tracking/index.js";
|
|
6
|
+
import { getAppName } from "./app-name.js";
|
|
7
|
+
|
|
8
|
+
const TELEMETRY_EVENT_NAME = "http.response";
|
|
9
|
+
const TRACKING_INGEST_PATHS = new Set([
|
|
10
|
+
"/track",
|
|
11
|
+
"/api/analytics/track",
|
|
12
|
+
"/api/events/track",
|
|
13
|
+
"/_agent-native/track",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function envValue(key: string): string | undefined {
|
|
17
|
+
const value = process.env[key]?.trim();
|
|
18
|
+
return value || undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boolEnv(key: string): boolean {
|
|
22
|
+
return ["1", "true", "yes", "on"].includes(
|
|
23
|
+
(process.env[key] ?? "").trim().toLowerCase(),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sampleRate(): number {
|
|
28
|
+
const raw = envValue("AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE");
|
|
29
|
+
if (!raw) return 1;
|
|
30
|
+
const parsed = Number.parseFloat(raw);
|
|
31
|
+
if (!Number.isFinite(parsed)) return 1;
|
|
32
|
+
return Math.max(0, Math.min(1, parsed));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shouldDisableTelemetry(): boolean {
|
|
36
|
+
return boolEnv("AGENT_NATIVE_HTTP_TELEMETRY_DISABLED");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requestPath(event: H3Event): string {
|
|
40
|
+
const raw =
|
|
41
|
+
event.url?.pathname ??
|
|
42
|
+
String(event.node?.req?.url ?? event.path ?? "/").split("?")[0] ??
|
|
43
|
+
"/";
|
|
44
|
+
return raw || "/";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeSegment(segment: string): string {
|
|
48
|
+
if (!segment) return segment;
|
|
49
|
+
if (/^[0-9]+$/.test(segment)) return ":id";
|
|
50
|
+
if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(segment)) return ":id";
|
|
51
|
+
if (
|
|
52
|
+
/^(run|turn|thread|design|screen|file|msg|key|tok)_[a-z0-9_-]+$/i.test(
|
|
53
|
+
segment,
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
return ":id";
|
|
57
|
+
}
|
|
58
|
+
if (/^(run|turn)-[0-9]{10,}-[a-z0-9]+$/i.test(segment)) return ":id";
|
|
59
|
+
if (segment.length > 36 && /^[a-z0-9_-]+$/i.test(segment)) return ":id";
|
|
60
|
+
return segment;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function normalizeHttpTelemetryPath(pathname: string): string {
|
|
64
|
+
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
65
|
+
return normalized
|
|
66
|
+
.split("/")
|
|
67
|
+
.map((segment, index) => (index === 0 ? "" : normalizeSegment(segment)))
|
|
68
|
+
.join("/");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function statusClass(statusCode: number): string {
|
|
72
|
+
if (!Number.isFinite(statusCode) || statusCode < 100) return "unknown";
|
|
73
|
+
return `${Math.floor(statusCode / 100)}xx`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function routeKind(pathname: string): string {
|
|
77
|
+
if (pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")) {
|
|
78
|
+
return "framework";
|
|
79
|
+
}
|
|
80
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) return "api";
|
|
81
|
+
if (pathname.startsWith("/.well-known/")) return "well-known";
|
|
82
|
+
return "app";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hostForEvent(event: H3Event): string | undefined {
|
|
86
|
+
return (
|
|
87
|
+
getHeader(event, "x-forwarded-host") ??
|
|
88
|
+
getHeader(event, "host") ??
|
|
89
|
+
undefined
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function organizationForHost(host: string | undefined): string | undefined {
|
|
94
|
+
const configured =
|
|
95
|
+
envValue("AGENT_NATIVE_ANALYTICS_ORG_NAME") ??
|
|
96
|
+
envValue("AGENT_NATIVE_ORG_NAME");
|
|
97
|
+
if (configured) return configured;
|
|
98
|
+
const normalized = host?.split(":")[0]?.toLowerCase();
|
|
99
|
+
return normalized?.endsWith(".agent-native.com") ||
|
|
100
|
+
normalized === "agent-native.com"
|
|
101
|
+
? "Builder.io"
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shouldTrack(pathname: string, statusCode: number): boolean {
|
|
106
|
+
if (shouldDisableTelemetry()) return false;
|
|
107
|
+
if (TRACKING_INGEST_PATHS.has(pathname)) return false;
|
|
108
|
+
if (pathname.startsWith("/api/analytics/replay")) return false;
|
|
109
|
+
if (statusCode >= 500) return true;
|
|
110
|
+
const rate = sampleRate();
|
|
111
|
+
if (rate <= 0) return false;
|
|
112
|
+
if (rate >= 1) return true;
|
|
113
|
+
return Math.random() < rate;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function responseStatusCode(event: H3Event): number {
|
|
117
|
+
const raw =
|
|
118
|
+
(event.node?.res as any)?.statusCode ??
|
|
119
|
+
(event.node?.res as any)?.status ??
|
|
120
|
+
(event as any).res?.status ??
|
|
121
|
+
200;
|
|
122
|
+
const parsed = Number(raw);
|
|
123
|
+
return Number.isFinite(parsed) ? parsed : 200;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function emitTelemetry(event: H3Event, startedAt: number): void {
|
|
127
|
+
const statusCode = responseStatusCode(event);
|
|
128
|
+
const pathname = requestPath(event);
|
|
129
|
+
if (!shouldTrack(pathname, statusCode)) return;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const host = hostForEvent(event);
|
|
133
|
+
const db = getDatabaseRuntimeFingerprint();
|
|
134
|
+
track(TELEMETRY_EVENT_NAME, {
|
|
135
|
+
source: "server",
|
|
136
|
+
app: getAppName(),
|
|
137
|
+
template: envValue("AGENT_NATIVE_TEMPLATE") ?? getAppName(),
|
|
138
|
+
organization: organizationForHost(host),
|
|
139
|
+
method: getMethod(event),
|
|
140
|
+
path: normalizeHttpTelemetryPath(pathname),
|
|
141
|
+
route_kind: routeKind(pathname),
|
|
142
|
+
status_code: statusCode,
|
|
143
|
+
status_class: statusClass(statusCode),
|
|
144
|
+
duration_ms: Math.max(0, Date.now() - startedAt),
|
|
145
|
+
host,
|
|
146
|
+
environment: envValue("NODE_ENV"),
|
|
147
|
+
deploy_context: envValue("CONTEXT") ?? envValue("VERCEL_ENV"),
|
|
148
|
+
deploy_id: envValue("DEPLOY_ID") ?? envValue("VERCEL_DEPLOYMENT_ID"),
|
|
149
|
+
commit_ref:
|
|
150
|
+
envValue("COMMIT_REF") ??
|
|
151
|
+
envValue("NETLIFY_COMMIT_REF") ??
|
|
152
|
+
envValue("VERCEL_GIT_COMMIT_SHA") ??
|
|
153
|
+
envValue("GIT_COMMIT_SHA"),
|
|
154
|
+
db_source: db.source,
|
|
155
|
+
db_dialect: db.dialect,
|
|
156
|
+
db_url_hash: db.urlHash,
|
|
157
|
+
db_neon_endpoint: db.neon?.endpointId,
|
|
158
|
+
db_neon_pooled: db.neon?.pooled,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// Response telemetry is best-effort. Never perturb request handling.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createHttpResponseTelemetryMiddleware(): EventHandler {
|
|
166
|
+
return defineEventHandler((event) => {
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
const res = event.node?.res as
|
|
169
|
+
| {
|
|
170
|
+
once?: (event: "finish" | "close", cb: () => void) => void;
|
|
171
|
+
on?: (event: "finish" | "close", cb: () => void) => void;
|
|
172
|
+
}
|
|
173
|
+
| undefined;
|
|
174
|
+
let emitted = false;
|
|
175
|
+
const emitOnce = () => {
|
|
176
|
+
if (emitted) return;
|
|
177
|
+
emitted = true;
|
|
178
|
+
emitTelemetry(event, startedAt);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (typeof res?.once === "function") {
|
|
182
|
+
res.once("finish", emitOnce);
|
|
183
|
+
res.once("close", emitOnce);
|
|
184
|
+
} else if (typeof res?.on === "function") {
|
|
185
|
+
res.on("finish", emitOnce);
|
|
186
|
+
res.on("close", emitOnce);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -94,6 +94,23 @@ export interface FireInternalDispatchOptions {
|
|
|
94
94
|
body?: Record<string, unknown>;
|
|
95
95
|
/** Max ms to wait for the outbound request to leave the box. Default 250ms. */
|
|
96
96
|
settleMs?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Await the dispatch response fully instead of racing the settle timer.
|
|
99
|
+
*
|
|
100
|
+
* The 250ms settle race is correct for a synchronous handler that must
|
|
101
|
+
* respond to its own caller quickly — but it is WRONG for a handoff fired
|
|
102
|
+
* from a function that is about to finish (e.g. a background worker chaining
|
|
103
|
+
* its continuation chunk): once the handler's promise resolves, the Lambda
|
|
104
|
+
* freezes and a still-in-flight dispatch fetch is killed WITHOUT rejecting,
|
|
105
|
+
* so the handoff is lost silently — the error path never fires. With
|
|
106
|
+
* `awaitResponse: true` the call resolves only after the target confirmed
|
|
107
|
+
* receipt (Netlify background functions 202 on enqueue, normally well under
|
|
108
|
+
* a second) and throws on any network error or non-2xx, bounded by
|
|
109
|
+
* `responseTimeoutMs`.
|
|
110
|
+
*/
|
|
111
|
+
awaitResponse?: boolean;
|
|
112
|
+
/** Max ms to await the dispatch response when `awaitResponse` is set. Default 15s. */
|
|
113
|
+
responseTimeoutMs?: number;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
async function dispatchResponseError(
|
|
@@ -167,10 +184,14 @@ export async function fireInternalDispatch(
|
|
|
167
184
|
}
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
const awaitResponse = options.awaitResponse === true;
|
|
170
188
|
const dispatchPromise = fetch(url, {
|
|
171
189
|
method: "POST",
|
|
172
190
|
headers,
|
|
173
191
|
body: JSON.stringify({ taskId: options.taskId, ...(options.body ?? {}) }),
|
|
192
|
+
...(awaitResponse
|
|
193
|
+
? { signal: AbortSignal.timeout(options.responseTimeoutMs ?? 15_000) }
|
|
194
|
+
: {}),
|
|
174
195
|
}).then(async (res) => {
|
|
175
196
|
if (!res.ok) {
|
|
176
197
|
throw await dispatchResponseError(options.path, res);
|
|
@@ -187,6 +208,14 @@ export async function fireInternalDispatch(
|
|
|
187
208
|
);
|
|
188
209
|
});
|
|
189
210
|
|
|
211
|
+
if (awaitResponse) {
|
|
212
|
+
// Confirmed handoff: resolve only once the target acknowledged the
|
|
213
|
+
// dispatch (throws on network error / timeout / non-2xx). Used by callers
|
|
214
|
+
// whose own invocation is about to end — see the option doc above.
|
|
215
|
+
await dispatchPromise;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
190
219
|
const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;
|
|
191
220
|
await Promise.race([
|
|
192
221
|
dispatchPromise,
|