@agent-native/core 0.133.2 → 0.134.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 +62 -0
- package/corpus/core/docs/content/observability.mdx +41 -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/client/use-action.ts +6 -0
- 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/content/actions/_batch-utils.ts +33 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
- package/corpus/templates/content/actions/_content-files.ts +31 -21
- package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
- package/corpus/templates/content/actions/_database-utils.ts +535 -41
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
- package/corpus/templates/content/actions/add-database-item.ts +15 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
- package/corpus/templates/content/actions/delete-database-items.ts +8 -2
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
- package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
- package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
- package/corpus/templates/content/actions/get-content-database.ts +12 -64
- package/corpus/templates/content/actions/move-database-item.ts +4 -1
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
- package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
- package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
- package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
- package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
- package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
- package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
- package/corpus/templates/content/app/i18n-data.ts +10 -0
- package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +25 -0
- package/corpus/templates/content/shared/api.ts +54 -4
- package/corpus/templates/content/shared/database-query.ts +359 -0
- 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/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +6 -0
- package/dist/client/use-action.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 +3 -3
- 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 +1 -1
- 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/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-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/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/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/client/use-action.ts +6 -0
- 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
|
@@ -232,6 +232,7 @@ All tables are dialect-agnostic (SQLite + Postgres) and strictly additive.
|
|
|
232
232
|
| `packages/core/src/observability/types.ts` | Shared type definitions |
|
|
233
233
|
| `packages/core/src/observability/store.ts` | SQL tables + CRUD |
|
|
234
234
|
| `packages/core/src/observability/traces.ts` | Auto-instrumentation |
|
|
235
|
+
| `packages/core/src/observability/posthog-ai.ts` | `$ai_trace` / `$ai_span` / `survey sent` emission, content bounding, `$ai_error` |
|
|
235
236
|
| `packages/core/src/observability/feedback.ts` | Feedback + Frustration Index |
|
|
236
237
|
| `packages/core/src/observability/evals.ts` | Eval engine (3 layers) |
|
|
237
238
|
| `packages/core/src/observability/experiments.ts` | A/B testing system |
|
|
@@ -272,16 +273,17 @@ The loop emits `agent.run` (with `agent.run_id`, `agent.thread_id`, `agent.user_
|
|
|
272
273
|
|
|
273
274
|
## Tracking Bridge
|
|
274
275
|
|
|
275
|
-
Instrumented agent loops
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
`$ai_session_id
|
|
284
|
-
`$
|
|
276
|
+
Instrumented agent loops emit server-side tracking events for every run through
|
|
277
|
+
`track()` from `@agent-native/core/tracking`, so configured PostHog, Agent Native
|
|
278
|
+
Analytics, Mixpanel, Amplitude, and webhook providers receive them through the
|
|
279
|
+
same best-effort fan-out as other tracking events.
|
|
280
|
+
|
|
281
|
+
- Events: `$ai_trace` per run, `$ai_span` per tool call, and `$ai_generation`
|
|
282
|
+
per model call. Every node carries the run id as `$ai_trace_id` and links
|
|
283
|
+
upward through `$ai_parent_id` so a backend can rebuild the tree.
|
|
284
|
+
`$ai_session_id` is the thread; the browser session is separate and ships as
|
|
285
|
+
`$session_id`, read from `X-Agent-Native-Session-Id` via
|
|
286
|
+
`RequestContext.browserSessionId`. Emission lives in `posthog-ai.ts`.
|
|
285
287
|
- Agent Native Analytics shape: the same event lands in `analytics_events` with
|
|
286
288
|
mirrored query-friendly properties such as `run_id`, `thread_id`,
|
|
287
289
|
`cost_cents_x100`, `duration_ms`, `tool_calls`, `successful_tools`,
|
|
@@ -293,6 +295,30 @@ LLM generation:
|
|
|
293
295
|
`parent_run_id` when available. `parent_turn_id` is separate because one
|
|
294
296
|
logical turn may span multiple concrete runs.
|
|
295
297
|
|
|
298
|
+
Constraints that are not visible from the emit site:
|
|
299
|
+
|
|
300
|
+
- **One generation per run, not per model round-trip.** The engine layer reports
|
|
301
|
+
aggregate usage through `onUsage` and exposes no per-step hook, so a multi-step
|
|
302
|
+
run collapses into a single generation carrying the whole message list.
|
|
303
|
+
Per-round-trip latency and intermediate turns are unavailable without a new
|
|
304
|
+
seam in `ai-sdk-engine.ts` / `builder-engine.ts`. Do not describe the current
|
|
305
|
+
output as per-step.
|
|
306
|
+
- **Disabled capture omits the field rather than sending an empty one.** An
|
|
307
|
+
empty array is indistinguishable from a run that genuinely had no messages.
|
|
308
|
+
Truncated content is marked, and a run over the span cap stamps
|
|
309
|
+
`$ai_spans_dropped` — a truncated run must not read as a complete one.
|
|
310
|
+
- **The structural tool-call list ships even when content capture is off.**
|
|
311
|
+
Backends derive their tool tags from tool-call blocks inside the output
|
|
312
|
+
choices and from nothing else, so tool names (without arguments) are always
|
|
313
|
+
emitted. The parallel first-party `tools` array stays because the dashboards
|
|
314
|
+
read it; that duplication is deliberate, not cleanup.
|
|
315
|
+
- **Only thumbs carry `sentiment`.** All four feedback types are reported, but a
|
|
316
|
+
category follow-up to a thumbs-down is detail about the same vote — counting
|
|
317
|
+
it again inflates the metric.
|
|
318
|
+
- **Never invent an external id to make an integration light up.** Survey-based
|
|
319
|
+
feedback is emitted only when a real survey id is configured, and nothing is
|
|
320
|
+
sent otherwise.
|
|
321
|
+
|
|
296
322
|
Do not build a separate LLM-observability ingestion API unless there is a clear
|
|
297
323
|
reason the tracking provider registry cannot express the use case. Keep prompt,
|
|
298
324
|
tool input, and model output content out of tracking by default; use the existing
|
|
@@ -79,7 +79,7 @@ Set the env var and the provider auto-registers at startup. No SDK dependencies
|
|
|
79
79
|
|
|
80
80
|
| Provider | Env vars |
|
|
81
81
|
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
-
| PostHog | `POSTHOG_API_KEY` (required), `POSTHOG_HOST` (optional, defaults to `https://us.i.posthog.com`)
|
|
82
|
+
| PostHog | `POSTHOG_API_KEY` (required), `POSTHOG_HOST` (optional, defaults to `https://us.i.posthog.com`), `POSTHOG_ERROR_TRACKING=false` (optional opt-out) |
|
|
83
83
|
| Mixpanel | `MIXPANEL_TOKEN` |
|
|
84
84
|
| Amplitude | `AMPLITUDE_API_KEY` |
|
|
85
85
|
| Agent Native Analytics | `AGENT_NATIVE_ANALYTICS_PUBLIC_KEY` (server), `AGENT_NATIVE_ANALYTICS_ENDPOINT` (optional, defaults to `https://analytics.agent-native.com/track`) |
|
|
@@ -89,6 +89,56 @@ Multiple providers can be active simultaneously. All receive every event.
|
|
|
89
89
|
|
|
90
90
|
Browser-side `trackEvent()` also forwards to Agent Native Analytics when `VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY` is present. Use `VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT` to override the default browser endpoint. The built-in Agent Native Analytics sender is quiet on localhost/local dev by default; set `AGENT_NATIVE_ANALYTICS_ALLOW_LOCALHOST=true` only for an intentional local ingestion test.
|
|
91
91
|
|
|
92
|
+
## Error Capture
|
|
93
|
+
|
|
94
|
+
Exceptions fan out through `server/capture-error.ts` to every registered
|
|
95
|
+
backend — Sentry, PostHog, and the tracking providers — from one `captureError()`
|
|
96
|
+
call. Backends are additive: configuring a second one does not displace the
|
|
97
|
+
first, and no backend is required for the others to work.
|
|
98
|
+
|
|
99
|
+
Emit through `captureError()` / `captureException()`. Never hand-roll a
|
|
100
|
+
`track("$exception", …)`: each backend needs its own payload shape and the
|
|
101
|
+
providers build it.
|
|
102
|
+
|
|
103
|
+
- **Provider-agnostic wiring must not live in a provider plugin.** The Nitro
|
|
104
|
+
`error` hook is in `core-routes-plugin.ts`, not `sentry-plugin.ts`, because
|
|
105
|
+
that plugin returns early when no `SENTRY_DSN` is set — hooking route errors
|
|
106
|
+
there meant an app on any other backend silently reported none.
|
|
107
|
+
- **Every backend applies `server/error-noise-filter.ts`.** It holds
|
|
108
|
+
production-tuned drop rules (expected 4xx, access-control rejections, Lambda
|
|
109
|
+
freeze/thaw `socket hang up`). A backend that skips it receives a firehose;
|
|
110
|
+
the `socket hang up` rule alone is ~10k events/day.
|
|
111
|
+
- **A backend can accept a malformed payload and still show a count.** PostHog
|
|
112
|
+
ingested the framework's camelCase `$exception` for a long time and rendered
|
|
113
|
+
empty, ungroupable issues — which reads as coverage, not as breakage. When
|
|
114
|
+
adding or changing a backend, check what an event looks like in its UI, not
|
|
115
|
+
just that the request returned 200.
|
|
116
|
+
- **Attribute the error.** Without a user id, server exceptions land under
|
|
117
|
+
`anonymous` and split one person in two against their browser events. Pass
|
|
118
|
+
`aiTraceId` for anything inside an agent run so the issue and the LLM trace
|
|
119
|
+
resolve to each other.
|
|
120
|
+
|
|
121
|
+
Symbolication is per-backend and not automatic: the framework uploads no source
|
|
122
|
+
maps to PostHog, so minified browser stacks stay minified there. Known gap, not
|
|
123
|
+
a bug to re-diagnose.
|
|
124
|
+
|
|
125
|
+
### Browser keys and the SSR shell
|
|
126
|
+
|
|
127
|
+
Public keys (`POSTHOG_PUBLIC_KEY`, the Sentry client DSN) ship inside the
|
|
128
|
+
CDN-cached SSR shell — publishable and identical for every visitor. Server keys
|
|
129
|
+
never do, and are never a fallback for a public one: `POSTHOG_API_KEY` may be a
|
|
130
|
+
private key and this value lands in public HTML.
|
|
131
|
+
|
|
132
|
+
Browser errors post directly to the backend rather than through
|
|
133
|
+
`/_agent-native/track`, because that route requires a resolved session and
|
|
134
|
+
relaying would drop every signed-out crash.
|
|
135
|
+
|
|
136
|
+
When adding a client config field, update **both** `server/posthog-config.ts`
|
|
137
|
+
and the mirrored worker emitter in `deploy/build.ts` — the worker bundles a
|
|
138
|
+
string copy and cannot import the module, so a one-sided edit drops the config
|
|
139
|
+
silently in deployed builds. `posthog-config.spec.ts` pins the two outputs
|
|
140
|
+
together.
|
|
141
|
+
|
|
92
142
|
## Default Baseline Events
|
|
93
143
|
|
|
94
144
|
Template roots call `configureTracking()` once during app startup. That installs default browser pageview tracking for hosted apps:
|
|
@@ -199,6 +249,10 @@ interface TrackingEvent {
|
|
|
199
249
|
| `packages/core/src/tracking/registry.ts` | `track()`, `identify()`, `registerTrackingProvider()`, `flushTracking()` |
|
|
200
250
|
| `packages/core/src/tracking/providers.ts` | Built-in providers (PostHog, Mixpanel, Amplitude, Agent Native Analytics, Webhook) and `registerBuiltinProviders()` |
|
|
201
251
|
| `packages/core/src/tracking/types.ts` | `TrackingEvent` and `TrackingProvider` interfaces |
|
|
252
|
+
| `packages/core/src/tracking/posthog-exception.ts` | `$exception_list` builder + stack-frame parser (isomorphic: server and browser) |
|
|
253
|
+
| `packages/core/src/tracking/redaction.ts` | Shared bounding/redaction helpers used by every exception emitter |
|
|
254
|
+
| `packages/core/src/server/error-noise-filter.ts` | Provider-agnostic drop rules, applied by both Sentry `beforeSend` and the route error hook |
|
|
255
|
+
| `packages/core/src/server/posthog-config.ts` | Public browser PostHog config (mirrored in `deploy/build.ts`) |
|
|
202
256
|
|
|
203
257
|
## Related Skills
|
|
204
258
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { trackingIdentityProperties } from "../observability/tracking-identity.js";
|
|
2
2
|
import type { CaptureErrorContext } from "../server/capture-error.js";
|
|
3
|
+
import {
|
|
4
|
+
boundedText,
|
|
5
|
+
exceptionParts,
|
|
6
|
+
safeTags,
|
|
7
|
+
safeValue,
|
|
8
|
+
} from "./redaction.js";
|
|
3
9
|
import { track } from "./registry.js";
|
|
4
10
|
|
|
5
11
|
export type TrackingExceptionLevel =
|
|
@@ -17,95 +23,13 @@ export interface TrackingExceptionContext extends CaptureErrorContext {
|
|
|
17
23
|
environment?: string;
|
|
18
24
|
runtime?: "node" | "cli";
|
|
19
25
|
source?: "server" | "cli";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const SECRET_RE = /\b(?:bearer|basic)\s+[^\s]+/gi;
|
|
28
|
-
const SECRET_KEY_RE =
|
|
29
|
-
/(?:authorization|cookie|set[-_]?cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)/i;
|
|
30
|
-
|
|
31
|
-
function redact(value: string): string {
|
|
32
|
-
return value
|
|
33
|
-
.replace(SECRET_RE, (match) => `${match.split(/\s+/, 1)[0]} <redacted>`)
|
|
34
|
-
.replace(
|
|
35
|
-
/([A-Za-z0-9_$.-]*(?:authorization|cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)[A-Za-z0-9_$.-]*\s*[:=]\s*)([^\s,;}]+)/gi,
|
|
36
|
-
"$1<redacted>",
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function boundedText(value: unknown, max: number): string {
|
|
41
|
-
const text = typeof value === "string" ? value : String(value ?? "");
|
|
42
|
-
const safe = redact(text);
|
|
43
|
-
return safe.length > max ? safe.slice(0, max) : safe;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function safeValue(value: unknown, depth = 2): unknown {
|
|
47
|
-
if (
|
|
48
|
-
value == null ||
|
|
49
|
-
typeof value === "boolean" ||
|
|
50
|
-
typeof value === "number"
|
|
51
|
-
) {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
if (typeof value === "string")
|
|
55
|
-
return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
|
|
56
|
-
if (depth <= 0) return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
|
|
57
|
-
if (Array.isArray(value)) {
|
|
58
|
-
return value.slice(0, 20).map((item) => safeValue(item, depth - 1));
|
|
59
|
-
}
|
|
60
|
-
if (typeof value === "object") {
|
|
61
|
-
const out: Record<string, unknown> = {};
|
|
62
|
-
for (const [key, child] of Object.entries(value)) {
|
|
63
|
-
if (Object.keys(out).length >= MAX_EXTRA_KEYS) break;
|
|
64
|
-
const safeKey = boundedText(key, 100);
|
|
65
|
-
out[safeKey] = SECRET_KEY_RE.test(safeKey)
|
|
66
|
-
? "<redacted>"
|
|
67
|
-
: safeValue(child, depth - 1);
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function exceptionParts(error: unknown): {
|
|
75
|
-
type: string;
|
|
76
|
-
message: string;
|
|
77
|
-
stack?: string;
|
|
78
|
-
} {
|
|
79
|
-
if (error instanceof Error) {
|
|
80
|
-
return {
|
|
81
|
-
type: boundedText(error.name || "Error", 200),
|
|
82
|
-
message: boundedText(
|
|
83
|
-
error.message || error.name || "Error",
|
|
84
|
-
MAX_MESSAGE_LENGTH,
|
|
85
|
-
),
|
|
86
|
-
...(error.stack
|
|
87
|
-
? { stack: boundedText(error.stack, MAX_STACK_LENGTH) }
|
|
88
|
-
: {}),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
type: "Error",
|
|
93
|
-
message: boundedText(error, MAX_MESSAGE_LENGTH),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function safeTags(
|
|
98
|
-
tags: Record<string, string | undefined> | undefined,
|
|
99
|
-
): Record<string, string> {
|
|
100
|
-
const out: Record<string, string> = {};
|
|
101
|
-
for (const [key, value] of Object.entries(tags ?? {})) {
|
|
102
|
-
if (Object.keys(out).length >= MAX_TAGS || value == null) break;
|
|
103
|
-
const safeKey = boundedText(key, 100);
|
|
104
|
-
out[safeKey] = SECRET_KEY_RE.test(safeKey)
|
|
105
|
-
? "<redacted>"
|
|
106
|
-
: boundedText(value, 200);
|
|
107
|
-
}
|
|
108
|
-
return out;
|
|
26
|
+
/**
|
|
27
|
+
* Who the exception is attributed to. Without it every server exception is
|
|
28
|
+
* ingested as `anonymous`, which splits one person into two in any backend
|
|
29
|
+
* that also receives their browser events.
|
|
30
|
+
*/
|
|
31
|
+
userId?: string;
|
|
32
|
+
orgId?: string;
|
|
109
33
|
}
|
|
110
34
|
|
|
111
35
|
/** Emit a bounded, redacted Node/CLI exception through first-party tracking. */
|
|
@@ -125,26 +49,37 @@ export function captureException(
|
|
|
125
49
|
...context.extra,
|
|
126
50
|
...(context.contexts ? { contexts: context.contexts } : {}),
|
|
127
51
|
});
|
|
128
|
-
track(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
? {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
52
|
+
track(
|
|
53
|
+
"$exception",
|
|
54
|
+
{
|
|
55
|
+
...trackingIdentityProperties(),
|
|
56
|
+
exceptionType: parts.type,
|
|
57
|
+
exceptionMessage: parts.message,
|
|
58
|
+
...(parts.stack ? { exceptionStack: parts.stack } : {}),
|
|
59
|
+
handled: context.handled ?? true,
|
|
60
|
+
level: context.level ?? "error",
|
|
61
|
+
occurredAt: new Date().toISOString(),
|
|
62
|
+
runtime: context.runtime ?? "node",
|
|
63
|
+
source: context.source ?? "server",
|
|
64
|
+
...(context.route ? { url: boundedText(context.route, 500) } : {}),
|
|
65
|
+
...(context.release
|
|
66
|
+
? { release: boundedText(context.release, 200) }
|
|
67
|
+
: {}),
|
|
68
|
+
...(context.environment
|
|
69
|
+
? { environment: boundedText(context.environment, 100) }
|
|
70
|
+
: {}),
|
|
71
|
+
...(context.orgId ? { orgId: boundedText(context.orgId, 200) } : {}),
|
|
72
|
+
// Top-level so error tracking and LLM analytics join on it.
|
|
73
|
+
...(context.aiTraceId
|
|
74
|
+
? { $ai_trace_id: boundedText(context.aiTraceId, 200) }
|
|
75
|
+
: {}),
|
|
76
|
+
...(Object.keys(tags).length ? { exceptionTags: tags } : {}),
|
|
77
|
+
...(extra && typeof extra === "object"
|
|
78
|
+
? { exceptionExtra: extra }
|
|
79
|
+
: {}),
|
|
80
|
+
},
|
|
81
|
+
context.userId ? { userId: context.userId } : undefined,
|
|
82
|
+
);
|
|
148
83
|
} catch {
|
|
149
84
|
// Error reporting must never mask the original failure.
|
|
150
85
|
}
|
|
@@ -12,4 +12,15 @@ export {
|
|
|
12
12
|
type TrackingExceptionContext,
|
|
13
13
|
type TrackingExceptionLevel,
|
|
14
14
|
} from "./error-capture.js";
|
|
15
|
+
export {
|
|
16
|
+
errorToPostHogExceptionProperties,
|
|
17
|
+
parseStackFrames,
|
|
18
|
+
reshapeTrackedExceptionProperties,
|
|
19
|
+
toPostHogExceptionProperties,
|
|
20
|
+
type PostHogExceptionEntry,
|
|
21
|
+
type PostHogExceptionInput,
|
|
22
|
+
type PostHogExceptionLevel,
|
|
23
|
+
type PostHogExceptionProperties,
|
|
24
|
+
type PostHogStackFrame,
|
|
25
|
+
} from "./posthog-exception.js";
|
|
15
26
|
export type { TrackingProvider, TrackingEvent } from "./types.js";
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build PostHog error-tracking payloads from a raw JS error or from the
|
|
3
|
+
* camelCase exception properties the framework's own `captureException()`
|
|
4
|
+
* emits.
|
|
5
|
+
*
|
|
6
|
+
* PostHog's error tracking only groups and renders an issue when the event
|
|
7
|
+
* carries `$exception_list` with per-frame stack data. An event named
|
|
8
|
+
* `$exception` without it is ingested and displayed as an empty, ungroupable
|
|
9
|
+
* issue — which is worse than no event, because the count looks like coverage.
|
|
10
|
+
*
|
|
11
|
+
* Frame parsing follows posthog-js (itself derived from Sentry's TraceKit
|
|
12
|
+
* fork), because PostHog's ingestion is written against that shape:
|
|
13
|
+
* - `Error: …` header lines are skipped, not parsed as frames
|
|
14
|
+
* - lines over 1 KB are skipped (the regexes backtrack)
|
|
15
|
+
* - frames are capped at 50 and reversed to oldest-call-first
|
|
16
|
+
* - an unresolvable function name is `?`, never empty
|
|
17
|
+
*
|
|
18
|
+
* Runs unchanged in Node and the browser: no `process`, no Node built-ins.
|
|
19
|
+
*
|
|
20
|
+
* @see https://posthog.com/docs/error-tracking/installation/manual
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
MAX_MESSAGE_LENGTH,
|
|
25
|
+
MAX_STACK_LENGTH,
|
|
26
|
+
boundedText,
|
|
27
|
+
exceptionParts,
|
|
28
|
+
} from "./redaction.js";
|
|
29
|
+
|
|
30
|
+
/** Matches posthog-js's `UNKNOWN_FUNCTION`. */
|
|
31
|
+
const UNKNOWN_FUNCTION = "?";
|
|
32
|
+
const STACKTRACE_FRAME_LIMIT = 50;
|
|
33
|
+
const MAX_STACK_LINE_LENGTH = 1024;
|
|
34
|
+
|
|
35
|
+
const ERROR_HEADER_RE = /\S*Error: /;
|
|
36
|
+
const WEBPACK_ERROR_RE = /\(error: (.*)\)/;
|
|
37
|
+
|
|
38
|
+
// " at fn (file:1:2)" / " at async Foo.bar (/app/x.js:2:3)" / " at /app/x.js:1:2"
|
|
39
|
+
const V8_FRAME_RE =
|
|
40
|
+
/^\s*at (?:async )?(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/;
|
|
41
|
+
// "fn@https://example.com/s.js:1:2" / "@https://example.com/s.js:1:2"
|
|
42
|
+
const GECKO_FRAME_RE = /^\s*(.*?)@(.+?)(?::(\d+))?(?::(\d+))?\s*$/;
|
|
43
|
+
|
|
44
|
+
export type PostHogExceptionLevel =
|
|
45
|
+
| "fatal"
|
|
46
|
+
| "error"
|
|
47
|
+
| "warning"
|
|
48
|
+
| "info"
|
|
49
|
+
| "debug";
|
|
50
|
+
|
|
51
|
+
export interface PostHogStackFrame {
|
|
52
|
+
/**
|
|
53
|
+
* Always `"custom"`. PostHog reserves the language-specific platforms for
|
|
54
|
+
* frames it will try to symbolicate against uploaded source maps; we upload
|
|
55
|
+
* none, so claiming one would render every minified frame as a failed
|
|
56
|
+
* resolution rather than as the raw frame it is.
|
|
57
|
+
*/
|
|
58
|
+
platform: "custom";
|
|
59
|
+
lang: string;
|
|
60
|
+
function: string;
|
|
61
|
+
filename?: string;
|
|
62
|
+
lineno?: number;
|
|
63
|
+
colno?: number;
|
|
64
|
+
in_app: boolean;
|
|
65
|
+
resolved: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PostHogExceptionEntry {
|
|
69
|
+
type: string;
|
|
70
|
+
value: string;
|
|
71
|
+
mechanism: { handled: boolean; synthetic: boolean; type?: string };
|
|
72
|
+
stacktrace?: { type: "raw"; frames: PostHogStackFrame[] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PostHogExceptionProperties {
|
|
76
|
+
$exception_list: PostHogExceptionEntry[];
|
|
77
|
+
$exception_level: PostHogExceptionLevel;
|
|
78
|
+
$exception_fingerprint?: string;
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PostHogExceptionInput {
|
|
83
|
+
/** Error class name, e.g. `TypeError`. */
|
|
84
|
+
type: string;
|
|
85
|
+
/** Error message. */
|
|
86
|
+
value: string;
|
|
87
|
+
/** Raw `error.stack` string, when available. */
|
|
88
|
+
stack?: string;
|
|
89
|
+
/** `false` for errors that crashed the request/page rather than being caught. */
|
|
90
|
+
handled?: boolean;
|
|
91
|
+
/** `true` when the framework synthesized the error from a non-Error throw. */
|
|
92
|
+
synthetic?: boolean;
|
|
93
|
+
/** Mechanism label, e.g. `onunhandledrejection`, `nitro.error`. */
|
|
94
|
+
mechanismType?: string;
|
|
95
|
+
level?: PostHogExceptionLevel;
|
|
96
|
+
/** Overrides PostHog's default grouping. */
|
|
97
|
+
fingerprint?: string;
|
|
98
|
+
/** Frame language tag. Defaults to `javascript`. */
|
|
99
|
+
lang?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isInAppFrame(filename: string | undefined): boolean {
|
|
103
|
+
if (!filename) return true;
|
|
104
|
+
return (
|
|
105
|
+
!filename.startsWith("node:") &&
|
|
106
|
+
!filename.includes("node_modules") &&
|
|
107
|
+
!filename.includes("/internal/") &&
|
|
108
|
+
filename !== "native"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makeFrame(
|
|
113
|
+
lang: string,
|
|
114
|
+
fn: string,
|
|
115
|
+
filename: string | undefined,
|
|
116
|
+
lineno: number | undefined,
|
|
117
|
+
colno: number | undefined,
|
|
118
|
+
): PostHogStackFrame {
|
|
119
|
+
const name = !fn || fn === "<anonymous>" ? UNKNOWN_FUNCTION : fn;
|
|
120
|
+
return {
|
|
121
|
+
platform: "custom",
|
|
122
|
+
lang,
|
|
123
|
+
function: name,
|
|
124
|
+
...(filename ? { filename } : {}),
|
|
125
|
+
...(lineno !== undefined && Number.isFinite(lineno) ? { lineno } : {}),
|
|
126
|
+
...(colno !== undefined && Number.isFinite(colno) ? { colno } : {}),
|
|
127
|
+
in_app: isInAppFrame(filename),
|
|
128
|
+
// Never `true`: we ship no source maps to PostHog, so a minified browser
|
|
129
|
+
// frame is exactly as informative as it looks. Claiming it is resolved
|
|
130
|
+
// would present a mangled name as the real one.
|
|
131
|
+
resolved: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseFrameLine(
|
|
136
|
+
line: string,
|
|
137
|
+
lang: string,
|
|
138
|
+
): PostHogStackFrame | undefined {
|
|
139
|
+
const v8 = V8_FRAME_RE.exec(line);
|
|
140
|
+
if (v8) {
|
|
141
|
+
const [, fn, file, lineNo, colNo, bare] = v8;
|
|
142
|
+
const filename = (file ?? bare)?.replace(/^file:\/\//, "");
|
|
143
|
+
return makeFrame(
|
|
144
|
+
lang,
|
|
145
|
+
fn ?? UNKNOWN_FUNCTION,
|
|
146
|
+
filename,
|
|
147
|
+
lineNo ? Number(lineNo) : undefined,
|
|
148
|
+
colNo ? Number(colNo) : undefined,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const gecko = GECKO_FRAME_RE.exec(line);
|
|
153
|
+
if (gecko) {
|
|
154
|
+
const [, fn, file, lineNo, colNo] = gecko;
|
|
155
|
+
return makeFrame(
|
|
156
|
+
lang,
|
|
157
|
+
fn || UNKNOWN_FUNCTION,
|
|
158
|
+
file,
|
|
159
|
+
lineNo ? Number(lineNo) : undefined,
|
|
160
|
+
colNo ? Number(colNo) : undefined,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse a `error.stack` string into PostHog stack frames, oldest call first.
|
|
169
|
+
*
|
|
170
|
+
* Returns an empty array when nothing parsed. Callers must treat that as
|
|
171
|
+
* "no frames" and omit `stacktrace` entirely rather than sending an empty
|
|
172
|
+
* frame list — PostHog renders the latter as a stack that exists and is empty.
|
|
173
|
+
*/
|
|
174
|
+
export function parseStackFrames(
|
|
175
|
+
stack: string | undefined,
|
|
176
|
+
lang = "javascript",
|
|
177
|
+
): PostHogStackFrame[] {
|
|
178
|
+
if (!stack) return [];
|
|
179
|
+
const frames: PostHogStackFrame[] = [];
|
|
180
|
+
|
|
181
|
+
for (const rawLine of stack.split("\n")) {
|
|
182
|
+
if (rawLine.length > MAX_STACK_LINE_LENGTH) continue;
|
|
183
|
+
const line = WEBPACK_ERROR_RE.test(rawLine)
|
|
184
|
+
? rawLine.replace(WEBPACK_ERROR_RE, "$1")
|
|
185
|
+
: rawLine;
|
|
186
|
+
if (ERROR_HEADER_RE.test(line)) continue;
|
|
187
|
+
|
|
188
|
+
const frame = parseFrameLine(line, lang);
|
|
189
|
+
if (frame) frames.push(frame);
|
|
190
|
+
if (frames.length >= STACKTRACE_FRAME_LIMIT) break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
frames.reverse();
|
|
194
|
+
return frames;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build the `$exception_*` properties for a PostHog error-tracking event.
|
|
199
|
+
*
|
|
200
|
+
* The caller merges these into the event properties alongside `distinct_id`
|
|
201
|
+
* and any app dimensions.
|
|
202
|
+
*/
|
|
203
|
+
export function toPostHogExceptionProperties(
|
|
204
|
+
input: PostHogExceptionInput,
|
|
205
|
+
): PostHogExceptionProperties {
|
|
206
|
+
const lang = input.lang ?? "javascript";
|
|
207
|
+
const frames = parseStackFrames(input.stack, lang);
|
|
208
|
+
const entry: PostHogExceptionEntry = {
|
|
209
|
+
type: boundedText(input.type || "Error", 200),
|
|
210
|
+
value: boundedText(
|
|
211
|
+
input.value || input.type || "Error",
|
|
212
|
+
MAX_MESSAGE_LENGTH,
|
|
213
|
+
),
|
|
214
|
+
mechanism: {
|
|
215
|
+
handled: input.handled ?? true,
|
|
216
|
+
synthetic: input.synthetic ?? false,
|
|
217
|
+
...(input.mechanismType
|
|
218
|
+
? { type: boundedText(input.mechanismType, 100) }
|
|
219
|
+
: {}),
|
|
220
|
+
},
|
|
221
|
+
...(frames.length ? { stacktrace: { type: "raw" as const, frames } } : {}),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
$exception_list: [entry],
|
|
226
|
+
$exception_level: input.level ?? "error",
|
|
227
|
+
...(input.fingerprint
|
|
228
|
+
? { $exception_fingerprint: boundedText(input.fingerprint, 200) }
|
|
229
|
+
: {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Build `$exception_*` properties directly from a thrown value. */
|
|
234
|
+
export function errorToPostHogExceptionProperties(
|
|
235
|
+
error: unknown,
|
|
236
|
+
options: Omit<PostHogExceptionInput, "type" | "value" | "stack"> = {},
|
|
237
|
+
): PostHogExceptionProperties {
|
|
238
|
+
const parts = exceptionParts(error);
|
|
239
|
+
return toPostHogExceptionProperties({
|
|
240
|
+
...options,
|
|
241
|
+
type: parts.type,
|
|
242
|
+
value: parts.message,
|
|
243
|
+
stack: parts.stack,
|
|
244
|
+
synthetic: options.synthetic ?? !(error instanceof Error),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reshape the camelCase properties emitted by `tracking/error-capture.ts` into
|
|
250
|
+
* PostHog's `$exception_list` form.
|
|
251
|
+
*
|
|
252
|
+
* Returns `undefined` when the event carries no recognizable exception fields,
|
|
253
|
+
* so the caller can pass it through untouched instead of inventing an empty
|
|
254
|
+
* issue out of an unrelated event that happens to be named `$exception`.
|
|
255
|
+
*/
|
|
256
|
+
export function reshapeTrackedExceptionProperties(
|
|
257
|
+
properties: Record<string, unknown> | undefined,
|
|
258
|
+
): Record<string, unknown> | undefined {
|
|
259
|
+
if (!properties) return undefined;
|
|
260
|
+
// Already in PostHog form (e.g. relayed from the browser) — leave it alone.
|
|
261
|
+
if (Array.isArray(properties.$exception_list)) return properties;
|
|
262
|
+
|
|
263
|
+
const type = properties.exceptionType;
|
|
264
|
+
const message = properties.exceptionMessage;
|
|
265
|
+
if (typeof type !== "string" && typeof message !== "string") {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const stack =
|
|
270
|
+
typeof properties.exceptionStack === "string"
|
|
271
|
+
? properties.exceptionStack.slice(0, MAX_STACK_LENGTH)
|
|
272
|
+
: undefined;
|
|
273
|
+
const level = properties.level;
|
|
274
|
+
|
|
275
|
+
const {
|
|
276
|
+
exceptionType: _type,
|
|
277
|
+
exceptionMessage: _message,
|
|
278
|
+
exceptionStack: _stack,
|
|
279
|
+
handled: _handled,
|
|
280
|
+
level: _level,
|
|
281
|
+
...rest
|
|
282
|
+
} = properties;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...rest,
|
|
286
|
+
...toPostHogExceptionProperties({
|
|
287
|
+
type: typeof type === "string" ? type : "Error",
|
|
288
|
+
value: typeof message === "string" ? message : "Error",
|
|
289
|
+
stack,
|
|
290
|
+
handled:
|
|
291
|
+
typeof properties.handled === "boolean" ? properties.handled : true,
|
|
292
|
+
level: isExceptionLevel(level) ? level : "error",
|
|
293
|
+
}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isExceptionLevel(value: unknown): value is PostHogExceptionLevel {
|
|
298
|
+
return (
|
|
299
|
+
value === "fatal" ||
|
|
300
|
+
value === "error" ||
|
|
301
|
+
value === "warning" ||
|
|
302
|
+
value === "info" ||
|
|
303
|
+
value === "debug"
|
|
304
|
+
);
|
|
305
|
+
}
|