@agent-native/core 0.84.67 → 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 +14 -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/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/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/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/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 +2 -2
- 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/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- 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/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/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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-response-telemetry.d.ts","sourceRoot":"","sources":["../../src/server/http-response-telemetry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,IAAI,CAAC;AA6DhD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMnE;AAgGD,wBAAgB,qCAAqC,IAAI,YAAY,CAwBpE"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { defineEventHandler, getHeader, getMethod } from "h3";
|
|
2
|
+
import { getDatabaseRuntimeFingerprint } from "../db/runtime-diagnostics.js";
|
|
3
|
+
import { track } from "../tracking/index.js";
|
|
4
|
+
import { getAppName } from "./app-name.js";
|
|
5
|
+
const TELEMETRY_EVENT_NAME = "http.response";
|
|
6
|
+
const TRACKING_INGEST_PATHS = new Set([
|
|
7
|
+
"/track",
|
|
8
|
+
"/api/analytics/track",
|
|
9
|
+
"/api/events/track",
|
|
10
|
+
"/_agent-native/track",
|
|
11
|
+
]);
|
|
12
|
+
function envValue(key) {
|
|
13
|
+
const value = process.env[key]?.trim();
|
|
14
|
+
return value || undefined;
|
|
15
|
+
}
|
|
16
|
+
function boolEnv(key) {
|
|
17
|
+
return ["1", "true", "yes", "on"].includes((process.env[key] ?? "").trim().toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
function sampleRate() {
|
|
20
|
+
const raw = envValue("AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE");
|
|
21
|
+
if (!raw)
|
|
22
|
+
return 1;
|
|
23
|
+
const parsed = Number.parseFloat(raw);
|
|
24
|
+
if (!Number.isFinite(parsed))
|
|
25
|
+
return 1;
|
|
26
|
+
return Math.max(0, Math.min(1, parsed));
|
|
27
|
+
}
|
|
28
|
+
function shouldDisableTelemetry() {
|
|
29
|
+
return boolEnv("AGENT_NATIVE_HTTP_TELEMETRY_DISABLED");
|
|
30
|
+
}
|
|
31
|
+
function requestPath(event) {
|
|
32
|
+
const raw = event.url?.pathname ??
|
|
33
|
+
String(event.node?.req?.url ?? event.path ?? "/").split("?")[0] ??
|
|
34
|
+
"/";
|
|
35
|
+
return raw || "/";
|
|
36
|
+
}
|
|
37
|
+
function normalizeSegment(segment) {
|
|
38
|
+
if (!segment)
|
|
39
|
+
return segment;
|
|
40
|
+
if (/^[0-9]+$/.test(segment))
|
|
41
|
+
return ":id";
|
|
42
|
+
if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(segment))
|
|
43
|
+
return ":id";
|
|
44
|
+
if (/^(run|turn|thread|design|screen|file|msg|key|tok)_[a-z0-9_-]+$/i.test(segment)) {
|
|
45
|
+
return ":id";
|
|
46
|
+
}
|
|
47
|
+
if (/^(run|turn)-[0-9]{10,}-[a-z0-9]+$/i.test(segment))
|
|
48
|
+
return ":id";
|
|
49
|
+
if (segment.length > 36 && /^[a-z0-9_-]+$/i.test(segment))
|
|
50
|
+
return ":id";
|
|
51
|
+
return segment;
|
|
52
|
+
}
|
|
53
|
+
export function normalizeHttpTelemetryPath(pathname) {
|
|
54
|
+
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
55
|
+
return normalized
|
|
56
|
+
.split("/")
|
|
57
|
+
.map((segment, index) => (index === 0 ? "" : normalizeSegment(segment)))
|
|
58
|
+
.join("/");
|
|
59
|
+
}
|
|
60
|
+
function statusClass(statusCode) {
|
|
61
|
+
if (!Number.isFinite(statusCode) || statusCode < 100)
|
|
62
|
+
return "unknown";
|
|
63
|
+
return `${Math.floor(statusCode / 100)}xx`;
|
|
64
|
+
}
|
|
65
|
+
function routeKind(pathname) {
|
|
66
|
+
if (pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")) {
|
|
67
|
+
return "framework";
|
|
68
|
+
}
|
|
69
|
+
if (pathname === "/api" || pathname.startsWith("/api/"))
|
|
70
|
+
return "api";
|
|
71
|
+
if (pathname.startsWith("/.well-known/"))
|
|
72
|
+
return "well-known";
|
|
73
|
+
return "app";
|
|
74
|
+
}
|
|
75
|
+
function hostForEvent(event) {
|
|
76
|
+
return (getHeader(event, "x-forwarded-host") ??
|
|
77
|
+
getHeader(event, "host") ??
|
|
78
|
+
undefined);
|
|
79
|
+
}
|
|
80
|
+
function organizationForHost(host) {
|
|
81
|
+
const configured = envValue("AGENT_NATIVE_ANALYTICS_ORG_NAME") ??
|
|
82
|
+
envValue("AGENT_NATIVE_ORG_NAME");
|
|
83
|
+
if (configured)
|
|
84
|
+
return configured;
|
|
85
|
+
const normalized = host?.split(":")[0]?.toLowerCase();
|
|
86
|
+
return normalized?.endsWith(".agent-native.com") ||
|
|
87
|
+
normalized === "agent-native.com"
|
|
88
|
+
? "Builder.io"
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
function shouldTrack(pathname, statusCode) {
|
|
92
|
+
if (shouldDisableTelemetry())
|
|
93
|
+
return false;
|
|
94
|
+
if (TRACKING_INGEST_PATHS.has(pathname))
|
|
95
|
+
return false;
|
|
96
|
+
if (pathname.startsWith("/api/analytics/replay"))
|
|
97
|
+
return false;
|
|
98
|
+
if (statusCode >= 500)
|
|
99
|
+
return true;
|
|
100
|
+
const rate = sampleRate();
|
|
101
|
+
if (rate <= 0)
|
|
102
|
+
return false;
|
|
103
|
+
if (rate >= 1)
|
|
104
|
+
return true;
|
|
105
|
+
return Math.random() < rate;
|
|
106
|
+
}
|
|
107
|
+
function responseStatusCode(event) {
|
|
108
|
+
const raw = event.node?.res?.statusCode ??
|
|
109
|
+
event.node?.res?.status ??
|
|
110
|
+
event.res?.status ??
|
|
111
|
+
200;
|
|
112
|
+
const parsed = Number(raw);
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : 200;
|
|
114
|
+
}
|
|
115
|
+
function emitTelemetry(event, startedAt) {
|
|
116
|
+
const statusCode = responseStatusCode(event);
|
|
117
|
+
const pathname = requestPath(event);
|
|
118
|
+
if (!shouldTrack(pathname, statusCode))
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
const host = hostForEvent(event);
|
|
122
|
+
const db = getDatabaseRuntimeFingerprint();
|
|
123
|
+
track(TELEMETRY_EVENT_NAME, {
|
|
124
|
+
source: "server",
|
|
125
|
+
app: getAppName(),
|
|
126
|
+
template: envValue("AGENT_NATIVE_TEMPLATE") ?? getAppName(),
|
|
127
|
+
organization: organizationForHost(host),
|
|
128
|
+
method: getMethod(event),
|
|
129
|
+
path: normalizeHttpTelemetryPath(pathname),
|
|
130
|
+
route_kind: routeKind(pathname),
|
|
131
|
+
status_code: statusCode,
|
|
132
|
+
status_class: statusClass(statusCode),
|
|
133
|
+
duration_ms: Math.max(0, Date.now() - startedAt),
|
|
134
|
+
host,
|
|
135
|
+
environment: envValue("NODE_ENV"),
|
|
136
|
+
deploy_context: envValue("CONTEXT") ?? envValue("VERCEL_ENV"),
|
|
137
|
+
deploy_id: envValue("DEPLOY_ID") ?? envValue("VERCEL_DEPLOYMENT_ID"),
|
|
138
|
+
commit_ref: envValue("COMMIT_REF") ??
|
|
139
|
+
envValue("NETLIFY_COMMIT_REF") ??
|
|
140
|
+
envValue("VERCEL_GIT_COMMIT_SHA") ??
|
|
141
|
+
envValue("GIT_COMMIT_SHA"),
|
|
142
|
+
db_source: db.source,
|
|
143
|
+
db_dialect: db.dialect,
|
|
144
|
+
db_url_hash: db.urlHash,
|
|
145
|
+
db_neon_endpoint: db.neon?.endpointId,
|
|
146
|
+
db_neon_pooled: db.neon?.pooled,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Response telemetry is best-effort. Never perturb request handling.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function createHttpResponseTelemetryMiddleware() {
|
|
154
|
+
return defineEventHandler((event) => {
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
const res = event.node?.res;
|
|
157
|
+
let emitted = false;
|
|
158
|
+
const emitOnce = () => {
|
|
159
|
+
if (emitted)
|
|
160
|
+
return;
|
|
161
|
+
emitted = true;
|
|
162
|
+
emitTelemetry(event, startedAt);
|
|
163
|
+
};
|
|
164
|
+
if (typeof res?.once === "function") {
|
|
165
|
+
res.once("finish", emitOnce);
|
|
166
|
+
res.once("close", emitOnce);
|
|
167
|
+
}
|
|
168
|
+
else if (typeof res?.on === "function") {
|
|
169
|
+
res.on("finish", emitOnce);
|
|
170
|
+
res.on("close", emitOnce);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=http-response-telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-response-telemetry.js","sourceRoot":"","sources":["../../src/server/http-response-telemetry.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineEventHandler, getHeader, getMethod } from \"h3\";\nimport type { EventHandler, H3Event } from \"h3\";\n\nimport { getDatabaseRuntimeFingerprint } from \"../db/runtime-diagnostics.js\";\nimport { track } from \"../tracking/index.js\";\nimport { getAppName } from \"./app-name.js\";\n\nconst TELEMETRY_EVENT_NAME = \"http.response\";\nconst TRACKING_INGEST_PATHS = new Set([\n \"/track\",\n \"/api/analytics/track\",\n \"/api/events/track\",\n \"/_agent-native/track\",\n]);\n\nfunction envValue(key: string): string | undefined {\n const value = process.env[key]?.trim();\n return value || undefined;\n}\n\nfunction boolEnv(key: string): boolean {\n return [\"1\", \"true\", \"yes\", \"on\"].includes(\n (process.env[key] ?? \"\").trim().toLowerCase(),\n );\n}\n\nfunction sampleRate(): number {\n const raw = envValue(\"AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE\");\n if (!raw) return 1;\n const parsed = Number.parseFloat(raw);\n if (!Number.isFinite(parsed)) return 1;\n return Math.max(0, Math.min(1, parsed));\n}\n\nfunction shouldDisableTelemetry(): boolean {\n return boolEnv(\"AGENT_NATIVE_HTTP_TELEMETRY_DISABLED\");\n}\n\nfunction requestPath(event: H3Event): string {\n const raw =\n event.url?.pathname ??\n String(event.node?.req?.url ?? event.path ?? \"/\").split(\"?\")[0] ??\n \"/\";\n return raw || \"/\";\n}\n\nfunction normalizeSegment(segment: string): string {\n if (!segment) return segment;\n if (/^[0-9]+$/.test(segment)) return \":id\";\n if (/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(segment)) return \":id\";\n if (\n /^(run|turn|thread|design|screen|file|msg|key|tok)_[a-z0-9_-]+$/i.test(\n segment,\n )\n ) {\n return \":id\";\n }\n if (/^(run|turn)-[0-9]{10,}-[a-z0-9]+$/i.test(segment)) return \":id\";\n if (segment.length > 36 && /^[a-z0-9_-]+$/i.test(segment)) return \":id\";\n return segment;\n}\n\nexport function normalizeHttpTelemetryPath(pathname: string): string {\n const normalized = pathname.startsWith(\"/\") ? pathname : `/${pathname}`;\n return normalized\n .split(\"/\")\n .map((segment, index) => (index === 0 ? \"\" : normalizeSegment(segment)))\n .join(\"/\");\n}\n\nfunction statusClass(statusCode: number): string {\n if (!Number.isFinite(statusCode) || statusCode < 100) return \"unknown\";\n return `${Math.floor(statusCode / 100)}xx`;\n}\n\nfunction routeKind(pathname: string): string {\n if (pathname === \"/_agent-native\" || pathname.startsWith(\"/_agent-native/\")) {\n return \"framework\";\n }\n if (pathname === \"/api\" || pathname.startsWith(\"/api/\")) return \"api\";\n if (pathname.startsWith(\"/.well-known/\")) return \"well-known\";\n return \"app\";\n}\n\nfunction hostForEvent(event: H3Event): string | undefined {\n return (\n getHeader(event, \"x-forwarded-host\") ??\n getHeader(event, \"host\") ??\n undefined\n );\n}\n\nfunction organizationForHost(host: string | undefined): string | undefined {\n const configured =\n envValue(\"AGENT_NATIVE_ANALYTICS_ORG_NAME\") ??\n envValue(\"AGENT_NATIVE_ORG_NAME\");\n if (configured) return configured;\n const normalized = host?.split(\":\")[0]?.toLowerCase();\n return normalized?.endsWith(\".agent-native.com\") ||\n normalized === \"agent-native.com\"\n ? \"Builder.io\"\n : undefined;\n}\n\nfunction shouldTrack(pathname: string, statusCode: number): boolean {\n if (shouldDisableTelemetry()) return false;\n if (TRACKING_INGEST_PATHS.has(pathname)) return false;\n if (pathname.startsWith(\"/api/analytics/replay\")) return false;\n if (statusCode >= 500) return true;\n const rate = sampleRate();\n if (rate <= 0) return false;\n if (rate >= 1) return true;\n return Math.random() < rate;\n}\n\nfunction responseStatusCode(event: H3Event): number {\n const raw =\n (event.node?.res as any)?.statusCode ??\n (event.node?.res as any)?.status ??\n (event as any).res?.status ??\n 200;\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : 200;\n}\n\nfunction emitTelemetry(event: H3Event, startedAt: number): void {\n const statusCode = responseStatusCode(event);\n const pathname = requestPath(event);\n if (!shouldTrack(pathname, statusCode)) return;\n\n try {\n const host = hostForEvent(event);\n const db = getDatabaseRuntimeFingerprint();\n track(TELEMETRY_EVENT_NAME, {\n source: \"server\",\n app: getAppName(),\n template: envValue(\"AGENT_NATIVE_TEMPLATE\") ?? getAppName(),\n organization: organizationForHost(host),\n method: getMethod(event),\n path: normalizeHttpTelemetryPath(pathname),\n route_kind: routeKind(pathname),\n status_code: statusCode,\n status_class: statusClass(statusCode),\n duration_ms: Math.max(0, Date.now() - startedAt),\n host,\n environment: envValue(\"NODE_ENV\"),\n deploy_context: envValue(\"CONTEXT\") ?? envValue(\"VERCEL_ENV\"),\n deploy_id: envValue(\"DEPLOY_ID\") ?? envValue(\"VERCEL_DEPLOYMENT_ID\"),\n commit_ref:\n envValue(\"COMMIT_REF\") ??\n envValue(\"NETLIFY_COMMIT_REF\") ??\n envValue(\"VERCEL_GIT_COMMIT_SHA\") ??\n envValue(\"GIT_COMMIT_SHA\"),\n db_source: db.source,\n db_dialect: db.dialect,\n db_url_hash: db.urlHash,\n db_neon_endpoint: db.neon?.endpointId,\n db_neon_pooled: db.neon?.pooled,\n });\n } catch {\n // Response telemetry is best-effort. Never perturb request handling.\n }\n}\n\nexport function createHttpResponseTelemetryMiddleware(): EventHandler {\n return defineEventHandler((event) => {\n const startedAt = Date.now();\n const res = event.node?.res as\n | {\n once?: (event: \"finish\" | \"close\", cb: () => void) => void;\n on?: (event: \"finish\" | \"close\", cb: () => void) => void;\n }\n | undefined;\n let emitted = false;\n const emitOnce = () => {\n if (emitted) return;\n emitted = true;\n emitTelemetry(event, startedAt);\n };\n\n if (typeof res?.once === \"function\") {\n res.once(\"finish\", emitOnce);\n res.once(\"close\", emitOnce);\n } else if (typeof res?.on === \"function\") {\n res.on(\"finish\", emitOnce);\n res.on(\"close\", emitOnce);\n }\n });\n}\n"]}
|
|
@@ -29,6 +29,23 @@ export interface FireInternalDispatchOptions {
|
|
|
29
29
|
body?: Record<string, unknown>;
|
|
30
30
|
/** Max ms to wait for the outbound request to leave the box. Default 250ms. */
|
|
31
31
|
settleMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Await the dispatch response fully instead of racing the settle timer.
|
|
34
|
+
*
|
|
35
|
+
* The 250ms settle race is correct for a synchronous handler that must
|
|
36
|
+
* respond to its own caller quickly — but it is WRONG for a handoff fired
|
|
37
|
+
* from a function that is about to finish (e.g. a background worker chaining
|
|
38
|
+
* its continuation chunk): once the handler's promise resolves, the Lambda
|
|
39
|
+
* freezes and a still-in-flight dispatch fetch is killed WITHOUT rejecting,
|
|
40
|
+
* so the handoff is lost silently — the error path never fires. With
|
|
41
|
+
* `awaitResponse: true` the call resolves only after the target confirmed
|
|
42
|
+
* receipt (Netlify background functions 202 on enqueue, normally well under
|
|
43
|
+
* a second) and throws on any network error or non-2xx, bounded by
|
|
44
|
+
* `responseTimeoutMs`.
|
|
45
|
+
*/
|
|
46
|
+
awaitResponse?: boolean;
|
|
47
|
+
/** Max ms to await the dispatch response when `awaitResponse` is set. Default 15s. */
|
|
48
|
+
responseTimeoutMs?: number;
|
|
32
49
|
}
|
|
33
50
|
export declare function fireInternalDispatch(options: FireInternalDispatchOptions): Promise<void>;
|
|
34
51
|
//# sourceMappingURL=self-dispatch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-dispatch.d.ts","sourceRoot":"","sources":["../../src/server/self-dispatch.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAgB9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,GAAG,GAAG,MAAM,CAoB9D;AAED,MAAM,WAAW,2BAA2B;IAC1C,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"self-dispatch.d.ts","sourceRoot":"","sources":["../../src/server/self-dispatch.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAgB9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,GAAG,GAAG,MAAM,CAoB9D;AAED,MAAM,WAAW,2BAA2B;IAC1C,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AA8CD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CA8Df"}
|
|
@@ -133,10 +133,14 @@ export async function fireInternalDispatch(options) {
|
|
|
133
133
|
console.error(`[self-dispatch] signInternalToken failed unexpectedly for ${options.taskId}:`, err);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
const awaitResponse = options.awaitResponse === true;
|
|
136
137
|
const dispatchPromise = fetch(url, {
|
|
137
138
|
method: "POST",
|
|
138
139
|
headers,
|
|
139
140
|
body: JSON.stringify({ taskId: options.taskId, ...(options.body ?? {}) }),
|
|
141
|
+
...(awaitResponse
|
|
142
|
+
? { signal: AbortSignal.timeout(options.responseTimeoutMs ?? 15_000) }
|
|
143
|
+
: {}),
|
|
140
144
|
}).then(async (res) => {
|
|
141
145
|
if (!res.ok) {
|
|
142
146
|
throw await dispatchResponseError(options.path, res);
|
|
@@ -149,6 +153,13 @@ export async function fireInternalDispatch(options) {
|
|
|
149
153
|
// logs diagnostic without changing the URL resolution order.
|
|
150
154
|
console.error(`[self-dispatch] dispatch to ${options.path} (base ${baseUrl}) failed:`, err);
|
|
151
155
|
});
|
|
156
|
+
if (awaitResponse) {
|
|
157
|
+
// Confirmed handoff: resolve only once the target acknowledged the
|
|
158
|
+
// dispatch (throws on network error / timeout / non-2xx). Used by callers
|
|
159
|
+
// whose own invocation is about to end — see the option doc above.
|
|
160
|
+
await dispatchPromise;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
152
163
|
const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;
|
|
153
164
|
await Promise.race([
|
|
154
165
|
dispatchPromise,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"self-dispatch.js","sourceRoot":"","sources":["../../src/server/self-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["import { isLocalDatabase } from \"../db/client.js\";\nimport { signInternalToken } from \"../integrations/internal-token.js\";\n/**\n * Shared self-dispatch helper for the framework's serverless background-work\n * pattern: enqueue a unit of work to SQL, then fire a fresh HTTP POST back to\n * this same deployment so the work runs in its own function invocation (with\n * its own full timeout budget) instead of riding on the request that created\n * it.\n *\n * This is the single mechanism that makes background work portable across every\n * host Nitro deploys to:\n * - Netlify Lambda / Vercel Functions / AWS Lambda — the dispatched request\n * hits a fresh function with its own budget; no `waitUntil` needed.\n * - Cloudflare Workers — same (and `waitUntil` still works as a belt-and-\n * suspenders fallback where the in-process path is used).\n * - Self-hosted / long-lived Node — the dispatch comes back as another\n * request to the same process; each handler still runs to completion.\n *\n * Originally inlined in both `a2a/handlers.ts` (`resolveSelfBaseUrl` +\n * `fireProcessTaskDispatch`) and `integrations/webhook-handler.ts`\n * (`resolveBaseUrl` + the dispatch in `enqueueAndDispatch`). Extracted here so\n * A2A, integration webhooks, and Agent Teams sub-agents share one tested\n * implementation.\n */\nimport {\n getConfiguredAppBasePath,\n withConfiguredAppBasePath,\n} from \"./app-base-path.js\";\n\n/**\n * On serverless, returning from the dispatching handler before the outbound\n * TCP handshake starts can freeze the function with the dispatch request stuck\n * in the queue. Racing the fetch against a short timer gives the request a\n * chance to leave the box at the cost of a little added latency on the\n * dispatching call. Mirrors the 250ms used by the A2A/webhook paths.\n */\nexport const DEFAULT_DISPATCH_SETTLE_MS = 250;\n\nfunction readHeader(event: any, name: string): string | undefined {\n try {\n const headers = event?.node?.req?.headers ?? event?.headers;\n if (!headers) return undefined;\n if (typeof headers.get === \"function\") {\n return headers.get(name) ?? undefined;\n }\n const map = headers as Record<string, string | undefined>;\n return map[name] ?? map[String(name).toLowerCase()];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve the base URL to fire a self-dispatch request at. Prefers explicit env\n * vars (most reliable on serverless, where inbound host headers can be the\n * platform's internal hostname), falling back to the inbound request headers\n * and finally localhost in dev.\n *\n * Throws in production / shared deployments when no env var is set — a silent\n * fallback to a bad host there would drop background work invisibly.\n */\nexport function resolveSelfDispatchBaseUrl(event?: any): string {\n const fromEnv =\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n process.env.BETTER_AUTH_URL;\n if (fromEnv) return withConfiguredAppBasePath(String(fromEnv));\n\n if (process.env.NODE_ENV === \"production\" || !isLocalDatabase()) {\n throw new Error(\n \"Self-dispatch requires APP_URL, URL, DEPLOY_URL, or BETTER_AUTH_URL in \" +\n \"production/shared deployments so background work can reach this \" +\n \"deployment's own URL.\",\n );\n }\n\n const proto = readHeader(event, \"x-forwarded-proto\") || \"http\";\n const host =\n readHeader(event, \"host\") || `localhost:${process.env.PORT || 3000}`;\n return withConfiguredAppBasePath(`${proto}://${host}`);\n}\n\nexport interface FireInternalDispatchOptions {\n /** Base URL of this deployment. Defaults to `resolveSelfDispatchBaseUrl(event)`. */\n baseUrl?: string;\n /** Request event used to derive the base URL when `baseUrl` is omitted. */\n event?: any;\n /** Framework route path to POST to (e.g. \"/_agent-native/agent-teams/_process-run\"). */\n path: string;\n /** Task/run id the processor will claim. Used to sign the HMAC token and as the default body. */\n taskId: string;\n /** Extra fields merged into the JSON body alongside `{ taskId }`. */\n body?: Record<string, unknown>;\n /** Max ms to wait for the outbound request to leave the box. Default 250ms. */\n settleMs?: number;\n}\n\nasync function dispatchResponseError(\n path: string,\n res: Response,\n): Promise<Error> {\n let body = \"\";\n try {\n body = (await res.text()).trim();\n } catch {\n body = \"\";\n }\n const detail = body ? `: ${body.slice(0, 300)}` : \"\";\n return new Error(\n `Self-dispatch to ${path} returned HTTP ${res.status} ${res.statusText}${detail}`,\n );\n}\n\n/**\n * Fire a fresh, HMAC-signed POST to a processor route on this same deployment.\n * Fire-and-forget: the dispatch is NOT awaited to completion (the processed run\n * may take minutes); it is only raced against a short settle timer so the\n * request reliably leaves a serverless box before it freezes.\n *\n * When `A2A_SECRET` is unset (local dev), the request is sent unsigned — the\n * processor accepts unsigned dispatches in dev and relies on the SQL atomic\n * claim for double-processing protection, mirroring the A2A/webhook flow.\n */\n/**\n * For host-root dispatch targets (`/.netlify/functions/*`), strip the configured\n * app base path suffix from the resolved base url so the request reaches the\n * function at the host root rather than under the workspace app base path. For\n * every other (framework-route) path the base-path-prefixed base url is returned\n * unchanged, preserving the existing self-dispatch behavior.\n */\nfunction rootBaseUrlForPath(baseUrl: string, path: string): string {\n if (!path.startsWith(\"/.netlify/\")) return baseUrl;\n const basePath = getConfiguredAppBasePath();\n if (!basePath) return baseUrl;\n const trimmed = baseUrl.replace(/\\/$/, \"\");\n if (trimmed.endsWith(basePath)) {\n return trimmed.slice(0, trimmed.length - basePath.length);\n }\n return trimmed;\n}\n\nexport async function fireInternalDispatch(\n options: FireInternalDispatchOptions,\n): Promise<void> {\n const baseUrl = options.baseUrl ?? resolveSelfDispatchBaseUrl(options.event);\n // Netlify function default urls (`/.netlify/functions/<name>`) live at the\n // HOST ROOT, not under the workspace app base path. `resolveSelfDispatchBaseUrl`\n // appends the configured base path (e.g. `https://host/starter`) so framework\n // routes land on the right app; for a host-root function url we must dispatch\n // to `https://host/.netlify/functions/<name>` instead. Strip the base path\n // suffix from the resolved base url for `/.netlify/*` dispatch targets only.\n const url = `${rootBaseUrlForPath(baseUrl, options.path)}${options.path}`;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(options.taskId)}`;\n } catch (err) {\n // Distinguish the documented \"no A2A_SECRET in dev\" path from a real\n // signing failure, so a malformed secret doesn't fail invisibly.\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[self-dispatch] signInternalToken failed unexpectedly for ${options.taskId}:`,\n err,\n );\n }\n }\n\n const dispatchPromise = fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId: options.taskId, ...(options.body ?? {}) }),\n }).then(async (res) => {\n if (!res.ok) {\n throw await dispatchResponseError(options.path, res);\n }\n });\n dispatchPromise.catch((err) => {\n // Include the resolved base URL: a self-dispatch failure is almost always\n // about *which* host we POST to (custom domain behind an edge/auth wall vs\n // the deploy URL), and that is invisible from the error alone. Keeps prod\n // logs diagnostic without changing the URL resolution order.\n console.error(\n `[self-dispatch] dispatch to ${options.path} (base ${baseUrl}) failed:`,\n err,\n );\n });\n\n const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;\n await Promise.race([\n dispatchPromise,\n new Promise<void>((resolve) => setTimeout(resolve, settleMs)),\n ]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"self-dispatch.js","sourceRoot":"","sources":["../../src/server/self-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["import { isLocalDatabase } from \"../db/client.js\";\nimport { signInternalToken } from \"../integrations/internal-token.js\";\n/**\n * Shared self-dispatch helper for the framework's serverless background-work\n * pattern: enqueue a unit of work to SQL, then fire a fresh HTTP POST back to\n * this same deployment so the work runs in its own function invocation (with\n * its own full timeout budget) instead of riding on the request that created\n * it.\n *\n * This is the single mechanism that makes background work portable across every\n * host Nitro deploys to:\n * - Netlify Lambda / Vercel Functions / AWS Lambda — the dispatched request\n * hits a fresh function with its own budget; no `waitUntil` needed.\n * - Cloudflare Workers — same (and `waitUntil` still works as a belt-and-\n * suspenders fallback where the in-process path is used).\n * - Self-hosted / long-lived Node — the dispatch comes back as another\n * request to the same process; each handler still runs to completion.\n *\n * Originally inlined in both `a2a/handlers.ts` (`resolveSelfBaseUrl` +\n * `fireProcessTaskDispatch`) and `integrations/webhook-handler.ts`\n * (`resolveBaseUrl` + the dispatch in `enqueueAndDispatch`). Extracted here so\n * A2A, integration webhooks, and Agent Teams sub-agents share one tested\n * implementation.\n */\nimport {\n getConfiguredAppBasePath,\n withConfiguredAppBasePath,\n} from \"./app-base-path.js\";\n\n/**\n * On serverless, returning from the dispatching handler before the outbound\n * TCP handshake starts can freeze the function with the dispatch request stuck\n * in the queue. Racing the fetch against a short timer gives the request a\n * chance to leave the box at the cost of a little added latency on the\n * dispatching call. Mirrors the 250ms used by the A2A/webhook paths.\n */\nexport const DEFAULT_DISPATCH_SETTLE_MS = 250;\n\nfunction readHeader(event: any, name: string): string | undefined {\n try {\n const headers = event?.node?.req?.headers ?? event?.headers;\n if (!headers) return undefined;\n if (typeof headers.get === \"function\") {\n return headers.get(name) ?? undefined;\n }\n const map = headers as Record<string, string | undefined>;\n return map[name] ?? map[String(name).toLowerCase()];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve the base URL to fire a self-dispatch request at. Prefers explicit env\n * vars (most reliable on serverless, where inbound host headers can be the\n * platform's internal hostname), falling back to the inbound request headers\n * and finally localhost in dev.\n *\n * Throws in production / shared deployments when no env var is set — a silent\n * fallback to a bad host there would drop background work invisibly.\n */\nexport function resolveSelfDispatchBaseUrl(event?: any): string {\n const fromEnv =\n process.env.APP_URL ||\n process.env.URL ||\n process.env.DEPLOY_URL ||\n process.env.BETTER_AUTH_URL;\n if (fromEnv) return withConfiguredAppBasePath(String(fromEnv));\n\n if (process.env.NODE_ENV === \"production\" || !isLocalDatabase()) {\n throw new Error(\n \"Self-dispatch requires APP_URL, URL, DEPLOY_URL, or BETTER_AUTH_URL in \" +\n \"production/shared deployments so background work can reach this \" +\n \"deployment's own URL.\",\n );\n }\n\n const proto = readHeader(event, \"x-forwarded-proto\") || \"http\";\n const host =\n readHeader(event, \"host\") || `localhost:${process.env.PORT || 3000}`;\n return withConfiguredAppBasePath(`${proto}://${host}`);\n}\n\nexport interface FireInternalDispatchOptions {\n /** Base URL of this deployment. Defaults to `resolveSelfDispatchBaseUrl(event)`. */\n baseUrl?: string;\n /** Request event used to derive the base URL when `baseUrl` is omitted. */\n event?: any;\n /** Framework route path to POST to (e.g. \"/_agent-native/agent-teams/_process-run\"). */\n path: string;\n /** Task/run id the processor will claim. Used to sign the HMAC token and as the default body. */\n taskId: string;\n /** Extra fields merged into the JSON body alongside `{ taskId }`. */\n body?: Record<string, unknown>;\n /** Max ms to wait for the outbound request to leave the box. Default 250ms. */\n settleMs?: number;\n /**\n * Await the dispatch response fully instead of racing the settle timer.\n *\n * The 250ms settle race is correct for a synchronous handler that must\n * respond to its own caller quickly — but it is WRONG for a handoff fired\n * from a function that is about to finish (e.g. a background worker chaining\n * its continuation chunk): once the handler's promise resolves, the Lambda\n * freezes and a still-in-flight dispatch fetch is killed WITHOUT rejecting,\n * so the handoff is lost silently — the error path never fires. With\n * `awaitResponse: true` the call resolves only after the target confirmed\n * receipt (Netlify background functions 202 on enqueue, normally well under\n * a second) and throws on any network error or non-2xx, bounded by\n * `responseTimeoutMs`.\n */\n awaitResponse?: boolean;\n /** Max ms to await the dispatch response when `awaitResponse` is set. Default 15s. */\n responseTimeoutMs?: number;\n}\n\nasync function dispatchResponseError(\n path: string,\n res: Response,\n): Promise<Error> {\n let body = \"\";\n try {\n body = (await res.text()).trim();\n } catch {\n body = \"\";\n }\n const detail = body ? `: ${body.slice(0, 300)}` : \"\";\n return new Error(\n `Self-dispatch to ${path} returned HTTP ${res.status} ${res.statusText}${detail}`,\n );\n}\n\n/**\n * Fire a fresh, HMAC-signed POST to a processor route on this same deployment.\n * Fire-and-forget: the dispatch is NOT awaited to completion (the processed run\n * may take minutes); it is only raced against a short settle timer so the\n * request reliably leaves a serverless box before it freezes.\n *\n * When `A2A_SECRET` is unset (local dev), the request is sent unsigned — the\n * processor accepts unsigned dispatches in dev and relies on the SQL atomic\n * claim for double-processing protection, mirroring the A2A/webhook flow.\n */\n/**\n * For host-root dispatch targets (`/.netlify/functions/*`), strip the configured\n * app base path suffix from the resolved base url so the request reaches the\n * function at the host root rather than under the workspace app base path. For\n * every other (framework-route) path the base-path-prefixed base url is returned\n * unchanged, preserving the existing self-dispatch behavior.\n */\nfunction rootBaseUrlForPath(baseUrl: string, path: string): string {\n if (!path.startsWith(\"/.netlify/\")) return baseUrl;\n const basePath = getConfiguredAppBasePath();\n if (!basePath) return baseUrl;\n const trimmed = baseUrl.replace(/\\/$/, \"\");\n if (trimmed.endsWith(basePath)) {\n return trimmed.slice(0, trimmed.length - basePath.length);\n }\n return trimmed;\n}\n\nexport async function fireInternalDispatch(\n options: FireInternalDispatchOptions,\n): Promise<void> {\n const baseUrl = options.baseUrl ?? resolveSelfDispatchBaseUrl(options.event);\n // Netlify function default urls (`/.netlify/functions/<name>`) live at the\n // HOST ROOT, not under the workspace app base path. `resolveSelfDispatchBaseUrl`\n // appends the configured base path (e.g. `https://host/starter`) so framework\n // routes land on the right app; for a host-root function url we must dispatch\n // to `https://host/.netlify/functions/<name>` instead. Strip the base path\n // suffix from the resolved base url for `/.netlify/*` dispatch targets only.\n const url = `${rootBaseUrlForPath(baseUrl, options.path)}${options.path}`;\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n try {\n headers[\"Authorization\"] = `Bearer ${signInternalToken(options.taskId)}`;\n } catch (err) {\n // Distinguish the documented \"no A2A_SECRET in dev\" path from a real\n // signing failure, so a malformed secret doesn't fail invisibly.\n if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {\n console.error(\n `[self-dispatch] signInternalToken failed unexpectedly for ${options.taskId}:`,\n err,\n );\n }\n }\n\n const awaitResponse = options.awaitResponse === true;\n const dispatchPromise = fetch(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ taskId: options.taskId, ...(options.body ?? {}) }),\n ...(awaitResponse\n ? { signal: AbortSignal.timeout(options.responseTimeoutMs ?? 15_000) }\n : {}),\n }).then(async (res) => {\n if (!res.ok) {\n throw await dispatchResponseError(options.path, res);\n }\n });\n dispatchPromise.catch((err) => {\n // Include the resolved base URL: a self-dispatch failure is almost always\n // about *which* host we POST to (custom domain behind an edge/auth wall vs\n // the deploy URL), and that is invisible from the error alone. Keeps prod\n // logs diagnostic without changing the URL resolution order.\n console.error(\n `[self-dispatch] dispatch to ${options.path} (base ${baseUrl}) failed:`,\n err,\n );\n });\n\n if (awaitResponse) {\n // Confirmed handoff: resolve only once the target acknowledged the\n // dispatch (throws on network error / timeout / non-2xx). Used by callers\n // whose own invocation is about to end — see the option doc above.\n await dispatchPromise;\n return;\n }\n\n const settleMs = options.settleMs ?? DEFAULT_DISPATCH_SETTLE_MS;\n await Promise.race([\n dispatchPromise,\n new Promise<void>((resolve) => setTimeout(resolve, settleMs)),\n ]);\n}\n"]}
|
|
@@ -188,6 +188,61 @@ track("checkout.completed", { total: 49.99, items: 3 });
|
|
|
188
188
|
|
|
189
189
|
يختلف هذا عن القياس عن بعد للمتصفح الداخلي لإطار العمل (`trackEvent()` / مشاهدات الصفحة التلقائية - راجع [Browser defaults](#browser-defaults) أدناه), والذي يدعم تحليلات المنتج الخاصة بـ Agent Native. استخدم `track()` لأحداث التحليلات الخاصة بتطبيقك والتي يجب أن تصل إلى مقدمي الخدمة الذين تم تكوينهم.
|
|
190
190
|
|
|
191
|
+
## إعادة تشغيل الجلسة {#session-replay}
|
|
192
|
+
|
|
193
|
+
يمكن لتطبيقات Agent Native الاشتراك في إعادة تشغيل جلسة المتصفح من الطرف الأول دون إضافة SDK تحليلات ثانٍ. استدعِ `configureTracking()` مرة واحدة في جذر المتصفح ومرر مفتاح Analytics العام بالإضافة إلى نقطة نهاية المُجمِّع:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { configureTracking } from "@agent-native/core/client";
|
|
197
|
+
|
|
198
|
+
configureTracking({
|
|
199
|
+
key: "anpk_...",
|
|
200
|
+
endpoint: "https://analytics.example.com/api/analytics/track",
|
|
201
|
+
sessionReplay: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
sampleRate: 0.1,
|
|
204
|
+
},
|
|
205
|
+
getDefaultProps: (_event, props) => ({
|
|
206
|
+
...props,
|
|
207
|
+
app: "my-app",
|
|
208
|
+
template: "my-template",
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
عندما يكون `sessionReplay.enabled` صادقًا (truthy)، يقوم العميل باستيراد `@rrweb/record` ديناميكيًا بعد بدء التشغيل وينشر أجزاء إعادة التشغيل إلى نقطة نهاية إعادة التشغيل. إذا كانت `endpoint` تنتهي بـ `/api/analytics/track` أو `/track`، يتم اشتقاق نقطة نهاية إعادة التشغيل تلقائيًا كـ `/api/analytics/replay`. تجاوز ذلك صراحةً باستخدام `sessionReplay.endpoint` عندما يكون مُجمِّع إعادة التشغيل موجودًا في مكان آخر.
|
|
214
|
+
|
|
215
|
+
تستدعي جذور قوالب Agent Native بالفعل `configureTracking()`. يمكن لعمليات نشر القوالب المستضافة تفعيل إعادة التشغيل باستخدام متغيرات بيئة Vite/Netlify، بينما يجب أن يُفضّل مستهلكو المكتبة الصيغة الصريحة `configureTracking({ key, endpoint, sessionReplay })` أعلاه:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
|
|
219
|
+
VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
|
|
220
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
|
|
221
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
يقوم مساعد المتصفح أيضًا بإجراء قراءة غير محظِّرة وبأفضل جهد ممكن لجلسة مصادقة Agent Native الحالية. إعادة التشغيل مخصصة لتسجيل الدخول فقط بشكل افتراضي: عندما يكون `sessionReplay` مفعّلاً، لا يبدأ التسجيل ما لم تُحلّ الجلسة إلى عنوان بريد إلكتروني لمستخدم. تتضمن إعادة تشغيل الجلسات المسجَّلة `userId`/`userEmail` المدعومين بالبريد الإلكتروني بالإضافة إلى `orgId`.
|
|
225
|
+
|
|
226
|
+
اضبط `sessionReplay.requireSignedInUser: false` أو `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false` فقط من أجل نشر إعادة تشغيل مجهولة الهوية مقصود. عندما يتم تعطيل تحكم المصادقة، تظل التسجيلات المجهولة قابلة للاستعلام حسب الزائر المجهول، والجلسة، والتطبيق/القالب، واسم المضيف، والمسار.
|
|
227
|
+
|
|
228
|
+
يتم أخذ عينات من إعادة تشغيل الجلسة بشكل حتمي لكل جلسة متصفح. يقوم `sampleRate` بقيمة `0.1` بتسجيل حوالي 10% من الجلسات المؤهلة؛ استخدم `1` عندما تكون المجموعة المؤهلة صغيرة عن قصد، مثل اختبار الاستخدام الداخلي لتسجيل الدخول فقط.
|
|
229
|
+
|
|
230
|
+
إعدادات الخصوصية الافتراضية متحفظة عن قصد لكنها لا تزال مفيدة لإعادة التشغيل:
|
|
231
|
+
|
|
232
|
+
- يتم إخفاء المدخلات بشكل افتراضي (`maskAllInputs: true`).
|
|
233
|
+
- يظل نص الصفحة مرئيًا ما لم يتم تمييز عنصر بـ `.an-mask` أو `data-an-mask`.
|
|
234
|
+
- يتم حظر المناطق الحساسة بمحددات مثل `[data-sensitive]`، و`.an-block`، و`.an-private`، و`data-an-block`، و`data-an-private`، والحقول الشبيهة ببطاقات الائتمان/كلمات المرور/أرقام الضمان الاجتماعي.
|
|
235
|
+
- يتم تنظيف عناوين URL باستخدام نفس مساعد `scrubUrl()` المستخدم بواسطة تحليلات المتصفح.
|
|
236
|
+
- التقاط إعادة التشغيل مخصص للويب فقط وباختيار المستخدم؛ فهو لا يسجل شاشات سطح المكتب الأصلية.
|
|
237
|
+
|
|
238
|
+
أثناء التسجيل، تلتقط إعادة تشغيل الجلسة أيضًا مخرجات وحدة تحكم المتصفح (`log`، و`info`، و`warn`، و`error`، و`debug`، بالإضافة إلى `error` / `unhandledrejection` الخاصة بالنافذة) وبيانات وصفية لطلبات الشبكة (`fetch` و XHR) كأحداث rrweb مخصصة موسومة، بحيث يمكن للوكلاء وعارض إعادة التشغيل تصحيح المشكلات التي يبلغ عنها المستخدمون. الالتقاط مفعّل افتراضيًا عند تفعيل إعادة التشغيل؛ اضبطه أو عطّله باستخدام خياري `sessionReplay.console` و`sessionReplay.network`، حيث يقبل كل منهما قيمة منطقية (boolean) أو كائن خيارات. لا يتم التقاط نصوص/رؤوس الطلبات والاستجابات مطلقًا، ويتم تنظيف عناوين URL، ويتم اقتطاع الرسائل، ويُستثنى حركة استيعاب/تتبع المسجِّل نفسه، وتضيف ميزانيات كل جلسة (1000 حدث وحدة تحكم / 2000 حدث شبكة) إشعار اقتطاع عند تجاوزها.
|
|
239
|
+
|
|
240
|
+
يخزّن قالب Analytics بيانات إعادة التشغيل الوصفية في SQL (`session_recordings`) ويخزّن الأجزاء عبر مراجع blob خاصة (`session_replay_chunks`). لا تتلقى المتصفحات والوكلاء أبدًا عناوين URL الخاصة بالموفر. تمر إعادة التشغيل عبر مسارات خادم محددة النطاق، وتُرجع أدوات الوكيل الافتراضية ملخصات أو أحداث إعادة تشغيل محدودة، وليس وصولاً خامًا إلى جدول الأجزاء.
|
|
241
|
+
|
|
242
|
+
عندما تحتاج إلى تسليم تسجيل خاص إلى وكيل خارجي، استخدم عنصر التحكم **نسخ للوكيل** في صفحة تفاصيل الجلسة. فهو يُنشئ رابط `agent_access` محدد النطاق بالتسجيل ومدته ساعتان، ويقوم بعرض SSR لحمولة اكتشاف صغيرة على `/sessions/:recordingId`، ولا يكشف إلا عن واجهات برمجة تطبيقات JSON الخاصة بسياق/أحداث الوكيل. لا تتغير رؤية التسجيل.
|
|
243
|
+
|
|
244
|
+
للتطوير المحلي فقط، يمكن لـ Analytics الرجوع إلى أجزاء SQL مضمّنة محدودة السعة عندما يكون تخزين blob الخاص غير متاح. يجب أن تُهيّئ عمليات النشر في الإنتاج تخزين blob خاصًا أو مشفّرًا بدلاً من الاعتماد على Postgres لحمولات إعادة التشغيل.
|
|
245
|
+
|
|
191
246
|
## متقدم: الموفرون المخصصون والداخليون {#advanced}
|
|
192
247
|
|
|
193
248
|
تحتاج معظم التطبيقات فقط إلى `track()` / `identify()` وموفر مدمج. بقية السطح - تسجيل موفري الخدمة المخصصين، واجهة `TrackingProvider`، الأجزاء الداخلية المجمعة، والقياس عن بعد للمتصفح الخاص بإطار العمل - موجود أدناه.
|
|
@@ -188,6 +188,61 @@ Hauptunterschiede zum [server `track()`](#track):
|
|
|
188
188
|
|
|
189
189
|
Dies unterscheidet sich von der internen Browser-Telemetrie des Frameworks (`trackEvent()` / automatische Seitenaufrufe – siehe [Browser defaults](#browser-defaults) unten), die die eigene Produktanalyse von Agent Native unterstützt. Verwenden Sie `track()` für die eigenen Analyseereignisse Ihrer App, die Ihre konfigurierten Anbieter erreichen sollen.
|
|
190
190
|
|
|
191
|
+
## Session Replay {#session-replay}
|
|
192
|
+
|
|
193
|
+
Agent-Native-Apps können sich für erstklassiges browserseitiges Session Replay entscheiden, ohne ein zweites Analytics-SDK hinzuzufügen. Rufen Sie `configureTracking()` einmal im Browser-Root auf und übergeben Sie den öffentlichen Analytics-Schlüssel sowie den Collector-Endpunkt:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { configureTracking } from "@agent-native/core/client";
|
|
197
|
+
|
|
198
|
+
configureTracking({
|
|
199
|
+
key: "anpk_...",
|
|
200
|
+
endpoint: "https://analytics.example.com/api/analytics/track",
|
|
201
|
+
sessionReplay: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
sampleRate: 0.1,
|
|
204
|
+
},
|
|
205
|
+
getDefaultProps: (_event, props) => ({
|
|
206
|
+
...props,
|
|
207
|
+
app: "my-app",
|
|
208
|
+
template: "my-template",
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Wenn `sessionReplay.enabled` einen truthy Wert hat, importiert der Client nach dem Start dynamisch `@rrweb/record` und sendet Replay-Chunks an den Replay-Endpunkt. Wenn `endpoint` auf `/api/analytics/track` oder `/track` endet, wird der Replay-Endpunkt automatisch als `/api/analytics/replay` abgeleitet. Überschreiben Sie ihn explizit mit `sessionReplay.endpoint`, wenn der Replay-Collector an anderer Stelle liegt.
|
|
214
|
+
|
|
215
|
+
Agent-Native-Template-Roots rufen `configureTracking()` bereits auf. Gehostete Template-Deployments können Replay über Vite-/Netlify-Umgebungsvariablen aktivieren, während Bibliotheks-Konsumenten die explizite Form `configureTracking({ key, endpoint, sessionReplay })` von oben bevorzugen sollten:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
|
|
219
|
+
VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
|
|
220
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
|
|
221
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Der Browser-Helfer führt außerdem einen nach bestem Bemühen erfolgenden, nicht blockierenden Lesevorgang der aktuellen Agent-Native-Auth-Sitzung durch. Replay ist standardmäßig nur für angemeldete Benutzer verfügbar: Wenn `sessionReplay` aktiviert ist, startet die Aufzeichnung nur, wenn die Sitzung zu einer Benutzer-E-Mail-Adresse aufgelöst werden kann. Replays angemeldeter Benutzer enthalten E-Mail-basierte `userId`/`userEmail` sowie `orgId`.
|
|
225
|
+
|
|
226
|
+
Setzen Sie `sessionReplay.requireSignedInUser: false` oder `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false` nur für ein absichtlich anonymes Replay-Deployment. Wenn die Auth-Prüfung deaktiviert ist, bleiben anonyme Aufzeichnungen nach anonymem Besucher, Sitzung, App/Template, Hostname und Pfad abfragbar.
|
|
227
|
+
|
|
228
|
+
Session Replay wird deterministisch pro Browser-Sitzung gesampelt. Eine `sampleRate` von `0.1` zeichnet etwa 10 % der infrage kommenden Sitzungen auf; verwenden Sie `1`, wenn die infrage kommende Population absichtlich klein ist, etwa beim Dogfooding nur für angemeldete Benutzer.
|
|
229
|
+
|
|
230
|
+
Die Datenschutz-Standardeinstellungen sind bewusst konservativ, aber für die Wiedergabe weiterhin nützlich:
|
|
231
|
+
|
|
232
|
+
- Eingaben werden standardmäßig maskiert (`maskAllInputs: true`).
|
|
233
|
+
- Seitentext bleibt sichtbar, sofern ein Element nicht mit `.an-mask` oder `data-an-mask` markiert ist.
|
|
234
|
+
- Sensible Bereiche werden mit Selektoren wie `[data-sensitive]`, `.an-block`, `.an-private`, `data-an-block`, `data-an-private` sowie kreditkarten-/passwort-/sozialversicherungsnummerähnlichen Feldern blockiert.
|
|
235
|
+
- URLs werden mit demselben `scrubUrl()`-Helfer bereinigt, der auch von der Browser-Analyse verwendet wird.
|
|
236
|
+
- Die Replay-Aufzeichnung ist ausschließlich webbasiert und opt-in; native Desktop-Bildschirme werden nicht aufgezeichnet.
|
|
237
|
+
|
|
238
|
+
Während der Aufzeichnung erfasst Session Replay außerdem die Browser-Konsolenausgabe (`log`, `info`, `warn`, `error`, `debug`, sowie die Fenster-Ereignisse `error` / `unhandledrejection`) und Netzwerkanfrage-Metadaten (`fetch` und XHR) als getaggte rrweb-Custom-Events, damit Agenten und der Replay-Viewer von Nutzern gemeldete Probleme debuggen können. Die Erfassung ist standardmäßig aktiviert, wenn Replay aktiviert ist; passen Sie sie mit den Optionen `sessionReplay.console` und `sessionReplay.network` an oder deaktivieren Sie sie, wobei jede Option entweder einen booleschen Wert oder ein Optionsobjekt akzeptiert. Request-/Response-Bodies und -Header werden nie erfasst, URLs werden bereinigt, Nachrichten werden gekürzt, der eigene Ingest-/Tracking-Traffic des Recorders wird ausgeschlossen, und sitzungsbezogene Budgets (1000 Konsolen- / 2000 Netzwerkereignisse) fügen bei Überschreitung einen Kürzungshinweis hinzu.
|
|
239
|
+
|
|
240
|
+
Das Analytics-Template speichert Replay-Metadaten in SQL (`session_recordings`) und Chunks über private Blob-Referenzen (`session_replay_chunks`). Browser und Agenten erhalten niemals Anbieter-URLs. Die Wiedergabe läuft über zugriffsbeschränkte Server-Routen, und die Standard-Agent-Tools liefern Zusammenfassungen oder begrenzte Replay-Ereignisse zurück, keinen Rohzugriff auf die Chunk-Tabelle.
|
|
241
|
+
|
|
242
|
+
Wenn Sie eine private Aufzeichnung an einen externen Agenten übergeben müssen, verwenden Sie auf der Sitzungsdetailseite die Funktion **Copy for agent**. Sie erzeugt einen zwei Stunden gültigen, aufzeichnungsgebundenen `agent_access`-Link, rendert eine kleine Discovery-Payload per SSR unter `/sessions/:recordingId` und stellt ausschließlich die JSON-APIs für Agent-Kontext/-Ereignisse bereit. Die Sichtbarkeit der Aufzeichnung ändert sich dadurch nicht.
|
|
243
|
+
|
|
244
|
+
Nur für die lokale Entwicklung kann Analytics auf begrenzte SQL-Inline-Chunks zurückgreifen, wenn kein privater Blob-Speicher verfügbar ist. Produktions-Deployments sollten privaten oder verschlüsselten Blob-Speicher konfigurieren, anstatt sich für Replay-Payloads auf Postgres zu verlassen.
|
|
245
|
+
|
|
191
246
|
## Erweitert: benutzerdefinierte Anbieter und Interna {#advanced}
|
|
192
247
|
|
|
193
248
|
Die meisten Apps benötigen nur `track()` / `identify()` und einen integrierten Anbieter. Der Rest der Oberfläche – Registrierung benutzerdefinierter Anbieter, die `TrackingProvider`-Schnittstelle, Batch-Interna und die eigene Browser-Telemetrie des Frameworks – finden Sie unten.
|
|
@@ -188,6 +188,61 @@ Diferencias clave con respecto al [server `track()`](#track):
|
|
|
188
188
|
|
|
189
189
|
Esto es distinto de la telemetría interna del navegador del marco (`trackEvent()` / vistas de página automáticas; consulte [Browser defaults](#browser-defaults) a continuación), que impulsa el propio análisis de productos de Agent Native. Utilice `track()` para los eventos de análisis propios de su aplicación que deberían llegar a sus proveedores configurados.
|
|
190
190
|
|
|
191
|
+
## Repetición de sesión {#session-replay}
|
|
192
|
+
|
|
193
|
+
Las aplicaciones de Agent Native pueden habilitar la repetición de sesión del navegador de origen sin agregar un segundo SDK de análisis. Llame a `configureTracking()` una vez en la raíz del navegador y pase la clave pública de Analytics más el endpoint del recolector:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { configureTracking } from "@agent-native/core/client";
|
|
197
|
+
|
|
198
|
+
configureTracking({
|
|
199
|
+
key: "anpk_...",
|
|
200
|
+
endpoint: "https://analytics.example.com/api/analytics/track",
|
|
201
|
+
sessionReplay: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
sampleRate: 0.1,
|
|
204
|
+
},
|
|
205
|
+
getDefaultProps: (_event, props) => ({
|
|
206
|
+
...props,
|
|
207
|
+
app: "my-app",
|
|
208
|
+
template: "my-template",
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Cuando `sessionReplay.enabled` es verdadero, el cliente importa dinámicamente `@rrweb/record` después del inicio y publica fragmentos de repetición en el endpoint de repetición. Si `endpoint` termina en `/api/analytics/track` o `/track`, el endpoint de repetición se deriva automáticamente como `/api/analytics/replay`. Anúlelo explícitamente con `sessionReplay.endpoint` cuando el recolector de repetición se encuentre en otro lugar.
|
|
214
|
+
|
|
215
|
+
Las raíces de las plantillas de Agent Native ya llaman a `configureTracking()`. Las implementaciones de plantillas alojadas pueden activar la repetición con variables de entorno de Vite/Netlify, mientras que los consumidores de la biblioteca deberían preferir la forma explícita `configureTracking({ key, endpoint, sessionReplay })` anterior:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
|
|
219
|
+
VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
|
|
220
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
|
|
221
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
El ayudante del navegador también realiza una lectura sin bloqueo y de mejor esfuerzo de la sesión de autenticación actual de Agent Native. La repetición es solo para usuarios con sesión iniciada de forma predeterminada: cuando `sessionReplay` está habilitado, la grabación no se inicia a menos que la sesión se resuelva en una dirección de correo electrónico de usuario. Las repeticiones de usuarios con sesión iniciada incluyen `userId`/`userEmail` respaldados por correo electrónico más `orgId`.
|
|
225
|
+
|
|
226
|
+
Configure `sessionReplay.requireSignedInUser: false` o `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false` solo para una implementación de repetición anónima intencional. Cuando la restricción de autenticación está deshabilitada, las grabaciones anónimas siguen siendo consultables por visitante anónimo, sesión, aplicación/plantilla, nombre de host y ruta.
|
|
227
|
+
|
|
228
|
+
La repetición de sesión se muestrea de forma determinista por sesión de navegador. Un `sampleRate` de `0.1` graba aproximadamente el 10% de las sesiones elegibles; use `1` cuando la población elegible sea intencionalmente pequeña, como en pruebas internas solo para usuarios con sesión iniciada.
|
|
229
|
+
|
|
230
|
+
Los valores predeterminados de privacidad son intencionalmente conservadores pero siguen siendo útiles para la reproducción:
|
|
231
|
+
|
|
232
|
+
- Las entradas están enmascaradas de forma predeterminada (`maskAllInputs: true`).
|
|
233
|
+
- El texto de la página permanece visible a menos que un elemento esté marcado con `.an-mask` o `data-an-mask`.
|
|
234
|
+
- Las zonas sensibles están bloqueadas con selectores como `[data-sensitive]`, `.an-block`, `.an-private`, `data-an-block`, `data-an-private`, y campos similares a tarjetas de crédito/contraseñas/SSN.
|
|
235
|
+
- Las URL se depuran con el mismo ayudante `scrubUrl()` que utiliza el análisis del navegador.
|
|
236
|
+
- La captura de repetición es solo para web y opcional; no graba pantallas de escritorio nativas.
|
|
237
|
+
|
|
238
|
+
Durante la grabación, la repetición de sesión también captura la salida de la consola del navegador (`log`, `info`, `warn`, `error`, `debug`, además de `error`/`unhandledrejection` de la ventana) y los metadatos de solicitudes de red (`fetch` y XHR) como eventos personalizados de rrweb etiquetados, para que los agentes y el visor de repeticiones puedan depurar los problemas reportados por los usuarios. La captura está habilitada de forma predeterminada cuando la repetición está activada; ajuste o desactive con las opciones `sessionReplay.console` y `sessionReplay.network`, cada una acepta un booleano o un objeto de opciones. Los cuerpos y encabezados de solicitud/respuesta nunca se capturan, las URL se depuran, los mensajes se truncan, el propio tráfico de ingesta/seguimiento del grabador se excluye, y los presupuestos por sesión (1000 eventos de consola / 2000 eventos de red) agregan un aviso de truncamiento cuando se superan.
|
|
239
|
+
|
|
240
|
+
La plantilla de Analytics almacena los metadatos de repetición en SQL (`session_recordings`) y almacena los fragmentos mediante referencias de blob privadas (`session_replay_chunks`). Los navegadores y los agentes nunca reciben las URL del proveedor. La reproducción pasa por rutas del servidor con alcance limitado y las herramientas del agente predeterminadas devuelven resúmenes o eventos de repetición acotados, no acceso sin procesar a la tabla de fragmentos.
|
|
241
|
+
|
|
242
|
+
Cuando necesite entregar una grabación privada a un agente externo, use el control **Copiar para el agente** de la página de detalle de la sesión. Este genera un enlace `agent_access` de dos horas con alcance limitado a la grabación, renderiza en el servidor (SSR) una pequeña carga útil de descubrimiento en `/sessions/:recordingId`, y expone únicamente las API JSON de contexto/eventos del agente. La visibilidad de la grabación no cambia.
|
|
243
|
+
|
|
244
|
+
Solo para desarrollo local, Analytics puede recurrir a fragmentos en línea de SQL limitados cuando el almacenamiento de blob privado no está disponible. Las implementaciones de producción deberían configurar almacenamiento de blob privado o cifrado en lugar de depender de Postgres para las cargas útiles de repetición.
|
|
245
|
+
|
|
191
246
|
## Avanzado: proveedores personalizados e internos {#advanced}
|
|
192
247
|
|
|
193
248
|
La mayoría de las aplicaciones solo necesitan `track()` / `identify()` y un proveedor integrado. El resto de la superficie (registro de proveedores personalizados, la interfaz `TrackingProvider`, componentes internos de procesamiento por lotes y la propia telemetría del navegador del marco) se encuentra a continuación.
|
|
@@ -188,6 +188,61 @@ Principales différences par rapport au [server `track()`](#track) :
|
|
|
188
188
|
|
|
189
189
|
Ceci est distinct de la télémétrie interne du navigateur du framework (`trackEvent()` / pages vues automatiques – voir [Browser defaults](#browser-defaults) ci-dessous), qui alimente les propres analyses de produits de Agent Native. Utilisez `track()` pour les événements d'analyse de votre application qui doivent atteindre vos fournisseurs configurés.
|
|
190
190
|
|
|
191
|
+
## Relecture de session {#session-replay}
|
|
192
|
+
|
|
193
|
+
Les applications Agent Native peuvent activer la relecture de session (session replay) côté navigateur en mode propriétaire, sans ajouter un second SDK d'analyse. Appelez `configureTracking()` une fois à la racine du navigateur et transmettez la clé publique Analytics ainsi que le point de terminaison du collecteur :
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { configureTracking } from "@agent-native/core/client";
|
|
197
|
+
|
|
198
|
+
configureTracking({
|
|
199
|
+
key: "anpk_...",
|
|
200
|
+
endpoint: "https://analytics.example.com/api/analytics/track",
|
|
201
|
+
sessionReplay: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
sampleRate: 0.1,
|
|
204
|
+
},
|
|
205
|
+
getDefaultProps: (_event, props) => ({
|
|
206
|
+
...props,
|
|
207
|
+
app: "my-app",
|
|
208
|
+
template: "my-template",
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Lorsque `sessionReplay.enabled` est vrai, le client importe dynamiquement `@rrweb/record` après le démarrage et transmet les blocs de relecture au point de terminaison de relecture. Si `endpoint` se termine par `/api/analytics/track` ou `/track`, le point de terminaison de relecture est dérivé automatiquement en `/api/analytics/replay`. Remplacez-le explicitement avec `sessionReplay.endpoint` lorsque le collecteur de relecture se trouve ailleurs.
|
|
214
|
+
|
|
215
|
+
Les racines des modèles Agent Native appellent déjà `configureTracking()`. Les déploiements de modèles hébergés peuvent activer la relecture avec des variables d'environnement Vite/Netlify, tandis que les consommateurs de bibliothèque devraient privilégier la forme explicite `configureTracking({ key, endpoint, sessionReplay })` ci-dessus :
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY=anpk_...
|
|
219
|
+
VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=https://analytics.example.com/api/analytics/track
|
|
220
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=true
|
|
221
|
+
VITE_AGENT_NATIVE_SESSION_REPLAY_SAMPLE_RATE=0.1
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
L'assistant navigateur effectue également une lecture non bloquante et au mieux de la session d'authentification Agent Native actuelle. La relecture est réservée aux utilisateurs connectés par défaut : lorsque `sessionReplay` est activé, l'enregistrement ne démarre pas tant que la session ne se résout pas en une adresse e-mail utilisateur. Les relectures des utilisateurs connectés incluent un `userId`/`userEmail` basé sur l'e-mail ainsi que `orgId`.
|
|
225
|
+
|
|
226
|
+
Définissez `sessionReplay.requireSignedInUser: false` ou `VITE_AGENT_NATIVE_SESSION_REPLAY_REQUIRE_AUTH=false` uniquement pour un déploiement de relecture anonyme intentionnel. Lorsque le contrôle d'authentification est désactivé, les enregistrements anonymes restent interrogeables par visiteur anonyme, session, application/modèle, nom d'hôte et chemin.
|
|
227
|
+
|
|
228
|
+
La relecture de session est échantillonnée de manière déterministe par session de navigateur. Un `sampleRate` de `0.1` enregistre environ 10 % des sessions éligibles ; utilisez `1` lorsque la population éligible est intentionnellement restreinte, par exemple pour du dogfooding réservé aux utilisateurs connectés.
|
|
229
|
+
|
|
230
|
+
Les paramètres de confidentialité par défaut sont volontairement prudents tout en restant utiles pour la relecture :
|
|
231
|
+
|
|
232
|
+
- Les champs de saisie sont masqués par défaut (`maskAllInputs: true`).
|
|
233
|
+
- Le texte de la page reste visible sauf si un élément est marqué avec `.an-mask` ou `data-an-mask`.
|
|
234
|
+
- Les zones sensibles sont bloquées avec des sélecteurs tels que `[data-sensitive]`, `.an-block`, `.an-private`, `data-an-block`, `data-an-private`, ainsi que les champs de type carte bancaire/mot de passe/numéro de sécurité sociale.
|
|
235
|
+
- Les URL sont nettoyées avec le même assistant `scrubUrl()` utilisé par les analyses du navigateur.
|
|
236
|
+
- La capture de relecture est réservée au web et opt-in ; elle n'enregistre pas les écrans natifs du bureau.
|
|
237
|
+
|
|
238
|
+
Pendant l'enregistrement, la relecture de session capture également la sortie de la console du navigateur (`log`, `info`, `warn`, `error`, `debug`, ainsi que les événements globaux `error` / `unhandledrejection`) et les métadonnées des requêtes réseau (`fetch` et XHR) sous forme d'événements personnalisés rrweb balisés, afin que les agents et la visionneuse de relecture puissent déboguer les problèmes signalés par les utilisateurs. La capture est activée par défaut lorsque la relecture est activée ; ajustez-la ou désactivez-la avec les options `sessionReplay.console` et `sessionReplay.network`, chacune acceptant un booléen ou un objet d'options. Les corps et en-têtes des requêtes/réponses ne sont jamais capturés, les URL sont nettoyées, les messages sont tronqués, le trafic d'ingestion/de suivi propre à l'enregistreur est exclu, et des budgets par session (1000 événements console / 2000 événements réseau) ajoutent une notice de troncature en cas de dépassement.
|
|
239
|
+
|
|
240
|
+
Le modèle Analytics stocke les métadonnées de relecture en SQL (`session_recordings`) et stocke les blocs via des références blob privées (`session_replay_chunks`). Les navigateurs et les agents ne reçoivent jamais les URL du fournisseur. La lecture passe par des routes serveur à portée limitée, et les outils d'agent par défaut renvoient des résumés ou des événements de relecture bornés, et non un accès brut à la table des blocs.
|
|
241
|
+
|
|
242
|
+
Lorsque vous devez transmettre un enregistrement privé à un agent externe, utilisez le contrôle **Copy for agent** de la page de détail de session. Il génère un lien `agent_access` limité à l'enregistrement, valide deux heures, effectue le rendu SSR d'une petite charge utile de découverte sur `/sessions/:recordingId`, et n'expose que les API JSON de contexte/événements de l'agent. La visibilité de l'enregistrement ne change pas.
|
|
243
|
+
|
|
244
|
+
Pour le développement local uniquement, Analytics peut se replier sur des blocs SQL en ligne plafonnés lorsque le stockage blob privé n'est pas disponible. Les déploiements en production doivent configurer un stockage blob privé ou chiffré plutôt que de s'appuyer sur Postgres pour les charges utiles de relecture.
|
|
245
|
+
|
|
191
246
|
## Avancé : fournisseurs personnalisés et éléments internes {#advanced}
|
|
192
247
|
|
|
193
248
|
La plupart des applications n'ont besoin que de `track()` / `identify()` et d'un fournisseur intégré. Le reste de la surface (enregistrement des fournisseurs personnalisés, de l'interface `TrackingProvider`, des éléments internes de traitement par lots et de la propre télémétrie du navigateur du framework) se trouve ci-dessous.
|