@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.
Files changed (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +62 -0
  3. package/corpus/core/docs/content/observability.mdx +41 -1
  4. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  5. package/corpus/core/docs/content/tracking.mdx +25 -0
  6. package/corpus/core/package.json +1 -1
  7. package/corpus/core/src/agent/run-manager.ts +7 -0
  8. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  9. package/corpus/core/src/client/analytics.ts +105 -1
  10. package/corpus/core/src/deploy/build.ts +38 -1
  11. package/corpus/core/src/observability/posthog-ai.ts +294 -0
  12. package/corpus/core/src/observability/routes.ts +29 -6
  13. package/corpus/core/src/observability/traces.ts +287 -12
  14. package/corpus/core/src/observability/types.ts +11 -0
  15. package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
  16. package/corpus/core/src/server/agent-run-context.ts +21 -0
  17. package/corpus/core/src/server/capture-error.ts +8 -0
  18. package/corpus/core/src/server/core-routes-plugin.ts +75 -4
  19. package/corpus/core/src/server/deploy-environment.ts +57 -0
  20. package/corpus/core/src/server/error-noise-filter.ts +268 -0
  21. package/corpus/core/src/server/posthog-config.ts +75 -0
  22. package/corpus/core/src/server/request-context.ts +6 -0
  23. package/corpus/core/src/server/sentry-config.ts +4 -8
  24. package/corpus/core/src/server/sentry-plugin.ts +13 -29
  25. package/corpus/core/src/server/sentry.ts +13 -156
  26. package/corpus/core/src/server/ssr-handler.ts +6 -1
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  30. package/corpus/core/src/tracking/error-capture.ts +44 -109
  31. package/corpus/core/src/tracking/index.ts +11 -0
  32. package/corpus/core/src/tracking/posthog-exception.ts +305 -0
  33. package/corpus/core/src/tracking/providers.ts +91 -14
  34. package/corpus/core/src/tracking/redaction.ts +105 -0
  35. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  36. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  37. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  38. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  39. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  40. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  41. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  43. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  44. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  45. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  46. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  47. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  48. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  49. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  50. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  51. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  52. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  53. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  54. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  55. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  56. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  57. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  58. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  59. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  60. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  61. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  62. package/corpus/templates/content/app/i18n-data.ts +130 -0
  63. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  64. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  65. package/corpus/templates/content/parity/matrix.md +1 -1
  66. package/corpus/templates/content/parity/matrix.ts +4 -5
  67. package/corpus/templates/content/shared/api.ts +4 -0
  68. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  69. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  70. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  71. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  72. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  73. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  74. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  75. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  76. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  77. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  78. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  79. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  80. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  81. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  82. package/corpus/templates/design/package.json +4 -0
  83. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  84. package/dist/agent/run-manager.d.ts.map +1 -1
  85. package/dist/agent/run-manager.js +7 -0
  86. package/dist/agent/run-manager.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +12 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +8 -0
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js +95 -1
  93. package/dist/client/analytics.js.map +1 -1
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/deploy/build.d.ts.map +1 -1
  97. package/dist/deploy/build.js +38 -1
  98. package/dist/deploy/build.js.map +1 -1
  99. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  100. package/dist/notifications/routes.d.ts +1 -1
  101. package/dist/observability/posthog-ai.d.ts +126 -0
  102. package/dist/observability/posthog-ai.d.ts.map +1 -0
  103. package/dist/observability/posthog-ai.js +190 -0
  104. package/dist/observability/posthog-ai.js.map +1 -0
  105. package/dist/observability/routes.d.ts +3 -3
  106. package/dist/observability/routes.d.ts.map +1 -1
  107. package/dist/observability/routes.js +26 -6
  108. package/dist/observability/routes.js.map +1 -1
  109. package/dist/observability/traces.d.ts +9 -0
  110. package/dist/observability/traces.d.ts.map +1 -1
  111. package/dist/observability/traces.js +219 -10
  112. package/dist/observability/traces.js.map +1 -1
  113. package/dist/observability/types.d.ts +10 -0
  114. package/dist/observability/types.d.ts.map +1 -1
  115. package/dist/observability/types.js +1 -0
  116. package/dist/observability/types.js.map +1 -1
  117. package/dist/progress/routes.d.ts +1 -1
  118. package/dist/resources/handlers.d.ts +1 -1
  119. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  120. package/dist/secrets/register-framework-secrets.js +14 -0
  121. package/dist/secrets/register-framework-secrets.js.map +1 -1
  122. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  123. package/dist/server/agent-run-context.d.ts +8 -0
  124. package/dist/server/agent-run-context.d.ts.map +1 -1
  125. package/dist/server/agent-run-context.js +18 -0
  126. package/dist/server/agent-run-context.js.map +1 -1
  127. package/dist/server/capture-error.d.ts +8 -0
  128. package/dist/server/capture-error.d.ts.map +1 -1
  129. package/dist/server/capture-error.js.map +1 -1
  130. package/dist/server/core-routes-plugin.d.ts +0 -23
  131. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  132. package/dist/server/core-routes-plugin.js +71 -7
  133. package/dist/server/core-routes-plugin.js.map +1 -1
  134. package/dist/server/deploy-environment.d.ts +17 -0
  135. package/dist/server/deploy-environment.d.ts.map +1 -0
  136. package/dist/server/deploy-environment.js +47 -0
  137. package/dist/server/deploy-environment.js.map +1 -0
  138. package/dist/server/error-noise-filter.d.ts +80 -0
  139. package/dist/server/error-noise-filter.d.ts.map +1 -0
  140. package/dist/server/error-noise-filter.js +173 -0
  141. package/dist/server/error-noise-filter.js.map +1 -0
  142. package/dist/server/posthog-config.d.ts +28 -0
  143. package/dist/server/posthog-config.d.ts.map +1 -0
  144. package/dist/server/posthog-config.js +53 -0
  145. package/dist/server/posthog-config.js.map +1 -0
  146. package/dist/server/request-context.d.ts +6 -0
  147. package/dist/server/request-context.d.ts.map +1 -1
  148. package/dist/server/request-context.js.map +1 -1
  149. package/dist/server/sentry-config.d.ts +1 -0
  150. package/dist/server/sentry-config.d.ts.map +1 -1
  151. package/dist/server/sentry-config.js +3 -1
  152. package/dist/server/sentry-config.js.map +1 -1
  153. package/dist/server/sentry-plugin.d.ts.map +1 -1
  154. package/dist/server/sentry-plugin.js +12 -27
  155. package/dist/server/sentry-plugin.js.map +1 -1
  156. package/dist/server/sentry.d.ts.map +1 -1
  157. package/dist/server/sentry.js +8 -124
  158. package/dist/server/sentry.js.map +1 -1
  159. package/dist/server/ssr-handler.d.ts.map +1 -1
  160. package/dist/server/ssr-handler.js +6 -1
  161. package/dist/server/ssr-handler.js.map +1 -1
  162. package/dist/server/transcribe-voice.d.ts +1 -1
  163. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  164. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  165. package/dist/shared/mcp-embed-headers.js +1 -1
  166. package/dist/shared/mcp-embed-headers.js.map +1 -1
  167. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  168. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  169. package/dist/tracking/error-capture.d.ts +7 -0
  170. package/dist/tracking/error-capture.d.ts.map +1 -1
  171. package/dist/tracking/error-capture.js +10 -73
  172. package/dist/tracking/error-capture.js.map +1 -1
  173. package/dist/tracking/index.d.ts +1 -0
  174. package/dist/tracking/index.d.ts.map +1 -1
  175. package/dist/tracking/index.js +1 -0
  176. package/dist/tracking/index.js.map +1 -1
  177. package/dist/tracking/posthog-exception.d.ts +103 -0
  178. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  179. package/dist/tracking/posthog-exception.js +181 -0
  180. package/dist/tracking/posthog-exception.js.map +1 -0
  181. package/dist/tracking/providers.d.ts +14 -0
  182. package/dist/tracking/providers.d.ts.map +1 -1
  183. package/dist/tracking/providers.js +63 -11
  184. package/dist/tracking/providers.js.map +1 -1
  185. package/dist/tracking/redaction.d.ts +25 -0
  186. package/dist/tracking/redaction.d.ts.map +1 -0
  187. package/dist/tracking/redaction.js +85 -0
  188. package/dist/tracking/redaction.js.map +1 -0
  189. package/docs/content/observability.mdx +41 -1
  190. package/docs/content/template-content-developers.mdx +1 -1
  191. package/docs/content/tracking.mdx +25 -0
  192. package/package.json +1 -1
  193. package/src/agent/run-manager.ts +7 -0
  194. package/src/client/agent-chat-adapter.ts +10 -0
  195. package/src/client/analytics.ts +105 -1
  196. package/src/deploy/build.ts +38 -1
  197. package/src/observability/posthog-ai.ts +294 -0
  198. package/src/observability/routes.ts +29 -6
  199. package/src/observability/traces.ts +287 -12
  200. package/src/observability/types.ts +11 -0
  201. package/src/secrets/register-framework-secrets.ts +16 -0
  202. package/src/server/agent-run-context.ts +21 -0
  203. package/src/server/capture-error.ts +8 -0
  204. package/src/server/core-routes-plugin.ts +75 -4
  205. package/src/server/deploy-environment.ts +57 -0
  206. package/src/server/error-noise-filter.ts +268 -0
  207. package/src/server/posthog-config.ts +75 -0
  208. package/src/server/request-context.ts +6 -0
  209. package/src/server/sentry-config.ts +4 -8
  210. package/src/server/sentry-plugin.ts +13 -29
  211. package/src/server/sentry.ts +13 -156
  212. package/src/server/ssr-handler.ts +6 -1
  213. package/src/shared/mcp-embed-headers.ts +1 -1
  214. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  215. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  216. package/src/tracking/error-capture.ts +44 -109
  217. package/src/tracking/index.ts +11 -0
  218. package/src/tracking/posthog-exception.ts +305 -0
  219. package/src/tracking/providers.ts +91 -14
  220. package/src/tracking/redaction.ts +105 -0
  221. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Provider-agnostic drop rules for server error reporting.
3
+ *
4
+ * These rules were tuned against real production volume while Sentry was the
5
+ * only backend, and they lived inside its `beforeSend`. They are not Sentry
6
+ * policy — they describe which server errors are non-bugs in *this* framework
7
+ * (expected 4xx, access-control rejections, Lambda freeze/thaw socket noise).
8
+ * Any second backend that skips them receives a firehose rather than a signal:
9
+ * the `socket hang up` rule alone accounts for ~10k events/day.
10
+ *
11
+ * The rules operate on a normalized signal so the same predicate can judge a
12
+ * Sentry `Event` (already parsed by the SDK) and a raw `Error` reaching the
13
+ * PostHog provider. Fields a given source cannot supply stay `undefined`, and
14
+ * every rule requires the evidence it depends on — an unknown never reads as
15
+ * a match.
16
+ */
17
+
18
+ import { parseStackFrames } from "../tracking/posthog-exception.js";
19
+
20
+ export interface ErrorSignalFrame {
21
+ function?: string;
22
+ filename?: string;
23
+ in_app?: boolean;
24
+ }
25
+
26
+ export interface NormalizedErrorSignal {
27
+ /** Error class name, e.g. `ValidationError`. */
28
+ type?: string;
29
+ /** Error message. */
30
+ value?: string;
31
+ /** Capture mechanism, e.g. `onunhandledrejection`. */
32
+ mechanismType?: string;
33
+ frames?: ErrorSignalFrame[];
34
+ /** HTTP status carried by h3's `HTTPError` / `H3Error`. */
35
+ statusCode?: number;
36
+ tags?: Record<string, string | undefined>;
37
+ /**
38
+ * Sentry-only: some SDK-internal rejections arrive with no `exception.values`
39
+ * at all and only a `metadata` blob. Left `undefined` elsewhere.
40
+ */
41
+ metadataValue?: string;
42
+ metadataFilename?: string;
43
+ hasExceptionValues?: boolean;
44
+ }
45
+
46
+ function isUnhandledRejection(signal: NormalizedErrorSignal): boolean {
47
+ return (
48
+ typeof signal.mechanismType === "string" &&
49
+ signal.mechanismType.endsWith("onunhandledrejection")
50
+ );
51
+ }
52
+
53
+ /** Expected user-input rejections. The framework and CLI both throw these. */
54
+ function isValidationNoise(signal: NormalizedErrorSignal): boolean {
55
+ return (
56
+ signal.type === "ValidationError" || signal.tags?.handled === "validation"
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Access-control rejections. These are 4xx user-facing errors that reached the
62
+ * error hook because a route forgot to catch them — fixing the route is the
63
+ * right answer, but until then they bury real bugs. Auth routes report their
64
+ * own failures at `warning` level, so this only sees the escape path.
65
+ */
66
+ function isAccessControlNoise(signal: NormalizedErrorSignal): boolean {
67
+ return (
68
+ signal.type === "ForbiddenError" || signal.type === "UnauthorizedError"
69
+ );
70
+ }
71
+
72
+ /**
73
+ * `socket hang up` unhandled rejections from Lambda freeze cycles. AWS recycles
74
+ * long-lived sockets (MCP Streamable HTTP long-polls, keep-alive agents) ~60s
75
+ * after a function returns 200; the next thaw delivers a socket-end event whose
76
+ * Promise has nobody left to await it. The function already returned correctly,
77
+ * so there is no user impact. The narrow frame match keeps genuine
78
+ * application-thrown socket errors visible.
79
+ */
80
+ function isLambdaSocketHangUpNoise(signal: NormalizedErrorSignal): boolean {
81
+ if (signal.value !== "socket hang up" || !isUnhandledRejection(signal)) {
82
+ return false;
83
+ }
84
+ return (signal.frames ?? []).some(
85
+ (frame) =>
86
+ frame?.function === "Socket.socketOnEnd" ||
87
+ frame?.filename === "node:_http_client",
88
+ );
89
+ }
90
+
91
+ /**
92
+ * SDK-only `ErrorEvent` promise rejections — typically the Neon serverless
93
+ * driver's WebSocket dying across a freeze/thaw and rejecting a floating
94
+ * promise with the raw ErrorEvent (`db/client.ts` already records these with
95
+ * context). Events with real application frames stay visible.
96
+ *
97
+ * "Application frame" must exclude the Sentry SDK's own bundled chunks:
98
+ * serverless bundles place them under the app root (e.g.
99
+ * `/var/task/_libs/@sentry/node+….mjs`), outside node_modules, so the SDK marks
100
+ * them `in_app` and the instrumentation stack alone would defeat this filter.
101
+ */
102
+ function isSdkErrorEventNoise(signal: NormalizedErrorSignal): boolean {
103
+ if (signal.value === "[object ErrorEvent]" && isUnhandledRejection(signal)) {
104
+ const frames = signal.frames ?? [];
105
+ const hasApplicationFrame = frames.some(
106
+ (frame) =>
107
+ frame?.in_app && !String(frame?.filename ?? "").includes("sentry"),
108
+ );
109
+ const hasSentryFrame = frames.some((frame) =>
110
+ String(frame?.filename ?? "").includes("sentry"),
111
+ );
112
+ if (!hasApplicationFrame && hasSentryFrame) return true;
113
+ }
114
+
115
+ return (
116
+ signal.metadataValue === "[object ErrorEvent]" &&
117
+ signal.hasExceptionValues === false &&
118
+ String(signal.metadataFilename ?? "").includes("sentry")
119
+ );
120
+ }
121
+
122
+ /**
123
+ * h3's `createError({ statusCode: 4xx })` produces an `HTTPError` (h3 v2) /
124
+ * `H3Error` (h3 v1). 4xx ones are handler-controlled "expected failure"
125
+ * responses that route through the error hook only because they bubble out of
126
+ * `defineEventHandler`. Capture when the status looks 5xx, or is missing on a
127
+ * generic Error masquerading as an HTTPError.
128
+ */
129
+ function isExpectedHttpNoise(signal: NormalizedErrorSignal): boolean {
130
+ if (signal.type !== "HTTPError" && signal.type !== "H3Error") return false;
131
+
132
+ const code = signal.statusCode;
133
+ if (typeof code === "number" && Number.isFinite(code)) {
134
+ return code >= 400 && code < 500;
135
+ }
136
+
137
+ // No status in the payload — match the common 4xx messages so handler-thrown
138
+ // 404/400/403/401 don't pollute the backend. A heuristic, but the
139
+ // alternatives (every 4xx becomes a real issue, or every route grows a
140
+ // catch+return) are worse.
141
+ const value = signal.value ?? "";
142
+ return (
143
+ /^Cannot find any route matching/i.test(value) ||
144
+ / not found$/i.test(value) ||
145
+ /Unauthenticated$/i.test(value) ||
146
+ /^Unauthorized$/i.test(value) ||
147
+ /^No access to /i.test(value)
148
+ );
149
+ }
150
+
151
+ /**
152
+ * `false` when the error is known non-bug noise and should not be reported.
153
+ *
154
+ * Deliberately fails open: anything the rules cannot positively identify as
155
+ * noise is reported. A dropped real error is invisible; a kept noisy one is
156
+ * merely loud.
157
+ */
158
+ export function shouldReportErrorSignal(
159
+ signal: NormalizedErrorSignal,
160
+ ): boolean {
161
+ return !(
162
+ isValidationNoise(signal) ||
163
+ isAccessControlNoise(signal) ||
164
+ isLambdaSocketHangUpNoise(signal) ||
165
+ isSdkErrorEventNoise(signal) ||
166
+ isExpectedHttpNoise(signal)
167
+ );
168
+ }
169
+
170
+ interface SentryLikeEvent {
171
+ exception?: {
172
+ values?: Array<{
173
+ type?: string;
174
+ value?: string;
175
+ mechanism?: { type?: string };
176
+ stacktrace?: { frames?: ErrorSignalFrame[] };
177
+ }>;
178
+ };
179
+ tags?: Record<string, unknown>;
180
+ contexts?: Record<string, Record<string, unknown> | undefined>;
181
+ metadata?: { value?: unknown; filename?: unknown };
182
+ }
183
+
184
+ function toNumericStatus(value: unknown): number | undefined {
185
+ if (typeof value === "number" && Number.isFinite(value)) return value;
186
+ if (typeof value === "string") {
187
+ const parsed = parseInt(value, 10);
188
+ if (Number.isFinite(parsed)) return parsed;
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ /** Normalize a Sentry event. Typed structurally so this module stays SDK-free. */
194
+ export function errorSignalFromSentryEvent(
195
+ event: SentryLikeEvent,
196
+ ): NormalizedErrorSignal {
197
+ const first = event.exception?.values?.[0];
198
+ const tags: Record<string, string | undefined> = {};
199
+ for (const [key, value] of Object.entries(event.tags ?? {})) {
200
+ if (typeof value === "string") tags[key] = value;
201
+ }
202
+
203
+ return {
204
+ type: first?.type,
205
+ value: first?.value ?? "",
206
+ mechanismType: first?.mechanism?.type,
207
+ frames: first?.stacktrace?.frames ?? [],
208
+ statusCode: toNumericStatus(
209
+ event.tags?.statusCode ?? event.contexts?.h3?.statusCode,
210
+ ),
211
+ tags,
212
+ metadataValue:
213
+ typeof event.metadata?.value === "string"
214
+ ? event.metadata.value
215
+ : undefined,
216
+ metadataFilename:
217
+ typeof event.metadata?.filename === "string"
218
+ ? event.metadata.filename
219
+ : undefined,
220
+ hasExceptionValues: Boolean(event.exception?.values?.length),
221
+ };
222
+ }
223
+
224
+ export interface ErrorSignalFromErrorOptions {
225
+ mechanismType?: string;
226
+ tags?: Record<string, string | undefined>;
227
+ }
228
+
229
+ /** Normalize a raw thrown value for backends that never see a Sentry event. */
230
+ export function errorSignalFromError(
231
+ error: unknown,
232
+ options: ErrorSignalFromErrorOptions = {},
233
+ ): NormalizedErrorSignal {
234
+ if (!(error instanceof Error)) {
235
+ return {
236
+ type: "Error",
237
+ value: typeof error === "string" ? error : String(error ?? ""),
238
+ mechanismType: options.mechanismType,
239
+ tags: options.tags,
240
+ hasExceptionValues: true,
241
+ };
242
+ }
243
+
244
+ const withStatus = error as Error & {
245
+ statusCode?: unknown;
246
+ status?: unknown;
247
+ };
248
+
249
+ return {
250
+ type: error.name || "Error",
251
+ value: error.message ?? "",
252
+ mechanismType: options.mechanismType,
253
+ frames: parseStackFrames(error.stack),
254
+ statusCode:
255
+ toNumericStatus(withStatus.statusCode) ??
256
+ toNumericStatus(withStatus.status),
257
+ tags: options.tags,
258
+ hasExceptionValues: true,
259
+ };
260
+ }
261
+
262
+ /** Convenience wrapper for the raw-error path. */
263
+ export function shouldReportError(
264
+ error: unknown,
265
+ options: ErrorSignalFromErrorOptions = {},
266
+ ): boolean {
267
+ return shouldReportErrorSignal(errorSignalFromError(error, options));
268
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Public PostHog config for the browser.
3
+ *
4
+ * PostHog project API keys are publishable — that is how `posthog-js` ships in
5
+ * every customer's bundle — so this mirrors how the Sentry client DSN already
6
+ * reaches the browser: env-derived, identical for every visitor, and therefore
7
+ * safe inside the CDN-cached SSR shell (see `guard:ssr-cache-shell`).
8
+ *
9
+ * The browser sends exceptions straight to PostHog rather than relaying them
10
+ * through `/_agent-native/track`: that route requires a resolved session by
11
+ * design, so relaying would silently drop every signed-out crash — exactly the
12
+ * class of failure that looks like "no errors" instead of "no reporting".
13
+ */
14
+
15
+ const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
16
+
17
+ function firstNonEmpty(
18
+ ...values: Array<string | undefined>
19
+ ): string | undefined {
20
+ for (const value of values) {
21
+ const trimmed = value?.trim();
22
+ if (trimmed) return trimmed;
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ export interface PublicPostHogConfig {
28
+ posthogKey: string;
29
+ posthogHost: string;
30
+ posthogErrorTracking: boolean;
31
+ }
32
+
33
+ /**
34
+ * Resolve the browser-facing PostHog config, or `undefined` when none is set.
35
+ *
36
+ * Note this deliberately does NOT fall back to `POSTHOG_API_KEY`: a
37
+ * server-only personal/private key must never be inlined into the public HTML
38
+ * shell. Operators opt into browser capture by setting a public key explicitly.
39
+ */
40
+ export function resolvePublicPostHogConfig(): PublicPostHogConfig | undefined {
41
+ const posthogKey = firstNonEmpty(
42
+ process.env.POSTHOG_PUBLIC_KEY,
43
+ process.env.VITE_POSTHOG_KEY,
44
+ process.env.VITE_POSTHOG_PUBLIC_KEY,
45
+ );
46
+ if (!posthogKey) return undefined;
47
+
48
+ const posthogHost = (
49
+ firstNonEmpty(
50
+ process.env.POSTHOG_PUBLIC_HOST,
51
+ process.env.VITE_POSTHOG_HOST,
52
+ process.env.POSTHOG_HOST,
53
+ ) ?? POSTHOG_DEFAULT_HOST
54
+ ).replace(/\/+$/, "");
55
+
56
+ return {
57
+ posthogKey,
58
+ posthogHost,
59
+ posthogErrorTracking:
60
+ process.env.POSTHOG_ERROR_TRACKING?.trim().toLowerCase() !== "false",
61
+ };
62
+ }
63
+
64
+ export function getPostHogClientConfigScript(): string | null {
65
+ const config = resolvePublicPostHogConfig();
66
+ if (!config) return null;
67
+
68
+ return [
69
+ "<script data-agent-native-posthog-config>",
70
+ "window.__AGENT_NATIVE_CONFIG__=Object.assign({},window.__AGENT_NATIVE_CONFIG__,",
71
+ JSON.stringify(config),
72
+ ");",
73
+ "</script>",
74
+ ].join("");
75
+ }
@@ -140,6 +140,12 @@ export interface RequestContext {
140
140
  */
141
141
  authCapability?: string;
142
142
  timezone?: string;
143
+ /**
144
+ * The caller's browser analytics session id, when the request came from a
145
+ * page. Emitted as PostHog's `$session_id` so agent traces join to session
146
+ * replay; never used for authorization.
147
+ */
148
+ browserSessionId?: string;
143
149
  /**
144
150
  * Set when code reads authenticated request context. Public SSR shell/data
145
151
  * should not depend on this value; user/org-specific reads belong behind
@@ -1,3 +1,5 @@
1
+ import { resolveDeployEnvironment } from "./deploy-environment.js";
2
+
1
3
  function firstNonEmpty(
2
4
  ...values: Array<string | undefined>
3
5
  ): string | undefined {
@@ -25,15 +27,9 @@ function resolveSentryDsnFromKeyProject(): string | undefined {
25
27
  return `https://${key}@${host}/${projectId}`;
26
28
  }
27
29
 
30
+ /** @deprecated Use `resolveDeployEnvironment()` — it is not Sentry-specific. */
28
31
  export function resolveSentryEnvironment(): string {
29
- return (
30
- firstNonEmpty(
31
- process.env.SENTRY_ENVIRONMENT,
32
- process.env.NETLIFY_CONTEXT,
33
- process.env.VERCEL_ENV,
34
- process.env.NODE_ENV,
35
- ) ?? "production"
36
- );
32
+ return resolveDeployEnvironment();
37
33
  }
38
34
 
39
35
  export function resolveServerSentryDsn(): string | undefined {
@@ -1,4 +1,4 @@
1
- import { getHeader, getMethod, type H3Event } from "h3";
1
+ import { type H3Event } from "h3";
2
2
 
3
3
  import { getSession } from "./auth.js";
4
4
  import { registerErrorCaptureProvider } from "./capture-error.js";
@@ -9,12 +9,16 @@ import { registerErrorCaptureProvider } from "./capture-error.js";
9
9
  * Wires three pieces:
10
10
  * 1. On startup, `initServerSentry()` reads `SENTRY_SERVER_DSN`/`SENTRY_DSN` and arms
11
11
  * the SDK (no-op when the env var is unset).
12
- * 2. On every request, hook into Nitro's `request` event: resolve the
12
+ * 2. Registers Sentry as a `captureError()` backend, so route errors and
13
+ * explicit captures alike reach it through the shared registry.
14
+ * 3. On every request, hook into Nitro's `request` event: resolve the
13
15
  * session via `getSession(event)` and tag the per-request isolation
14
16
  * scope with the user's id/email/orgId. Wrapped in try/catch so a
15
17
  * session-resolution failure can never 500 the request.
16
- * 3. On every Nitro `error` event, capture the exception with the route,
17
- * method, and user-agent attached as searchable tags.
18
+ *
19
+ * It does NOT hook Nitro's `error` event — that is provider-agnostic and lives
20
+ * in `core-routes-plugin.ts`, so error reporting works with PostHog (or any
21
+ * other backend) configured and no Sentry DSN set at all.
18
22
  *
19
23
  * Mounted as a default plugin from `framework-request-handler.ts` —
20
24
  * templates that don't define `server/plugins/sentry.ts` get this for
@@ -44,14 +48,6 @@ function readRoute(event: H3Event): string | undefined {
44
48
  }
45
49
  }
46
50
 
47
- function readUserAgent(event: H3Event): string | undefined {
48
- try {
49
- return getHeader(event, "user-agent");
50
- } catch {
51
- return undefined;
52
- }
53
- }
54
-
55
51
  /**
56
52
  * Skip session resolution for paths that obviously don't need one. Avoids
57
53
  * a DB round-trip on every static-asset / favicon / public-share request
@@ -109,23 +105,11 @@ export function createSentryPlugin(): NitroPluginDef {
109
105
  setSentryRequestContext({ userEmail: ctx.userEmail, orgId: ctx.orgId });
110
106
  });
111
107
 
112
- // Per-error: capture with route/method/UA tags. Nitro's `error` hook
113
- // signature is (error, { event, tags }) — we forward what we can.
114
- nitroApp.hooks?.hook?.(
115
- "error",
116
- (error: unknown, ctx?: { event?: H3Event }) => {
117
- try {
118
- const event = ctx?.event;
119
- captureRouteError(error, {
120
- route: event ? readRoute(event) : undefined,
121
- method: event ? getMethod(event) : undefined,
122
- userAgent: event ? readUserAgent(event) : undefined,
123
- });
124
- } catch {
125
- // Sentry capture must never escape into Nitro's error path.
126
- }
127
- },
128
- );
108
+ // Route errors are NOT hooked here. `core-routes-plugin.ts` owns the
109
+ // provider-agnostic Nitro `error` hook and routes it through
110
+ // `captureError()`, which reaches Sentry via the registration above along
111
+ // with every other configured backend. Hooking it here too would report
112
+ // each route error to Sentry twice.
129
113
  };
130
114
  }
131
115
 
@@ -1,7 +1,3 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
1
  /**
6
2
  * Server-side Sentry initialization for Nitro.
7
3
  *
@@ -24,36 +20,18 @@ import * as Sentry from "@sentry/node";
24
20
 
25
21
  import type { AuthSession } from "./auth.js";
26
22
  import {
27
- resolveSentryEnvironment,
28
- resolveServerSentryDsn,
29
- } from "./sentry-config.js";
23
+ resolveDeployEnvironment,
24
+ resolveServerRelease,
25
+ } from "./deploy-environment.js";
26
+ import {
27
+ errorSignalFromSentryEvent,
28
+ shouldReportErrorSignal,
29
+ } from "./error-noise-filter.js";
30
+ import { resolveServerSentryDsn } from "./sentry-config.js";
30
31
 
31
32
  let _initStarted = false;
32
33
  let _initSucceeded = false;
33
34
 
34
- /**
35
- * Resolve the agent-native version baked into core's package.json so Sentry
36
- * "release" reflects the running framework version. Mirrors how the CLI
37
- * computes `_version` — same dist layout, same fallback string. Guarded so
38
- * a missing/unreadable package.json never crashes server boot.
39
- */
40
- function resolveServerRelease(): string {
41
- const explicit = process.env.AGENT_NATIVE_RELEASE;
42
- if (explicit) return explicit;
43
- try {
44
- const here = path.dirname(fileURLToPath(import.meta.url));
45
- // dist/server/sentry.js → ../../package.json
46
- const pkgPath = path.resolve(here, "../../package.json");
47
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as {
48
- version?: string;
49
- };
50
- if (pkg?.version) return `agent-native-server@${pkg.version}`;
51
- } catch {
52
- // ignore — fall through to "unknown"
53
- }
54
- return "agent-native-server@unknown";
55
- }
56
-
57
35
  function parseTracesSampleRate(): number {
58
36
  const raw = process.env.SENTRY_SERVER_TRACES_SAMPLE_RATE;
59
37
  if (!raw) return 0;
@@ -88,7 +66,7 @@ export function initServerSentry(): boolean {
88
66
 
89
67
  Sentry.init({
90
68
  dsn,
91
- environment: resolveSentryEnvironment(),
69
+ environment: resolveDeployEnvironment(),
92
70
  release: resolveServerRelease(),
93
71
  tracesSampleRate: parseTracesSampleRate(),
94
72
  // sendDefaultPii MUST stay false — the framework runs inside customer
@@ -96,133 +74,12 @@ export function initServerSentry(): boolean {
96
74
  // cookies, or process.env contents to Sentry without explicit consent.
97
75
  sendDefaultPii: false,
98
76
  beforeSend(event) {
99
- // Drop expected user-input rejections so they don't pollute Sentry
100
- // with non-bug noise. Mirrors the CLI's drop list the framework
101
- // and CLI both throw `ValidationError` for the same class of input
102
- // failures, and exception type comes through as the class name.
103
- const exceptionType = event.exception?.values?.[0]?.type;
104
- if (
105
- exceptionType === "ValidationError" ||
106
- event.tags?.handled === "validation"
107
- ) {
108
- return null;
109
- }
110
- // Drop access-control rejections (caller lacks permission, signed
111
- // out, etc.). These are 4xx user-facing errors that propagated to
112
- // Nitro's error hook because a route forgot to catch them — fixing
113
- // the route is the right answer, but in the meantime they bury
114
- // real bugs and don't represent server failures. Auth-routes use
115
- // `captureAuthError` directly with `level: warning` so this filter
116
- // only sees the generic-handler escape path.
117
- if (
118
- exceptionType === "ForbiddenError" ||
119
- exceptionType === "UnauthorizedError"
120
- ) {
77
+ // Drop rules live in `error-noise-filter.ts` so every backend applies
78
+ // the same ones they describe which server errors are non-bugs in
79
+ // this framework, not a Sentry preference.
80
+ if (!shouldReportErrorSignal(errorSignalFromSentryEvent(event))) {
121
81
  return null;
122
82
  }
123
- // Drop "socket hang up" unhandled promise rejections that fire from
124
- // Lambda freeze cycles. AWS Lambda recycles long-lived sockets (e.g.
125
- // MCP Streamable HTTP long-polls, keep-alive HTTP agents) ~60s after
126
- // a function returns 200; the next thaw delivers a socket-end event
127
- // whose Promise has nobody left to await it. The function itself
128
- // already returned correctly, so there's no user impact — just
129
- // ~10k events/day of noise. The narrow shape (unhandled rejection
130
- // from `Socket.socketOnEnd` in `node:_http_client`) keeps real
131
- // application-thrown socket errors from being silenced.
132
- const exceptionValue = event.exception?.values?.[0]?.value ?? "";
133
- const exceptionMechanism = event.exception?.values?.[0]?.mechanism?.type;
134
- const isUnhandledRejection =
135
- typeof exceptionMechanism === "string" &&
136
- exceptionMechanism.endsWith("onunhandledrejection");
137
- if (exceptionValue === "socket hang up" && isUnhandledRejection) {
138
- const frames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
139
- const fromHttpClient = frames.some(
140
- (f) =>
141
- f?.function === "Socket.socketOnEnd" ||
142
- f?.filename === "node:_http_client",
143
- );
144
- if (fromHttpClient) {
145
- return null;
146
- }
147
- }
148
- // Drop SDK-only ErrorEvent promise rejections. These arrive as Node
149
- // unhandled rejections with no application frames — typically the Neon
150
- // serverless driver's WebSocket dying across a Lambda freeze/thaw and
151
- // rejecting a floating promise with the raw ErrorEvent (the per-client
152
- // logger in db/client.ts already records these with context). Keep any
153
- // event with real app frames so thrown ErrorEvents stay visible.
154
- //
155
- // "Application frame" must exclude the Sentry SDK's own bundled chunks:
156
- // serverless bundles place them under the app root (e.g.
157
- // `/var/task/_libs/@sentry/node+….mjs`), outside node_modules, so the
158
- // SDK marks them in_app and the unhandled-rejection instrumentation
159
- // stack alone would defeat this filter.
160
- if (exceptionValue === "[object ErrorEvent]" && isUnhandledRejection) {
161
- const frames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
162
- const hasApplicationFrame = frames.some(
163
- (frame) =>
164
- frame?.in_app && !String(frame?.filename ?? "").includes("sentry"),
165
- );
166
- const hasSentryFrame = frames.some((frame) =>
167
- String(frame?.filename ?? "").includes("sentry"),
168
- );
169
- if (!hasApplicationFrame && hasSentryFrame) {
170
- return null;
171
- }
172
- }
173
- const metadata = (
174
- event as {
175
- metadata?: {
176
- filename?: unknown;
177
- value?: unknown;
178
- };
179
- }
180
- ).metadata;
181
- if (
182
- metadata?.value === "[object ErrorEvent]" &&
183
- !event.exception?.values?.length &&
184
- String(metadata.filename ?? "").includes("sentry")
185
- ) {
186
- return null;
187
- }
188
- // h3's `createError({ statusCode: 4xx, ... })` produces an
189
- // `HTTPError` (h3 v2) / `H3Error` (h3 v1). 4xx HTTPErrors are
190
- // handler-controlled "expected failure" responses (404 not found,
191
- // 400 bad input) that route through Nitro's error hook just
192
- // because they bubble out of `defineEventHandler`. They aren't
193
- // bugs — they're the documented way to return a 4xx in h3.
194
- // Capture only when the statusCode looks like 5xx (real failure)
195
- // or is missing (generic Error masquerading as HTTPError).
196
- if (exceptionType === "HTTPError" || exceptionType === "H3Error") {
197
- const statusCode = (event.tags?.statusCode ??
198
- (
199
- event.contexts as
200
- | Record<string, Record<string, unknown>>
201
- | undefined
202
- )?.h3?.statusCode) as number | string | undefined;
203
- const code =
204
- typeof statusCode === "string"
205
- ? parseInt(statusCode, 10)
206
- : statusCode;
207
- if (typeof code === "number" && code >= 400 && code < 500) {
208
- return null;
209
- }
210
- // No statusCode in the event payload — fall back to matching the
211
- // common 4xx messages so handler-thrown 404/400/403/401 don't
212
- // pollute Sentry. This is a heuristic, but the alternatives
213
- // (every 4xx becomes a "real" issue, or we patch every route to
214
- // catch+return) are worse.
215
- const value = event.exception?.values?.[0]?.value ?? "";
216
- if (
217
- /^Cannot find any route matching/i.test(value) ||
218
- / not found$/i.test(value) ||
219
- /Unauthenticated$/i.test(value) ||
220
- /^Unauthorized$/i.test(value) ||
221
- /^No access to /i.test(value)
222
- ) {
223
- return null;
224
- }
225
- }
226
83
 
227
84
  // Defense in depth: scrub PII even if some integration auto-attached
228
85
  // request metadata despite sendDefaultPii: false.
@@ -36,6 +36,7 @@ import {
36
36
  stripAppBasePath as canonicalStripAppBasePath,
37
37
  } from "./app-base-path.js";
38
38
  import { captureError } from "./capture-error.js";
39
+ import { getPostHogClientConfigScript } from "./posthog-config.js";
39
40
  import { runWithRequestContext } from "./request-context.js";
40
41
  import {
41
42
  getRealtimeClientConfigScript,
@@ -357,7 +358,11 @@ async function rewriteMountedResponse(
357
358
  requestUrl: string,
358
359
  ): Promise<Response> {
359
360
  const clientConfigScript =
360
- [getSentryClientConfigScript(), getRealtimeClientConfigScript()]
361
+ [
362
+ getSentryClientConfigScript(),
363
+ getPostHogClientConfigScript(),
364
+ getRealtimeClientConfigScript(),
365
+ ]
361
366
  .filter(Boolean)
362
367
  .join("") || null;
363
368
  const headers = new Headers(response.headers);
@@ -1,5 +1,5 @@
1
1
  export const MCP_EMBED_CORS_ALLOW_HEADERS =
2
- "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
2
+ "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-Agent-Native-Client-Compatibility,X-Agent-Native-Build-Id,X-User-Timezone,X-Agent-Native-Session-Id,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
3
3
  export const EMBED_TRANSPLANT_HEADER = "x-agent-native-embed-transplant";
4
4
 
5
5
  const CLAUDE_MCP_CONTENT_HOST_RE = /^[a-f0-9]{32}\.claudemcpcontent\.com$/i;