@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.
Files changed (213) 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/tracking.mdx +25 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/run-manager.ts +7 -0
  7. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  8. package/corpus/core/src/client/analytics.ts +105 -1
  9. package/corpus/core/src/client/use-action.ts +6 -0
  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/content/actions/_batch-utils.ts +33 -0
  36. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
  37. package/corpus/templates/content/actions/_content-files.ts +31 -21
  38. package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
  39. package/corpus/templates/content/actions/_database-utils.ts +535 -41
  40. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
  41. package/corpus/templates/content/actions/add-database-item.ts +15 -1
  42. package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
  43. package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
  44. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
  45. package/corpus/templates/content/actions/delete-database-items.ts +8 -2
  46. package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
  47. package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
  48. package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
  49. package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
  50. package/corpus/templates/content/actions/get-content-database.ts +12 -64
  51. package/corpus/templates/content/actions/move-database-item.ts +4 -1
  52. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
  54. package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
  55. package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
  56. package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
  57. package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
  58. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
  59. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
  60. package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
  61. package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
  62. package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
  63. package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
  64. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
  65. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
  66. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
  67. package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
  68. package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
  69. package/corpus/templates/content/app/i18n-data.ts +10 -0
  70. package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
  71. package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
  72. package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
  73. package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
  74. package/corpus/templates/content/parity/matrix.md +2 -1
  75. package/corpus/templates/content/parity/matrix.ts +25 -0
  76. package/corpus/templates/content/shared/api.ts +54 -4
  77. package/corpus/templates/content/shared/database-query.ts +359 -0
  78. package/dist/agent/run-manager.d.ts.map +1 -1
  79. package/dist/agent/run-manager.js +7 -0
  80. package/dist/agent/run-manager.js.map +1 -1
  81. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  82. package/dist/client/agent-chat-adapter.js +12 -0
  83. package/dist/client/agent-chat-adapter.js.map +1 -1
  84. package/dist/client/analytics.d.ts +8 -0
  85. package/dist/client/analytics.d.ts.map +1 -1
  86. package/dist/client/analytics.js +95 -1
  87. package/dist/client/analytics.js.map +1 -1
  88. package/dist/client/use-action.d.ts.map +1 -1
  89. package/dist/client/use-action.js +6 -0
  90. package/dist/client/use-action.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/collab/struct-routes.d.ts +1 -1
  93. package/dist/deploy/build.d.ts.map +1 -1
  94. package/dist/deploy/build.js +38 -1
  95. package/dist/deploy/build.js.map +1 -1
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/notifications/routes.d.ts +3 -3
  98. package/dist/observability/posthog-ai.d.ts +126 -0
  99. package/dist/observability/posthog-ai.d.ts.map +1 -0
  100. package/dist/observability/posthog-ai.js +190 -0
  101. package/dist/observability/posthog-ai.js.map +1 -0
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/observability/routes.d.ts.map +1 -1
  104. package/dist/observability/routes.js +26 -6
  105. package/dist/observability/routes.js.map +1 -1
  106. package/dist/observability/traces.d.ts +9 -0
  107. package/dist/observability/traces.d.ts.map +1 -1
  108. package/dist/observability/traces.js +219 -10
  109. package/dist/observability/traces.js.map +1 -1
  110. package/dist/observability/types.d.ts +10 -0
  111. package/dist/observability/types.d.ts.map +1 -1
  112. package/dist/observability/types.js +1 -0
  113. package/dist/observability/types.js.map +1 -1
  114. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  115. package/dist/secrets/register-framework-secrets.js +14 -0
  116. package/dist/secrets/register-framework-secrets.js.map +1 -1
  117. package/dist/server/agent-run-context.d.ts +8 -0
  118. package/dist/server/agent-run-context.d.ts.map +1 -1
  119. package/dist/server/agent-run-context.js +18 -0
  120. package/dist/server/agent-run-context.js.map +1 -1
  121. package/dist/server/capture-error.d.ts +8 -0
  122. package/dist/server/capture-error.d.ts.map +1 -1
  123. package/dist/server/capture-error.js.map +1 -1
  124. package/dist/server/core-routes-plugin.d.ts +0 -23
  125. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  126. package/dist/server/core-routes-plugin.js +71 -7
  127. package/dist/server/core-routes-plugin.js.map +1 -1
  128. package/dist/server/deploy-environment.d.ts +17 -0
  129. package/dist/server/deploy-environment.d.ts.map +1 -0
  130. package/dist/server/deploy-environment.js +47 -0
  131. package/dist/server/deploy-environment.js.map +1 -0
  132. package/dist/server/error-noise-filter.d.ts +80 -0
  133. package/dist/server/error-noise-filter.d.ts.map +1 -0
  134. package/dist/server/error-noise-filter.js +173 -0
  135. package/dist/server/error-noise-filter.js.map +1 -0
  136. package/dist/server/posthog-config.d.ts +28 -0
  137. package/dist/server/posthog-config.d.ts.map +1 -0
  138. package/dist/server/posthog-config.js +53 -0
  139. package/dist/server/posthog-config.js.map +1 -0
  140. package/dist/server/request-context.d.ts +6 -0
  141. package/dist/server/request-context.d.ts.map +1 -1
  142. package/dist/server/request-context.js.map +1 -1
  143. package/dist/server/sentry-config.d.ts +1 -0
  144. package/dist/server/sentry-config.d.ts.map +1 -1
  145. package/dist/server/sentry-config.js +3 -1
  146. package/dist/server/sentry-config.js.map +1 -1
  147. package/dist/server/sentry-plugin.d.ts.map +1 -1
  148. package/dist/server/sentry-plugin.js +12 -27
  149. package/dist/server/sentry-plugin.js.map +1 -1
  150. package/dist/server/sentry.d.ts.map +1 -1
  151. package/dist/server/sentry.js +8 -124
  152. package/dist/server/sentry.js.map +1 -1
  153. package/dist/server/ssr-handler.d.ts.map +1 -1
  154. package/dist/server/ssr-handler.js +6 -1
  155. package/dist/server/ssr-handler.js.map +1 -1
  156. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  157. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  158. package/dist/shared/mcp-embed-headers.js +1 -1
  159. package/dist/shared/mcp-embed-headers.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  161. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  162. package/dist/tracking/error-capture.d.ts +7 -0
  163. package/dist/tracking/error-capture.d.ts.map +1 -1
  164. package/dist/tracking/error-capture.js +10 -73
  165. package/dist/tracking/error-capture.js.map +1 -1
  166. package/dist/tracking/index.d.ts +1 -0
  167. package/dist/tracking/index.d.ts.map +1 -1
  168. package/dist/tracking/index.js +1 -0
  169. package/dist/tracking/index.js.map +1 -1
  170. package/dist/tracking/posthog-exception.d.ts +103 -0
  171. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  172. package/dist/tracking/posthog-exception.js +181 -0
  173. package/dist/tracking/posthog-exception.js.map +1 -0
  174. package/dist/tracking/providers.d.ts +14 -0
  175. package/dist/tracking/providers.d.ts.map +1 -1
  176. package/dist/tracking/providers.js +63 -11
  177. package/dist/tracking/providers.js.map +1 -1
  178. package/dist/tracking/redaction.d.ts +25 -0
  179. package/dist/tracking/redaction.d.ts.map +1 -0
  180. package/dist/tracking/redaction.js +85 -0
  181. package/dist/tracking/redaction.js.map +1 -0
  182. package/docs/content/observability.mdx +41 -1
  183. package/docs/content/tracking.mdx +25 -0
  184. package/package.json +1 -1
  185. package/src/agent/run-manager.ts +7 -0
  186. package/src/client/agent-chat-adapter.ts +10 -0
  187. package/src/client/analytics.ts +105 -1
  188. package/src/client/use-action.ts +6 -0
  189. package/src/deploy/build.ts +38 -1
  190. package/src/observability/posthog-ai.ts +294 -0
  191. package/src/observability/routes.ts +29 -6
  192. package/src/observability/traces.ts +287 -12
  193. package/src/observability/types.ts +11 -0
  194. package/src/secrets/register-framework-secrets.ts +16 -0
  195. package/src/server/agent-run-context.ts +21 -0
  196. package/src/server/capture-error.ts +8 -0
  197. package/src/server/core-routes-plugin.ts +75 -4
  198. package/src/server/deploy-environment.ts +57 -0
  199. package/src/server/error-noise-filter.ts +268 -0
  200. package/src/server/posthog-config.ts +75 -0
  201. package/src/server/request-context.ts +6 -0
  202. package/src/server/sentry-config.ts +4 -8
  203. package/src/server/sentry-plugin.ts +13 -29
  204. package/src/server/sentry.ts +13 -156
  205. package/src/server/ssr-handler.ts +6 -1
  206. package/src/shared/mcp-embed-headers.ts +1 -1
  207. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  208. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  209. package/src/tracking/error-capture.ts +44 -109
  210. package/src/tracking/index.ts +11 -0
  211. package/src/tracking/posthog-exception.ts +305 -0
  212. package/src/tracking/providers.ts +91 -14
  213. package/src/tracking/redaction.ts +105 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ * PostHog LLM-analytics events (`$ai_trace`, `$ai_span`, `$ai_generation`
3
+ * content fields).
4
+ *
5
+ * PostHog models an agent run as a tree: one `$ai_trace` per run, `$ai_span`
6
+ * for non-model work (tool calls), and `$ai_generation` for model round-trips.
7
+ * Every node shares `$ai_trace_id` and links upward through `$ai_parent_id`.
8
+ * Emitting only a generation — which is what this framework did — makes PostHog
9
+ * synthesize a placeholder trace with no tool steps in it.
10
+ *
11
+ * `$ai_session_id` groups traces into a conversation. It is deliberately NOT
12
+ * PostHog's `$session_id`: the latter is the browser session used for session
13
+ * replay, and the two are different lifetimes.
14
+ *
15
+ * Content (`$ai_input` / `$ai_output_choices` / `$ai_input_state` /
16
+ * `$ai_output_state`) is gated on config and always OMITTED when disabled.
17
+ * Sending `[]` instead would be indistinguishable from a run that genuinely had
18
+ * no messages.
19
+ *
20
+ * @see https://posthog.com/docs/ai-observability/traces
21
+ * @see https://posthog.com/docs/ai-observability/spans
22
+ */
23
+
24
+ import { sendPostHogEvent } from "../tracking/providers.js";
25
+ import { boundedText } from "../tracking/redaction.js";
26
+
27
+ /** Hard ceiling on serialized content per `$ai_*` field. */
28
+ export const MAX_AI_CONTENT_BYTES = 128 * 1024;
29
+ /** Hard ceiling on emitted `$ai_span` events per run. */
30
+ export const MAX_AI_SPANS_PER_RUN = 100;
31
+
32
+ export interface AiErrorDetail {
33
+ message: string;
34
+ terminal_code?: string;
35
+ terminal_state?: string;
36
+ retryable?: boolean;
37
+ }
38
+
39
+ function trackAiEvent(
40
+ name: string,
41
+ properties: Record<string, unknown>,
42
+ userId: string | null,
43
+ ): void {
44
+ for (const key of Object.keys(properties)) {
45
+ if (properties[key] === undefined) delete properties[key];
46
+ }
47
+ try {
48
+ void import("../tracking/registry.js")
49
+ .then(({ track }) => {
50
+ track(name, properties, { userId: userId ?? undefined });
51
+ })
52
+ .catch(() => {});
53
+ // coercion-ok: a throw here would break the run it is observing
54
+ } catch {
55
+ // Tracking must never affect the agent run or trace persistence.
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Serialize a content value under a byte ceiling.
61
+ *
62
+ * Returns `{ truncated: true }` with a placeholder rather than a silently
63
+ * shortened payload — a trace that shows half a conversation as if it were the
64
+ * whole one is worse than one that says it was cut.
65
+ */
66
+ export function boundAiContent(value: unknown): {
67
+ value: unknown;
68
+ truncated: boolean;
69
+ } {
70
+ let serialized: string;
71
+ try {
72
+ serialized = JSON.stringify(value);
73
+ } catch {
74
+ return { value: "[unserializable]", truncated: true };
75
+ }
76
+ if (serialized === undefined) return { value: undefined, truncated: false };
77
+
78
+ const bytes =
79
+ typeof Buffer !== "undefined"
80
+ ? Buffer.byteLength(serialized, "utf8")
81
+ : new TextEncoder().encode(serialized).length;
82
+ if (bytes <= MAX_AI_CONTENT_BYTES) return { value, truncated: false };
83
+
84
+ return {
85
+ value: `[truncated: ${bytes} bytes exceeded the ${MAX_AI_CONTENT_BYTES}-byte trace content limit]`,
86
+ truncated: true,
87
+ };
88
+ }
89
+
90
+ export interface AiTraceEventInput {
91
+ runId: string;
92
+ threadId: string | null;
93
+ userId: string | null;
94
+ /** Human-readable name for the run, e.g. the agent or thread name. */
95
+ spanName: string;
96
+ model: string;
97
+ provider: string;
98
+ latencySeconds: number;
99
+ isError: boolean;
100
+ error?: AiErrorDetail;
101
+ inputTokens?: number;
102
+ outputTokens?: number;
103
+ costUsd?: number;
104
+ createdAt: number;
105
+ /** Browser session id, when the run originated from a page. Links the trace
106
+ * to PostHog session replay. */
107
+ browserSessionId?: string;
108
+ /** Omitted unless `capturePrompts` is on. */
109
+ inputState?: unknown;
110
+ outputState?: unknown;
111
+ extraProperties?: Record<string, unknown>;
112
+ }
113
+
114
+ export function emitAiTraceEvent(input: AiTraceEventInput): void {
115
+ const inputContent =
116
+ input.inputState === undefined
117
+ ? undefined
118
+ : boundAiContent(input.inputState);
119
+ const outputContent =
120
+ input.outputState === undefined
121
+ ? undefined
122
+ : boundAiContent(input.outputState);
123
+
124
+ trackAiEvent(
125
+ "$ai_trace",
126
+ {
127
+ ...input.extraProperties,
128
+ $ai_trace_id: input.runId,
129
+ $ai_session_id: input.threadId ?? undefined,
130
+ $ai_span_name: input.spanName,
131
+ $ai_model: input.model,
132
+ $ai_provider: input.provider,
133
+ $ai_latency: input.latencySeconds,
134
+ $ai_is_error: input.isError,
135
+ $ai_error: input.error,
136
+ $ai_input_tokens: input.inputTokens,
137
+ $ai_output_tokens: input.outputTokens,
138
+ $ai_total_cost_usd: input.costUsd,
139
+ $ai_input_state: inputContent?.value,
140
+ $ai_output_state: outputContent?.value,
141
+ $ai_input_truncated: inputContent?.truncated || undefined,
142
+ $ai_output_truncated: outputContent?.truncated || undefined,
143
+ $session_id: input.browserSessionId,
144
+ created_at: new Date(input.createdAt).toISOString(),
145
+ },
146
+ input.userId,
147
+ );
148
+ }
149
+
150
+ export interface AiSpanEventInput {
151
+ runId: string;
152
+ threadId: string | null;
153
+ userId: string | null;
154
+ spanId: string;
155
+ /** Defaults to the run's trace id, which is the tree root. */
156
+ parentId?: string;
157
+ spanName: string;
158
+ latencySeconds: number;
159
+ isError: boolean;
160
+ error?: AiErrorDetail;
161
+ createdAt: number;
162
+ browserSessionId?: string;
163
+ /** Omitted unless `captureToolArgs` / `captureToolResults` are on. */
164
+ inputState?: unknown;
165
+ outputState?: unknown;
166
+ extraProperties?: Record<string, unknown>;
167
+ }
168
+
169
+ export function emitAiSpanEvent(input: AiSpanEventInput): void {
170
+ const inputContent =
171
+ input.inputState === undefined
172
+ ? undefined
173
+ : boundAiContent(input.inputState);
174
+ const outputContent =
175
+ input.outputState === undefined
176
+ ? undefined
177
+ : boundAiContent(input.outputState);
178
+
179
+ trackAiEvent(
180
+ "$ai_span",
181
+ {
182
+ ...input.extraProperties,
183
+ $ai_trace_id: input.runId,
184
+ $ai_session_id: input.threadId ?? undefined,
185
+ $ai_span_id: input.spanId,
186
+ $ai_parent_id: input.parentId ?? input.runId,
187
+ $ai_span_name: input.spanName,
188
+ $ai_latency: input.latencySeconds,
189
+ $ai_is_error: input.isError,
190
+ $ai_error: input.error,
191
+ $ai_input_state: inputContent?.value,
192
+ $ai_output_state: outputContent?.value,
193
+ $ai_input_truncated: inputContent?.truncated || undefined,
194
+ $ai_output_truncated: outputContent?.truncated || undefined,
195
+ $session_id: input.browserSessionId,
196
+ created_at: new Date(input.createdAt).toISOString(),
197
+ },
198
+ input.userId,
199
+ );
200
+ }
201
+
202
+ export interface AiFeedbackSurveyInput {
203
+ runId: string | null;
204
+ threadId: string | null;
205
+ userId: string | null;
206
+ feedbackType: "thumbs_up" | "thumbs_down" | "category" | "text";
207
+ /** The submitted value: sentiment label, chosen category, or free text. */
208
+ value: string;
209
+ submissionId: string;
210
+ model?: string;
211
+ browserSessionId?: string;
212
+ }
213
+
214
+ /**
215
+ * Emit PostHog's documented manual feedback event for an LLM trace.
216
+ *
217
+ * PostHog surfaces feedback in LLM analytics only through `survey sent` keyed
218
+ * to a real survey id — `$ai_feedback` is not a PostHog event and renders
219
+ * nowhere. Returns `false` and emits nothing when no survey id is configured:
220
+ * inventing one would produce events attached to a survey that does not exist.
221
+ *
222
+ * Sent to PostHog ONLY, not through `track()`. The survey response carries the
223
+ * user's free-text feedback verbatim, and configuring a PostHog survey id must
224
+ * not silently start shipping that text to Mixpanel, Amplitude, webhooks, or
225
+ * Agent Native Analytics. Those backends get the content-free `$ai_feedback`
226
+ * event instead.
227
+ *
228
+ * @see https://posthog.com/docs/ai-observability/user-feedback/manual-event-capture
229
+ */
230
+ export function emitAiFeedbackSurveyEvent(
231
+ input: AiFeedbackSurveyInput,
232
+ ): boolean {
233
+ const surveyId = process.env.POSTHOG_AI_FEEDBACK_SURVEY_ID?.trim();
234
+ if (!surveyId) return false;
235
+
236
+ const questionId = process.env.POSTHOG_AI_FEEDBACK_SURVEY_QUESTION_ID?.trim();
237
+ // PostHog accepts `$survey_response` for a single-question survey and
238
+ // `$survey_response_<questionId>` when the survey has named questions.
239
+ const responseKey = questionId
240
+ ? `$survey_response_${questionId}`
241
+ : "$survey_response";
242
+
243
+ const properties: Record<string, unknown> = {
244
+ $survey_id: surveyId,
245
+ [responseKey]: input.value,
246
+ $survey_submission_id: input.submissionId,
247
+ $survey_completed: true,
248
+ $ai_trace_id: input.runId ?? undefined,
249
+ $ai_session_id: input.threadId ?? undefined,
250
+ $ai_model: input.model,
251
+ $session_id: input.browserSessionId,
252
+ feedback_type: input.feedbackType,
253
+ source: "agent_observability",
254
+ };
255
+ for (const key of Object.keys(properties)) {
256
+ if (properties[key] === undefined) delete properties[key];
257
+ }
258
+
259
+ return sendPostHogEvent(
260
+ "survey sent",
261
+ properties,
262
+ input.userId ?? "anonymous",
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Build a structured `$ai_error` from the run's failure information.
268
+ *
269
+ * Returns `undefined` when the run did not fail, so `$ai_error` is absent
270
+ * rather than an empty object on healthy traces.
271
+ */
272
+ export function toAiErrorDetail(
273
+ errorMessage: string | null | undefined,
274
+ terminalOutcome?: {
275
+ state?: string;
276
+ code?: string;
277
+ retryable?: boolean;
278
+ },
279
+ ): AiErrorDetail | undefined {
280
+ if (!errorMessage && !terminalOutcome?.code) return undefined;
281
+ return {
282
+ message: boundedText(
283
+ errorMessage ?? terminalOutcome?.code ?? "error",
284
+ 1000,
285
+ ),
286
+ ...(terminalOutcome?.state
287
+ ? { terminal_state: terminalOutcome.state }
288
+ : {}),
289
+ ...(terminalOutcome?.code ? { terminal_code: terminalOutcome.code } : {}),
290
+ ...(terminalOutcome?.retryable !== undefined
291
+ ? { retryable: terminalOutcome.retryable }
292
+ : {}),
293
+ };
294
+ }
@@ -30,7 +30,9 @@ import {
30
30
 
31
31
  import { getSession } from "../server/auth.js";
32
32
  import { readBody } from "../server/h3-helpers.js";
33
+ import { getRequestContext } from "../server/request-context.js";
33
34
  import { track } from "../tracking/registry.js";
35
+ import { emitAiFeedbackSurveyEvent } from "./posthog-ai.js";
34
36
  import {
35
37
  getObservabilityOverview,
36
38
  getTraceSummaries,
@@ -207,11 +209,11 @@ export function createObservabilityHandler() {
207
209
  userId: owner,
208
210
  createdAt: Date.now(),
209
211
  });
210
- // Emit one content-free analytics event for the explicit thumb itself.
211
- // Category follow-ups intentionally do not emit: a thumbs-down followed
212
- // by a category would otherwise double-count the same negative signal.
213
- if (feedbackType === "thumbs_up" || feedbackType === "thumbs_down") {
212
+ {
214
213
  const runId = body.runId ? String(body.runId) : null;
214
+ const threadId = body.threadId ? String(body.threadId) : null;
215
+ const isThumb =
216
+ feedbackType === "thumbs_up" || feedbackType === "thumbs_down";
215
217
  let model: string | undefined;
216
218
  if (runId) {
217
219
  try {
@@ -223,13 +225,21 @@ export function createObservabilityHandler() {
223
225
  }
224
226
  }
225
227
 
226
- const threadId = body.threadId ? String(body.threadId) : null;
228
+ // Every submission is reported, including `category` and `text`, which
229
+ // previously emitted nothing at all. Only thumbs carry `sentiment` —
230
+ // a category follow-up to a thumbs-down is extra detail about the same
231
+ // vote, so counting it as a second negative would inflate the metric.
227
232
  track(
228
233
  "$ai_feedback",
229
234
  {
230
235
  ...trackingIdentityProperties(),
231
236
  source: "agent_observability",
232
- sentiment: feedbackType === "thumbs_up" ? "positive" : "negative",
237
+ ...(isThumb
238
+ ? {
239
+ sentiment:
240
+ feedbackType === "thumbs_up" ? "positive" : "negative",
241
+ }
242
+ : {}),
233
243
  feedback_type: feedbackType,
234
244
  run_id: runId,
235
245
  thread_id: threadId,
@@ -240,6 +250,19 @@ export function createObservabilityHandler() {
240
250
  },
241
251
  { userId: owner },
242
252
  );
253
+
254
+ // PostHog shows feedback in LLM analytics only via `survey sent`.
255
+ // No-ops unless a survey id is configured.
256
+ emitAiFeedbackSurveyEvent({
257
+ runId,
258
+ threadId,
259
+ userId: owner,
260
+ feedbackType,
261
+ value: isThumb ? feedbackType : value,
262
+ submissionId: id,
263
+ model,
264
+ browserSessionId: getRequestContext()?.browserSessionId,
265
+ });
243
266
  }
244
267
  // Fire-and-forget: recompute satisfaction score for the thread.
245
268
  if (body.threadId) {