@agent-native/core 0.133.3 → 0.134.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +62 -0
- package/corpus/core/docs/content/observability.mdx +41 -1
- package/corpus/core/docs/content/template-content-developers.mdx +1 -1
- package/corpus/core/docs/content/tracking.mdx +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-manager.ts +7 -0
- package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
- package/corpus/core/src/client/analytics.ts +105 -1
- package/corpus/core/src/deploy/build.ts +38 -1
- package/corpus/core/src/observability/posthog-ai.ts +294 -0
- package/corpus/core/src/observability/routes.ts +29 -6
- package/corpus/core/src/observability/traces.ts +287 -12
- package/corpus/core/src/observability/types.ts +11 -0
- package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
- package/corpus/core/src/server/agent-run-context.ts +21 -0
- package/corpus/core/src/server/capture-error.ts +8 -0
- package/corpus/core/src/server/core-routes-plugin.ts +75 -4
- package/corpus/core/src/server/deploy-environment.ts +57 -0
- package/corpus/core/src/server/error-noise-filter.ts +268 -0
- package/corpus/core/src/server/posthog-config.ts +75 -0
- package/corpus/core/src/server/request-context.ts +6 -0
- package/corpus/core/src/server/sentry-config.ts +4 -8
- package/corpus/core/src/server/sentry-plugin.ts +13 -29
- package/corpus/core/src/server/sentry.ts +13 -156
- package/corpus/core/src/server/ssr-handler.ts +6 -1
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/corpus/core/src/tracking/error-capture.ts +44 -109
- package/corpus/core/src/tracking/index.ts +11 -0
- package/corpus/core/src/tracking/posthog-exception.ts +305 -0
- package/corpus/core/src/tracking/providers.ts +91 -14
- package/corpus/core/src/tracking/redaction.ts +105 -0
- package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
- package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
- package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
- package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
- package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
- package/corpus/templates/content/actions/_database-utils.ts +12 -3
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
- package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
- package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
- package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
- package/corpus/templates/content/actions/remove-database-items.ts +183 -0
- package/corpus/templates/content/actions/set-document-property.ts +57 -39
- package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
- package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
- package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
- package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
- package/corpus/templates/content/app/i18n-data.ts +130 -0
- package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
- package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +4 -5
- package/corpus/templates/content/shared/api.ts +4 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
- package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
- package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
- package/corpus/templates/design/app/pages/Index.tsx +3 -1
- package/corpus/templates/design/app/pages/Present.tsx +2 -1
- package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
- package/corpus/templates/design/package.json +4 -0
- package/corpus/templates/design/shared/html-integrity.ts +802 -556
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +7 -0
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +12 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +95 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +38 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/posthog-ai.d.ts +126 -0
- package/dist/observability/posthog-ai.d.ts.map +1 -0
- package/dist/observability/posthog-ai.js +190 -0
- package/dist/observability/posthog-ai.js.map +1 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts.map +1 -1
- package/dist/observability/routes.js +26 -6
- package/dist/observability/routes.js.map +1 -1
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +219 -10
- package/dist/observability/traces.js.map +1 -1
- package/dist/observability/types.d.ts +10 -0
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/observability/types.js +1 -0
- package/dist/observability/types.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +14 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +8 -0
- package/dist/server/agent-run-context.d.ts.map +1 -1
- package/dist/server/agent-run-context.js +18 -0
- package/dist/server/agent-run-context.js.map +1 -1
- package/dist/server/capture-error.d.ts +8 -0
- package/dist/server/capture-error.d.ts.map +1 -1
- package/dist/server/capture-error.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +0 -23
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +71 -7
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/deploy-environment.d.ts +17 -0
- package/dist/server/deploy-environment.d.ts.map +1 -0
- package/dist/server/deploy-environment.js +47 -0
- package/dist/server/deploy-environment.js.map +1 -0
- package/dist/server/error-noise-filter.d.ts +80 -0
- package/dist/server/error-noise-filter.d.ts.map +1 -0
- package/dist/server/error-noise-filter.js +173 -0
- package/dist/server/error-noise-filter.js.map +1 -0
- package/dist/server/posthog-config.d.ts +28 -0
- package/dist/server/posthog-config.d.ts.map +1 -0
- package/dist/server/posthog-config.js +53 -0
- package/dist/server/posthog-config.js.map +1 -0
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/sentry-config.d.ts +1 -0
- package/dist/server/sentry-config.d.ts.map +1 -1
- package/dist/server/sentry-config.js +3 -1
- package/dist/server/sentry-config.js.map +1 -1
- package/dist/server/sentry-plugin.d.ts.map +1 -1
- package/dist/server/sentry-plugin.js +12 -27
- package/dist/server/sentry-plugin.js.map +1 -1
- package/dist/server/sentry.d.ts.map +1 -1
- package/dist/server/sentry.js +8 -124
- package/dist/server/sentry.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +6 -1
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/dist/tracking/error-capture.d.ts +7 -0
- package/dist/tracking/error-capture.d.ts.map +1 -1
- package/dist/tracking/error-capture.js +10 -73
- package/dist/tracking/error-capture.js.map +1 -1
- package/dist/tracking/index.d.ts +1 -0
- package/dist/tracking/index.d.ts.map +1 -1
- package/dist/tracking/index.js +1 -0
- package/dist/tracking/index.js.map +1 -1
- package/dist/tracking/posthog-exception.d.ts +103 -0
- package/dist/tracking/posthog-exception.d.ts.map +1 -0
- package/dist/tracking/posthog-exception.js +181 -0
- package/dist/tracking/posthog-exception.js.map +1 -0
- package/dist/tracking/providers.d.ts +14 -0
- package/dist/tracking/providers.d.ts.map +1 -1
- package/dist/tracking/providers.js +63 -11
- package/dist/tracking/providers.js.map +1 -1
- package/dist/tracking/redaction.d.ts +25 -0
- package/dist/tracking/redaction.d.ts.map +1 -0
- package/dist/tracking/redaction.js +85 -0
- package/dist/tracking/redaction.js.map +1 -0
- package/docs/content/observability.mdx +41 -1
- package/docs/content/template-content-developers.mdx +1 -1
- package/docs/content/tracking.mdx +25 -0
- package/package.json +1 -1
- package/src/agent/run-manager.ts +7 -0
- package/src/client/agent-chat-adapter.ts +10 -0
- package/src/client/analytics.ts +105 -1
- package/src/deploy/build.ts +38 -1
- package/src/observability/posthog-ai.ts +294 -0
- package/src/observability/routes.ts +29 -6
- package/src/observability/traces.ts +287 -12
- package/src/observability/types.ts +11 -0
- package/src/secrets/register-framework-secrets.ts +16 -0
- package/src/server/agent-run-context.ts +21 -0
- package/src/server/capture-error.ts +8 -0
- package/src/server/core-routes-plugin.ts +75 -4
- package/src/server/deploy-environment.ts +57 -0
- package/src/server/error-noise-filter.ts +268 -0
- package/src/server/posthog-config.ts +75 -0
- package/src/server/request-context.ts +6 -0
- package/src/server/sentry-config.ts +4 -8
- package/src/server/sentry-plugin.ts +13 -29
- package/src/server/sentry.ts +13 -156
- package/src/server/ssr-handler.ts +6 -1
- package/src/shared/mcp-embed-headers.ts +1 -1
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
- package/src/tracking/error-capture.ts +44 -109
- package/src/tracking/index.ts +11 -0
- package/src/tracking/posthog-exception.ts +305 -0
- package/src/tracking/providers.ts +91 -14
- package/src/tracking/redaction.ts +105 -0
- package/corpus/templates/content/actions/delete-database-items.ts +0 -89
|
@@ -3,6 +3,15 @@ import type {
|
|
|
3
3
|
AgentLoopUsage,
|
|
4
4
|
} from "../agent/production-agent.js";
|
|
5
5
|
import type { AgentChatEvent, AgentToolInput } from "../agent/types.js";
|
|
6
|
+
import { getRequestContext } from "../server/request-context.js";
|
|
7
|
+
import {
|
|
8
|
+
MAX_AI_CONTENT_BYTES,
|
|
9
|
+
MAX_AI_SPANS_PER_RUN,
|
|
10
|
+
boundAiContent,
|
|
11
|
+
emitAiSpanEvent,
|
|
12
|
+
emitAiTraceEvent,
|
|
13
|
+
toAiErrorDetail,
|
|
14
|
+
} from "./posthog-ai.js";
|
|
6
15
|
import { type AgentSpan, endAgentSpan, startAgentSpan } from "./tracing.js";
|
|
7
16
|
import { trackingIdentityProperties } from "./tracking-identity.js";
|
|
8
17
|
import type { TraceSpan, TraceSummary, ObservabilityConfig } from "./types.js";
|
|
@@ -117,6 +126,17 @@ function emitLlmGenerationTrackingEvent(args: {
|
|
|
117
126
|
variantId: string;
|
|
118
127
|
}>;
|
|
119
128
|
modelSelectionSource?: string;
|
|
129
|
+
/**
|
|
130
|
+
* PostHog content fields. Each is `undefined` unless the matching capture
|
|
131
|
+
* flag is on, and is then OMITTED from the event — never sent as `[]`, which
|
|
132
|
+
* PostHog would render as "the model was called with no messages".
|
|
133
|
+
*/
|
|
134
|
+
aiInput?: unknown;
|
|
135
|
+
aiOutputChoices?: unknown;
|
|
136
|
+
aiTools?: unknown;
|
|
137
|
+
aiInputTruncated?: boolean;
|
|
138
|
+
aiOutputTruncated?: boolean;
|
|
139
|
+
browserSessionId?: string;
|
|
120
140
|
}): void {
|
|
121
141
|
const provider = llmProviderFromEngine(args.engineName, args.model);
|
|
122
142
|
const costUsd =
|
|
@@ -128,6 +148,15 @@ function emitLlmGenerationTrackingEvent(args: {
|
|
|
128
148
|
? args.inputTokens + args.outputTokens
|
|
129
149
|
: undefined;
|
|
130
150
|
const error = args.errorMessage ?? undefined;
|
|
151
|
+
const terminalCode =
|
|
152
|
+
args.terminalOutcome?.state === "failed" ||
|
|
153
|
+
args.terminalOutcome?.state === "input_required"
|
|
154
|
+
? args.terminalOutcome.code
|
|
155
|
+
: undefined;
|
|
156
|
+
const terminalRetryable =
|
|
157
|
+
args.terminalOutcome?.state === "failed"
|
|
158
|
+
? args.terminalOutcome.retryable
|
|
159
|
+
: undefined;
|
|
131
160
|
const properties: Record<string, unknown> = {
|
|
132
161
|
...trackingIdentityProperties(),
|
|
133
162
|
source: "agent_observability",
|
|
@@ -154,15 +183,8 @@ function emitLlmGenerationTrackingEvent(args: {
|
|
|
154
183
|
tools: args.tools,
|
|
155
184
|
tools_truncated: args.toolsTruncated,
|
|
156
185
|
terminal_state: args.terminalOutcome?.state,
|
|
157
|
-
terminal_code:
|
|
158
|
-
|
|
159
|
-
args.terminalOutcome?.state === "input_required"
|
|
160
|
-
? args.terminalOutcome.code
|
|
161
|
-
: undefined,
|
|
162
|
-
terminal_retryable:
|
|
163
|
-
args.terminalOutcome?.state === "failed"
|
|
164
|
-
? args.terminalOutcome.retryable
|
|
165
|
-
: undefined,
|
|
186
|
+
terminal_code: terminalCode,
|
|
187
|
+
terminal_retryable: terminalRetryable,
|
|
166
188
|
delegated: args.delegation ? true : undefined,
|
|
167
189
|
delegation_protocol: args.delegation?.protocol,
|
|
168
190
|
caller_app: args.delegation?.callerApp,
|
|
@@ -175,19 +197,36 @@ function emitLlmGenerationTrackingEvent(args: {
|
|
|
175
197
|
$ai_trace_id: args.runId,
|
|
176
198
|
$ai_session_id: args.threadId ?? undefined,
|
|
177
199
|
$ai_span_id: args.llmSpanId,
|
|
178
|
-
$ai_span_name:
|
|
179
|
-
|
|
200
|
+
$ai_span_name: args.model,
|
|
201
|
+
// Parent is the run's trace, not the internal `agent_run` span id — the
|
|
202
|
+
// latter is never emitted to PostHog, so pointing at it orphaned the
|
|
203
|
+
// generation and PostHog rendered a placeholder trace around it.
|
|
204
|
+
$ai_parent_id: args.runId,
|
|
180
205
|
$ai_model: args.model,
|
|
181
206
|
$ai_provider: provider,
|
|
182
207
|
$ai_input_tokens: args.inputTokens,
|
|
183
208
|
$ai_output_tokens: args.outputTokens,
|
|
184
209
|
$ai_latency: Math.round((args.durationMs / 1000) * 1000) / 1000,
|
|
185
210
|
$ai_is_error: args.status === "error",
|
|
186
|
-
$ai_error:
|
|
211
|
+
$ai_error:
|
|
212
|
+
args.status === "error"
|
|
213
|
+
? toAiErrorDetail(error, {
|
|
214
|
+
state: args.terminalOutcome?.state,
|
|
215
|
+
code: terminalCode,
|
|
216
|
+
retryable: terminalRetryable,
|
|
217
|
+
})
|
|
218
|
+
: undefined,
|
|
187
219
|
$ai_cache_read_input_tokens: args.cacheReadTokens,
|
|
188
220
|
$ai_cache_creation_input_tokens: args.cacheWriteTokens,
|
|
189
221
|
$ai_request_count: 1,
|
|
190
222
|
$ai_total_cost_usd: costUsd,
|
|
223
|
+
$ai_input: args.aiInput,
|
|
224
|
+
$ai_output_choices: args.aiOutputChoices,
|
|
225
|
+
$ai_tools: args.aiTools,
|
|
226
|
+
$ai_input_truncated: args.aiInputTruncated || undefined,
|
|
227
|
+
$ai_output_truncated: args.aiOutputTruncated || undefined,
|
|
228
|
+
$ai_time_to_first_token: args.firstTokenMs,
|
|
229
|
+
$session_id: args.browserSessionId,
|
|
191
230
|
};
|
|
192
231
|
if (args.experimentAssignments?.length) {
|
|
193
232
|
properties.experiment_ids = args.experimentAssignments
|
|
@@ -220,6 +259,96 @@ function emitLlmGenerationTrackingEvent(args: {
|
|
|
220
259
|
}
|
|
221
260
|
}
|
|
222
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Build the PostHog content fields for a run's `$ai_generation`.
|
|
264
|
+
*
|
|
265
|
+
* This is one generation per run carrying the run's whole message list, not one
|
|
266
|
+
* per model round-trip: the engine layer reports aggregate usage only
|
|
267
|
+
* (`onUsage`) and exposes no per-step hook. Consequence to know when reading a
|
|
268
|
+
* trace — per-round-trip latency and intermediate assistant turns are not
|
|
269
|
+
* visible; a multi-step run collapses into a single generation node.
|
|
270
|
+
*
|
|
271
|
+
* `$ai_output_choices` is emitted whenever tool calls happened even with
|
|
272
|
+
* `capturePrompts` off, because PostHog derives `$ai_tools_called` /
|
|
273
|
+
* `$ai_tool_call_count` from tool-call blocks inside it and nothing else. The
|
|
274
|
+
* assistant's text content stays gated; only the structural call list ships.
|
|
275
|
+
*/
|
|
276
|
+
function buildGenerationContent(args: {
|
|
277
|
+
config: ObservabilityConfig;
|
|
278
|
+
messages: unknown;
|
|
279
|
+
tools: unknown;
|
|
280
|
+
assistantText: string;
|
|
281
|
+
toolSpans: TraceSpan[];
|
|
282
|
+
}): {
|
|
283
|
+
aiInput?: unknown;
|
|
284
|
+
aiOutputChoices?: unknown;
|
|
285
|
+
aiTools?: unknown;
|
|
286
|
+
aiInputTruncated?: boolean;
|
|
287
|
+
aiOutputTruncated?: boolean;
|
|
288
|
+
} {
|
|
289
|
+
const { config } = args;
|
|
290
|
+
|
|
291
|
+
const input = config.capturePrompts
|
|
292
|
+
? boundAiContent(redactSensitiveFields(args.messages))
|
|
293
|
+
: undefined;
|
|
294
|
+
|
|
295
|
+
const toolCalls = args.toolSpans
|
|
296
|
+
.slice(0, MAX_TRACKED_GENERATION_TOOL_CALLS)
|
|
297
|
+
.map((span) => ({
|
|
298
|
+
type: "function" as const,
|
|
299
|
+
id: span.id,
|
|
300
|
+
function: {
|
|
301
|
+
name: span.name,
|
|
302
|
+
// Already redacted at span construction, and only present when
|
|
303
|
+
// `captureToolArgs` is on.
|
|
304
|
+
...((span.metadata as { input?: unknown } | null)?.input !== undefined
|
|
305
|
+
? { arguments: (span.metadata as { input?: unknown }).input }
|
|
306
|
+
: {}),
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
const hasChoice = config.capturePrompts || toolCalls.length > 0;
|
|
311
|
+
const output = hasChoice
|
|
312
|
+
? boundAiContent([
|
|
313
|
+
{
|
|
314
|
+
role: "assistant",
|
|
315
|
+
...(config.capturePrompts
|
|
316
|
+
? { content: redactToolErrorMessage(args.assistantText) }
|
|
317
|
+
: {}),
|
|
318
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
319
|
+
},
|
|
320
|
+
])
|
|
321
|
+
: undefined;
|
|
322
|
+
|
|
323
|
+
const toolList = Array.isArray(args.tools)
|
|
324
|
+
? args.tools
|
|
325
|
+
.filter(
|
|
326
|
+
(tool): tool is { name: string; description?: string } =>
|
|
327
|
+
!!tool && typeof (tool as { name?: unknown }).name === "string",
|
|
328
|
+
)
|
|
329
|
+
.map((tool) => ({
|
|
330
|
+
type: "function" as const,
|
|
331
|
+
function: {
|
|
332
|
+
name: tool.name,
|
|
333
|
+
...(typeof tool.description === "string"
|
|
334
|
+
? { description: tool.description }
|
|
335
|
+
: {}),
|
|
336
|
+
},
|
|
337
|
+
}))
|
|
338
|
+
: [];
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
aiInput: input?.value,
|
|
342
|
+
aiOutputChoices: output?.value,
|
|
343
|
+
// Tool definitions are app configuration rather than user content, so they
|
|
344
|
+
// are not gated — without them a trace shows calls to tools nobody can
|
|
345
|
+
// identify. Schemas are excluded to keep the event small.
|
|
346
|
+
aiTools: toolList.length ? toolList : undefined,
|
|
347
|
+
aiInputTruncated: input?.truncated,
|
|
348
|
+
aiOutputTruncated: output?.truncated,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
223
352
|
/** Keys whose values are stripped from persisted tool inputs when
|
|
224
353
|
* `captureToolArgs` is enabled. Matched case-insensitively and tolerant
|
|
225
354
|
* of `_` / `-` separators. M14 in the MCP/A2A audit: tool calls
|
|
@@ -324,6 +453,15 @@ export async function instrumentAgentLoop(opts: {
|
|
|
324
453
|
};
|
|
325
454
|
/** Raw user-authored message before prompt/context enrichment. */
|
|
326
455
|
sentimentInput?: string;
|
|
456
|
+
/**
|
|
457
|
+
* Browser session id of the request that started this run, when it came from
|
|
458
|
+
* a page. Emitted as PostHog's `$session_id` so agent traces join to session
|
|
459
|
+
* replay — distinct from `$ai_session_id`, which is the thread.
|
|
460
|
+
*
|
|
461
|
+
* Defaults to the in-flight request context, which the agent-chat route
|
|
462
|
+
* populates from the `X-Agent-Native-Session-Id` header.
|
|
463
|
+
*/
|
|
464
|
+
browserSessionId?: string;
|
|
327
465
|
classifyError?: (error: unknown) =>
|
|
328
466
|
| {
|
|
329
467
|
status?: "success" | "error";
|
|
@@ -348,6 +486,11 @@ export async function instrumentAgentLoop(opts: {
|
|
|
348
486
|
.catch(() => null)
|
|
349
487
|
: Promise.resolve(null);
|
|
350
488
|
|
|
489
|
+
// Falls back to the in-flight request so callers deep in the agent stack
|
|
490
|
+
// don't have to thread it down by hand.
|
|
491
|
+
const browserSessionId =
|
|
492
|
+
opts.browserSessionId ?? getRequestContext()?.browserSessionId;
|
|
493
|
+
|
|
351
494
|
// Optional OpenTelemetry root span for this run. No-ops unless a host has
|
|
352
495
|
// installed `@opentelemetry/api` and registered a provider. The promise is
|
|
353
496
|
// resolved before the loop runs so child tool/model spans can parent under
|
|
@@ -389,6 +532,10 @@ export async function instrumentAgentLoop(opts: {
|
|
|
389
532
|
const toolNameToCounters = new Map<string, number[]>();
|
|
390
533
|
const toolCallIdToCounter = new Map<string, number>();
|
|
391
534
|
const generationToolCalls = new Map<number, GenerationToolCall>();
|
|
535
|
+
// Assistant text, accumulated only when prompt capture is on so a disabled
|
|
536
|
+
// config never holds message content in memory in the first place.
|
|
537
|
+
const assistantTextParts: string[] = [];
|
|
538
|
+
let assistantTextLength = 0;
|
|
392
539
|
|
|
393
540
|
let toolCallCount = 0;
|
|
394
541
|
let successfulTools = 0;
|
|
@@ -432,6 +579,14 @@ export async function instrumentAgentLoop(opts: {
|
|
|
432
579
|
|
|
433
580
|
const instrumentedSend = (event: AgentChatEvent): void => {
|
|
434
581
|
try {
|
|
582
|
+
if (
|
|
583
|
+
config.capturePrompts &&
|
|
584
|
+
event.type === "text" &&
|
|
585
|
+
assistantTextLength < MAX_AI_CONTENT_BYTES
|
|
586
|
+
) {
|
|
587
|
+
assistantTextParts.push(event.text);
|
|
588
|
+
assistantTextLength += event.text.length;
|
|
589
|
+
}
|
|
435
590
|
// Some guardrails intentionally stop the loop by emitting a terminal
|
|
436
591
|
// event and returning usage instead of throwing. Preserve that terminal
|
|
437
592
|
// state in telemetry so a tripwire/loop-limit/provider error cannot be
|
|
@@ -741,6 +896,13 @@ export async function instrumentAgentLoop(opts: {
|
|
|
741
896
|
? Math.max(0, usage.firstEngineEventAtMs - runStart)
|
|
742
897
|
: undefined;
|
|
743
898
|
const llmSpanId = spanId();
|
|
899
|
+
const generationContent = buildGenerationContent({
|
|
900
|
+
config,
|
|
901
|
+
messages: loopOpts.messages,
|
|
902
|
+
tools: loopOpts.tools,
|
|
903
|
+
assistantText: assistantTextParts.join(""),
|
|
904
|
+
toolSpans: spans.filter((s) => s.spanType === "tool_call"),
|
|
905
|
+
});
|
|
744
906
|
const llmSpan: TraceSpan = {
|
|
745
907
|
id: llmSpanId,
|
|
746
908
|
runId,
|
|
@@ -798,6 +960,8 @@ export async function instrumentAgentLoop(opts: {
|
|
|
798
960
|
createdAt: runStart,
|
|
799
961
|
experimentAssignments: opts.experimentAssignments,
|
|
800
962
|
modelSelectionSource: opts.modelSelectionSource,
|
|
963
|
+
browserSessionId,
|
|
964
|
+
...generationContent,
|
|
801
965
|
});
|
|
802
966
|
}
|
|
803
967
|
|
|
@@ -822,6 +986,117 @@ export async function instrumentAgentLoop(opts: {
|
|
|
822
986
|
};
|
|
823
987
|
spans.push(parentSpan);
|
|
824
988
|
|
|
989
|
+
// PostHog LLM analytics: the run is a `$ai_trace`, each tool call an
|
|
990
|
+
// `$ai_span` under it. Emitted from the collected spans rather than from a
|
|
991
|
+
// second instrumentation pass, so the tree PostHog shows and the tree we
|
|
992
|
+
// persist cannot drift apart.
|
|
993
|
+
try {
|
|
994
|
+
const aiError =
|
|
995
|
+
runStatus === "error"
|
|
996
|
+
? toAiErrorDetail(errorMessage, {
|
|
997
|
+
state: terminalOutcome?.state,
|
|
998
|
+
code:
|
|
999
|
+
terminalOutcome?.state === "failed" ||
|
|
1000
|
+
terminalOutcome?.state === "input_required"
|
|
1001
|
+
? terminalOutcome.code
|
|
1002
|
+
: undefined,
|
|
1003
|
+
retryable:
|
|
1004
|
+
terminalOutcome?.state === "failed"
|
|
1005
|
+
? terminalOutcome.retryable
|
|
1006
|
+
: undefined,
|
|
1007
|
+
})
|
|
1008
|
+
: undefined;
|
|
1009
|
+
const provider = llmProviderFromEngine(
|
|
1010
|
+
typeof loopOpts.engine?.name === "string"
|
|
1011
|
+
? loopOpts.engine.name
|
|
1012
|
+
: undefined,
|
|
1013
|
+
usage?.model ?? loopOpts.model,
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
const toolSpans = config.captureLlmSpans
|
|
1017
|
+
? spans.filter((s) => s.spanType === "tool_call")
|
|
1018
|
+
: [];
|
|
1019
|
+
const emittedToolSpans = toolSpans.slice(0, MAX_AI_SPANS_PER_RUN);
|
|
1020
|
+
const droppedToolSpans = toolSpans.length - emittedToolSpans.length;
|
|
1021
|
+
|
|
1022
|
+
emitAiTraceEvent({
|
|
1023
|
+
runId,
|
|
1024
|
+
threadId,
|
|
1025
|
+
userId,
|
|
1026
|
+
spanName: "agent_run",
|
|
1027
|
+
model: usage?.model ?? loopOpts.model,
|
|
1028
|
+
provider,
|
|
1029
|
+
latencySeconds: Math.round(totalDurationMs) / 1000,
|
|
1030
|
+
isError: runStatus === "error",
|
|
1031
|
+
error: aiError,
|
|
1032
|
+
inputTokens: usage?.usageReported ? usage.inputTokens : undefined,
|
|
1033
|
+
outputTokens: usage?.usageReported ? usage.outputTokens : undefined,
|
|
1034
|
+
costUsd: usage?.usageReported
|
|
1035
|
+
? costUsdFromCenticents(costCentsX100)
|
|
1036
|
+
: undefined,
|
|
1037
|
+
createdAt: runStart,
|
|
1038
|
+
browserSessionId,
|
|
1039
|
+
extraProperties: {
|
|
1040
|
+
...trackingIdentityProperties(),
|
|
1041
|
+
source: "agent_observability",
|
|
1042
|
+
run_id: runId,
|
|
1043
|
+
thread_id: threadId,
|
|
1044
|
+
// A truncated run must not read as a complete one.
|
|
1045
|
+
...(droppedToolSpans > 0
|
|
1046
|
+
? {
|
|
1047
|
+
$ai_spans_dropped: droppedToolSpans,
|
|
1048
|
+
$ai_spans_emitted: emittedToolSpans.length,
|
|
1049
|
+
}
|
|
1050
|
+
: {}),
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
for (const span of emittedToolSpans) {
|
|
1055
|
+
// `span.errorMessage` is the raw tool result. It routinely contains
|
|
1056
|
+
// upstream response bodies with Authorization headers and standalone
|
|
1057
|
+
// API keys, so it gets the same redaction + bounding the generation
|
|
1058
|
+
// event's `tools[].error_message` already applies, and the same
|
|
1059
|
+
// `captureToolResults` gate — exporting it here otherwise reintroduced
|
|
1060
|
+
// the leak that gate exists to prevent. `$ai_is_error` still marks the
|
|
1061
|
+
// failure when the content is withheld.
|
|
1062
|
+
const toolErrorMessage =
|
|
1063
|
+
span.status === "error" &&
|
|
1064
|
+
span.errorMessage &&
|
|
1065
|
+
config.captureToolResults
|
|
1066
|
+
? truncateToolErrorMessage(
|
|
1067
|
+
redactToolErrorMessage(span.errorMessage),
|
|
1068
|
+
)
|
|
1069
|
+
: undefined;
|
|
1070
|
+
|
|
1071
|
+
emitAiSpanEvent({
|
|
1072
|
+
runId,
|
|
1073
|
+
threadId,
|
|
1074
|
+
userId,
|
|
1075
|
+
spanId: span.id,
|
|
1076
|
+
spanName: span.name,
|
|
1077
|
+
latencySeconds: Math.round(span.durationMs) / 1000,
|
|
1078
|
+
isError: span.status === "error",
|
|
1079
|
+
error: toolErrorMessage
|
|
1080
|
+
? toAiErrorDetail(toolErrorMessage)
|
|
1081
|
+
: undefined,
|
|
1082
|
+
createdAt: span.createdAt,
|
|
1083
|
+
browserSessionId,
|
|
1084
|
+
// `metadata.input` is already redacted and only present when
|
|
1085
|
+
// `captureToolArgs` is on; absent stays absent.
|
|
1086
|
+
inputState: (span.metadata as { input?: unknown } | null)?.input,
|
|
1087
|
+
outputState: toolErrorMessage,
|
|
1088
|
+
extraProperties: {
|
|
1089
|
+
...trackingIdentityProperties(),
|
|
1090
|
+
source: "agent_observability",
|
|
1091
|
+
span_type: "tool_call",
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
// coercion-ok: a throw here would skip trace persistence below
|
|
1096
|
+
} catch {
|
|
1097
|
+
// LLM analytics must never affect the run or trace persistence.
|
|
1098
|
+
}
|
|
1099
|
+
|
|
825
1100
|
const summary: TraceSummary = {
|
|
826
1101
|
runId,
|
|
827
1102
|
threadId,
|
|
@@ -175,9 +175,19 @@ export interface ExperimentMetricResult {
|
|
|
175
175
|
|
|
176
176
|
export interface ObservabilityConfig {
|
|
177
177
|
enabled: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Export prompt and completion content (`$ai_input`, `$ai_output_choices`)
|
|
180
|
+
* to configured LLM-analytics backends. Off by default: message bodies are
|
|
181
|
+
* user data, and a trace backend is not a place to put it without a decision.
|
|
182
|
+
*
|
|
183
|
+
* When off the fields are OMITTED, never sent empty — an empty array is
|
|
184
|
+
* indistinguishable from a genuinely empty prompt.
|
|
185
|
+
*/
|
|
178
186
|
capturePrompts: boolean;
|
|
179
187
|
captureToolArgs: boolean;
|
|
180
188
|
captureToolResults: boolean;
|
|
189
|
+
/** Emit one `$ai_span` per tool call alongside the run's `$ai_trace`. */
|
|
190
|
+
captureLlmSpans: boolean;
|
|
181
191
|
evalSampleRate: number;
|
|
182
192
|
/**
|
|
183
193
|
* Classify the raw user message as positive, negative, or neutral. Off by
|
|
@@ -203,6 +213,7 @@ export const DEFAULT_OBSERVABILITY_CONFIG: ObservabilityConfig = {
|
|
|
203
213
|
capturePrompts: false,
|
|
204
214
|
captureToolArgs: false,
|
|
205
215
|
captureToolResults: false,
|
|
216
|
+
captureLlmSpans: true,
|
|
206
217
|
evalSampleRate: 0,
|
|
207
218
|
inferredSentimentEnabled: false,
|
|
208
219
|
inferredSentimentSampleRate: 0,
|
|
@@ -140,6 +140,22 @@ export function registerFrameworkSecrets(): void {
|
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// PostHog — product analytics, error tracking, and LLM analytics. One key
|
|
144
|
+
// arms all three; `POSTHOG_ERROR_TRACKING=false` opts out of exceptions
|
|
145
|
+
// while keeping analytics.
|
|
146
|
+
if (!getRequiredSecret("POSTHOG_API_KEY")) {
|
|
147
|
+
registerRequiredSecret({
|
|
148
|
+
key: "POSTHOG_API_KEY",
|
|
149
|
+
label: "PostHog project API key",
|
|
150
|
+
description:
|
|
151
|
+
"Sends product analytics, server exceptions, and LLM traces to PostHog. Set POSTHOG_HOST for self-hosted or EU projects.",
|
|
152
|
+
docsUrl: "https://posthog.com/docs/getting-started/install",
|
|
153
|
+
scope: "workspace",
|
|
154
|
+
kind: "api-key",
|
|
155
|
+
required: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
// Web-search tool backends — optional; the tool selects the first
|
|
144
160
|
// configured manual key at call time, then falls back to Builder Connect.
|
|
145
161
|
const webSearchKeys: Array<{
|
|
@@ -78,6 +78,25 @@ export function readAgentRunTimezone(event: H3Event): string | undefined {
|
|
|
78
78
|
: undefined;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The caller's browser analytics session id, when the page sent one.
|
|
83
|
+
*
|
|
84
|
+
* Emitted as PostHog's `$session_id` on the run's `$ai_*` events so an agent
|
|
85
|
+
* trace joins to the session replay it happened in. Distinct from
|
|
86
|
+
* `$ai_session_id`, which is the conversation thread.
|
|
87
|
+
*/
|
|
88
|
+
export function readAgentRunBrowserSessionId(
|
|
89
|
+
event: H3Event,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
const raw = readHeaderValue(event, "x-agent-native-session-id");
|
|
92
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
93
|
+
return typeof value === "string" &&
|
|
94
|
+
value.trim().length > 0 &&
|
|
95
|
+
value.trim().length < 128
|
|
96
|
+
? value.trim()
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
export function seedAgentRunOwnerContext(
|
|
82
101
|
event: H3Event,
|
|
83
102
|
ownerContext: AgentRunOwnerContext,
|
|
@@ -186,6 +205,7 @@ export async function resolveAgentRunRequestContext(options: {
|
|
|
186
205
|
}): Promise<RequestContext> {
|
|
187
206
|
const orgId = await resolveAgentRunOrgId(options);
|
|
188
207
|
const timezone = readAgentRunTimezone(options.event);
|
|
208
|
+
const browserSessionId = readAgentRunBrowserSessionId(options.event);
|
|
189
209
|
const waitUntil = requestWaitUntil(options.event);
|
|
190
210
|
const run = {
|
|
191
211
|
...(options.isBackgroundWorker ? { isBackgroundWorker: true } : {}),
|
|
@@ -196,6 +216,7 @@ export async function resolveAgentRunRequestContext(options: {
|
|
|
196
216
|
userName: options.ownerContext.name,
|
|
197
217
|
orgId,
|
|
198
218
|
timezone,
|
|
219
|
+
...(browserSessionId ? { browserSessionId } : {}),
|
|
199
220
|
...(Object.keys(run).length > 0 ? { run } : {}),
|
|
200
221
|
};
|
|
201
222
|
}
|
|
@@ -11,6 +11,14 @@ export interface CaptureErrorContext {
|
|
|
11
11
|
extra?: Record<string, unknown>;
|
|
12
12
|
/** Grouped diagnostic cards shown on the captured event. */
|
|
13
13
|
contexts?: Record<string, Record<string, unknown>>;
|
|
14
|
+
/**
|
|
15
|
+
* The agent run this error belongs to, when it belongs to one.
|
|
16
|
+
*
|
|
17
|
+
* Emitted as a top-level `$ai_trace_id` so an error-tracking issue and the
|
|
18
|
+
* LLM trace it came from resolve to each other. Nesting the run id inside
|
|
19
|
+
* `extra` (as callers did before) puts it somewhere no backend can join on.
|
|
20
|
+
*/
|
|
21
|
+
aiTraceId?: string;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export type CaptureErrorProvider = (
|
|
@@ -157,7 +157,12 @@ import {
|
|
|
157
157
|
readDeployCredentialEnv,
|
|
158
158
|
resolveSecret,
|
|
159
159
|
} from "./credential-provider.js";
|
|
160
|
+
import {
|
|
161
|
+
resolveDeployEnvironment,
|
|
162
|
+
resolveServerRelease,
|
|
163
|
+
} from "./deploy-environment.js";
|
|
160
164
|
import { createEmbedStartRouteHandler } from "./embed-route.js";
|
|
165
|
+
import { shouldReportError } from "./error-noise-filter.js";
|
|
161
166
|
import {
|
|
162
167
|
getH3App,
|
|
163
168
|
awaitBootstrap,
|
|
@@ -178,7 +183,11 @@ import { createOpenRouteHandler } from "./open-route.js";
|
|
|
178
183
|
import { createPollEventsHandler } from "./poll-events.js";
|
|
179
184
|
import { createPollHandler } from "./poll.js";
|
|
180
185
|
import { createRealtimeTokenHandler } from "./realtime-token.js";
|
|
181
|
-
import {
|
|
186
|
+
import {
|
|
187
|
+
getRequestContext,
|
|
188
|
+
hasRequestContext,
|
|
189
|
+
runWithRequestContext,
|
|
190
|
+
} from "./request-context.js";
|
|
182
191
|
import {
|
|
183
192
|
findUnsupportedScopedKeyNames,
|
|
184
193
|
saveKeyValuesToScopedSecrets,
|
|
@@ -1298,6 +1307,53 @@ export async function readLegacyCoreRouteInitSettings(
|
|
|
1298
1307
|
* PUT /_agent-native/application-state/compose/:id — upsert compose draft
|
|
1299
1308
|
* DELETE /_agent-native/application-state/compose/:id — delete compose draft
|
|
1300
1309
|
*/
|
|
1310
|
+
/**
|
|
1311
|
+
* Route every Nitro route error through the provider-agnostic `captureError()`
|
|
1312
|
+
* registry, filtered by the shared noise rules.
|
|
1313
|
+
*
|
|
1314
|
+
* This lives here rather than in `sentry-plugin.ts` because that plugin bails
|
|
1315
|
+
* out when no `SENTRY_DSN` is configured — wiring the hook there meant an app
|
|
1316
|
+
* running PostHog (or any other backend) with no Sentry project reported no
|
|
1317
|
+
* route errors at all, while still looking configured.
|
|
1318
|
+
*/
|
|
1319
|
+
function wireRouteErrorCapture(nitroApp: any): void {
|
|
1320
|
+
nitroApp.hooks?.hook?.(
|
|
1321
|
+
"error",
|
|
1322
|
+
(error: unknown, ctx?: { event?: H3Event }) => {
|
|
1323
|
+
try {
|
|
1324
|
+
const event = ctx?.event;
|
|
1325
|
+
const route = (() => {
|
|
1326
|
+
try {
|
|
1327
|
+
return event?.url?.pathname;
|
|
1328
|
+
// coercion-ok: a missing route tag must not suppress the error
|
|
1329
|
+
} catch {
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
})();
|
|
1333
|
+
const userAgent = (() => {
|
|
1334
|
+
try {
|
|
1335
|
+
return event ? getHeader(event, "user-agent") : undefined;
|
|
1336
|
+
// coercion-ok: a missing UA tag must not suppress the error
|
|
1337
|
+
} catch {
|
|
1338
|
+
return undefined;
|
|
1339
|
+
}
|
|
1340
|
+
})();
|
|
1341
|
+
|
|
1342
|
+
if (!shouldReportError(error, { tags: { route } })) return;
|
|
1343
|
+
|
|
1344
|
+
captureError(error, {
|
|
1345
|
+
route,
|
|
1346
|
+
method: event ? getMethod(event) : undefined,
|
|
1347
|
+
userAgent,
|
|
1348
|
+
});
|
|
1349
|
+
// coercion-ok: rethrowing here would replace the app's real error
|
|
1350
|
+
} catch {
|
|
1351
|
+
// Error reporting must never escape into Nitro's error path.
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1301
1357
|
export function createCoreRoutesPlugin(
|
|
1302
1358
|
options: CoreRoutesPluginOptions = {},
|
|
1303
1359
|
): NitroPluginDef {
|
|
@@ -1375,14 +1431,29 @@ export function createCoreRoutesPlugin(
|
|
|
1375
1431
|
// already registered the same key win.
|
|
1376
1432
|
registerFrameworkSecrets();
|
|
1377
1433
|
registerBuiltinProviders();
|
|
1378
|
-
|
|
1434
|
+
// Named for the destination it actually reaches: every configured
|
|
1435
|
+
// tracking provider (PostHog, Mixpanel, Amplitude, Agent Native
|
|
1436
|
+
// Analytics, webhook), not just one of them.
|
|
1437
|
+
registerErrorCaptureProvider("tracking", (error, context) => {
|
|
1438
|
+
// Attribute to the in-flight request's user so server exceptions and
|
|
1439
|
+
// that same person's browser events share one `distinct_id`.
|
|
1440
|
+
const requestContext = hasRequestContext()
|
|
1441
|
+
? getRequestContext()
|
|
1442
|
+
: undefined;
|
|
1379
1443
|
captureException(error, {
|
|
1380
1444
|
...context,
|
|
1381
1445
|
handled: false,
|
|
1382
1446
|
runtime: "node",
|
|
1383
1447
|
source: "server",
|
|
1384
|
-
|
|
1385
|
-
|
|
1448
|
+
release: resolveServerRelease(),
|
|
1449
|
+
environment: resolveDeployEnvironment(),
|
|
1450
|
+
...(requestContext?.userEmail
|
|
1451
|
+
? { userId: requestContext.userEmail }
|
|
1452
|
+
: {}),
|
|
1453
|
+
...(requestContext?.orgId ? { orgId: requestContext.orgId } : {}),
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
wireRouteErrorCapture(nitroApp);
|
|
1386
1457
|
registerBuiltinNotificationChannels();
|
|
1387
1458
|
|
|
1388
1459
|
try {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment identity shared by every error-reporting backend.
|
|
3
|
+
*
|
|
4
|
+
* These were originally Sentry-private helpers. They describe the deployment,
|
|
5
|
+
* not the vendor, and a second backend that reports errors without them
|
|
6
|
+
* produces issues nobody can tie to a release.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
function firstNonEmpty(
|
|
14
|
+
...values: Array<string | undefined>
|
|
15
|
+
): string | undefined {
|
|
16
|
+
for (const value of values) {
|
|
17
|
+
const trimmed = value?.trim();
|
|
18
|
+
if (trimmed) return trimmed;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The deploy environment name, e.g. `production`, `deploy-preview`. */
|
|
24
|
+
export function resolveDeployEnvironment(): string {
|
|
25
|
+
return (
|
|
26
|
+
firstNonEmpty(
|
|
27
|
+
process.env.SENTRY_ENVIRONMENT,
|
|
28
|
+
process.env.NETLIFY_CONTEXT,
|
|
29
|
+
process.env.VERCEL_ENV,
|
|
30
|
+
process.env.NODE_ENV,
|
|
31
|
+
) ?? "production"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the agent-native version baked into core's package.json so the
|
|
37
|
+
* reported "release" reflects the running framework version. Mirrors how the
|
|
38
|
+
* CLI computes `_version` — same dist layout, same fallback string. Guarded so
|
|
39
|
+
* a missing/unreadable package.json never crashes server boot.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveServerRelease(): string {
|
|
42
|
+
const explicit = process.env.AGENT_NATIVE_RELEASE;
|
|
43
|
+
if (explicit) return explicit;
|
|
44
|
+
try {
|
|
45
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
// dist/server/deploy-environment.js → ../../package.json
|
|
47
|
+
const pkgPath = path.resolve(here, "../../package.json");
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
|
|
49
|
+
version?: string;
|
|
50
|
+
};
|
|
51
|
+
if (pkg?.version) return `agent-native-server@${pkg.version}`;
|
|
52
|
+
// coercion-ok: falls through to the distinct "unknown" release marker
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore — fall through to "unknown"
|
|
55
|
+
}
|
|
56
|
+
return "agent-native-server@unknown";
|
|
57
|
+
}
|