@obsunified/collector 2.0.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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * RFC 0002 — HTTP surface for application-aware Analyses.
3
+ *
4
+ * GET /internal/analyses — list registered definitions
5
+ * GET /internal/analyses/:id/result — definition + latest result
6
+ * GET /internal/analyses/results — bulk: every definition + latest
7
+ * POST /internal/analyses/:id/run — Stage 4: on-demand re-run for
8
+ * view: "page" investigations
9
+ *
10
+ * Cron writes happen via the scheduled handler (see analyses-runner.ts).
11
+ * The on-demand POST exists so an investigation page's [Re-run] button
12
+ * doesn't have to wait for the next 5-min tick.
13
+ */
14
+
15
+ import type {
16
+ AnalysesListResponse,
17
+ AnalysisResultResponse,
18
+ AnalysisResultsBulkResponse,
19
+ } from "@obsunified/types";
20
+ import { getConfiguredRetentionHours } from "@obsunified/types/constants";
21
+ import type { CollectorPlugin } from "../framework/collector";
22
+ import { runSqlAnalysis } from "../lib/analyses-runner";
23
+ import { AnalysesStore } from "../lib/analyses-store";
24
+ import { sqlDbFor } from "../lib/sql-db";
25
+ import { getProjectId } from "./_context";
26
+
27
+ export const analysesRoutesPlugin: CollectorPlugin = {
28
+ name: "analyses-routes",
29
+ register(app, runtime) {
30
+ app.get("/internal/analyses", async (c) => {
31
+ const projectId = getProjectId(c);
32
+ const store = new AnalysesStore(sqlDbFor(c.env));
33
+ const analyses = await store.listDefinitions(projectId);
34
+ const response: AnalysesListResponse = {
35
+ analyses,
36
+ timestamp: new Date().toISOString(),
37
+ };
38
+ return c.json(response);
39
+ });
40
+
41
+ app.get("/internal/analyses/results", async (c) => {
42
+ const projectId = getProjectId(c);
43
+ const store = new AnalysesStore(sqlDbFor(c.env));
44
+ const definitions = await store.listDefinitions(projectId);
45
+ const ids = definitions.map((def) => def.id);
46
+ const latest = await store.getLatestResultsBulk(projectId, ids);
47
+ const response: AnalysisResultsBulkResponse = {
48
+ results: definitions.map((definition) => ({
49
+ definition,
50
+ result: latest.get(definition.id) ?? null,
51
+ })),
52
+ timestamp: new Date().toISOString(),
53
+ };
54
+ return c.json(response);
55
+ });
56
+
57
+ // Stage 4 [Re-run] target. Re-executes the SQL right now and persists
58
+ // the new result. We deliberately keep this narrow:
59
+ // - only `view: "page"` definitions (tile cron is fast enough)
60
+ // - no narrate pass on the on-demand path; the page UI uses the most
61
+ // recent narrative from the cron tick (cheaper, avoids surprise
62
+ // LLM calls from a button click)
63
+ app.post("/internal/analyses/:id/run", async (c) => {
64
+ const projectId = getProjectId(c);
65
+ const id = c.req.param("id");
66
+ if (!id) {
67
+ return c.json({ error: "Bad Request", message: "id is required" }, 400);
68
+ }
69
+ const store = new AnalysesStore(sqlDbFor(c.env));
70
+ const definitions = await store.listDefinitions(projectId);
71
+ const definition = definitions.find((def) => def.id === id);
72
+ if (!definition) {
73
+ return c.json(
74
+ { error: "Not Found", message: `Analysis ${id} not found` },
75
+ 404,
76
+ );
77
+ }
78
+ if (definition.view !== "page") {
79
+ return c.json(
80
+ {
81
+ error: "Bad Request",
82
+ message: "on-demand run only supported for view=page analyses",
83
+ },
84
+ 400,
85
+ );
86
+ }
87
+ const retentionHours = getConfiguredRetentionHours(c.env.RETENTION_HOURS);
88
+ const expiresAt = Date.now() + retentionHours * 3600 * 1000;
89
+ try {
90
+ const result = await runSqlAnalysis(definition, {
91
+ db: sqlDbFor(c.env),
92
+ projectId,
93
+ retentionHours,
94
+ });
95
+ await store.insertResult(result, expiresAt);
96
+ await store.markRan(projectId, definition.id, result.generatedAt);
97
+ const response: AnalysisResultResponse = {
98
+ definition,
99
+ result,
100
+ timestamp: new Date().toISOString(),
101
+ };
102
+ return c.json(response);
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ runtime.logger.error("[analyses] on-demand run failed", {
106
+ analysis_id: id,
107
+ project_id: projectId,
108
+ error: message,
109
+ });
110
+ return c.json({ error: "Internal Server Error", message }, 500);
111
+ }
112
+ });
113
+
114
+ app.get("/internal/analyses/:id/result", async (c) => {
115
+ const projectId = getProjectId(c);
116
+ const id = c.req.param("id");
117
+ if (!id) {
118
+ return c.json({ error: "Bad Request", message: "id is required" }, 400);
119
+ }
120
+ const store = new AnalysesStore(sqlDbFor(c.env));
121
+ const definitions = await store.listDefinitions(projectId);
122
+ const definition = definitions.find((def) => def.id === id);
123
+ if (!definition) {
124
+ return c.json(
125
+ { error: "Not Found", message: `Analysis ${id} not found` },
126
+ 404,
127
+ );
128
+ }
129
+ const result = await store.getLatestResult(projectId, id);
130
+ const response: AnalysisResultResponse = {
131
+ definition,
132
+ result,
133
+ timestamp: new Date().toISOString(),
134
+ };
135
+ return c.json(response);
136
+ });
137
+ },
138
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * RFC 0002 Stage 5 — Ask box HTTP surface.
3
+ *
4
+ * POST /internal/ask { question } → AskResponse
5
+ *
6
+ * The dashboard's top-bar AskBox calls this once per question. Single-turn
7
+ * for v1; the conversational `/ask` thread (with follow-ups + "Save as
8
+ * panel") lands in a follow-up commit.
9
+ *
10
+ * The route does no SQL itself — it composes runAsk() from the catalog
11
+ * lookups (AnalysesStore + getAllAnalysesForProject) and the LLM config.
12
+ * If ANTHROPIC_API_KEY is unset we return a structured error rather than
13
+ * 500-ing, so the dashboard can render a "set ANTHROPIC_API_KEY to
14
+ * enable Ask" hint instead of a generic failure.
15
+ */
16
+
17
+ import type { AskRequest, AskResponse } from "@obsunified/types";
18
+ import { getAllAnalysesForProject } from "../analyses/index";
19
+ import type { CollectorPlugin } from "../framework/collector";
20
+ import { AnalysesStore } from "../lib/analyses-store";
21
+ import { runAsk } from "../lib/ask";
22
+ import type { LlmConfig } from "../lib/llm";
23
+ import { sqlDbFor } from "../lib/sql-db";
24
+ import { getProjectId } from "./_context";
25
+
26
+ const MAX_QUESTION_CHARS = 1000;
27
+
28
+ export const askRoutesPlugin: CollectorPlugin = {
29
+ name: "ask-routes",
30
+ register(app, runtime) {
31
+ app.post("/internal/ask", async (c) => {
32
+ let body: Partial<AskRequest> = {};
33
+ try {
34
+ body = (await c.req.json()) as Partial<AskRequest>;
35
+ } catch {
36
+ return c.json(
37
+ { error: "Bad Request", message: "body must be JSON" },
38
+ 400,
39
+ );
40
+ }
41
+ const question = (body.question ?? "").trim();
42
+ if (!question) {
43
+ return c.json(
44
+ { error: "Bad Request", message: "question is required" },
45
+ 400,
46
+ );
47
+ }
48
+ if (question.length > MAX_QUESTION_CHARS) {
49
+ return c.json(
50
+ {
51
+ error: "Bad Request",
52
+ message: `question exceeds ${MAX_QUESTION_CHARS} chars`,
53
+ },
54
+ 400,
55
+ );
56
+ }
57
+
58
+ const projectId = getProjectId(c);
59
+ // OpenAI wins if both keys are set.
60
+ // Empty strings in .dev.vars come through as "" (not undefined),
61
+ // so coerce to undefined explicitly — otherwise an empty
62
+ // OPENAI_BASE_URL clobbers the default and we end up POSTing
63
+ // to a relative URL.
64
+ const openaiBase = c.env.OPENAI_BASE_URL?.trim()
65
+ ? c.env.OPENAI_BASE_URL
66
+ : undefined;
67
+ const narrativeModel = c.env.NARRATIVE_MODEL?.trim()
68
+ ? c.env.NARRATIVE_MODEL
69
+ : undefined;
70
+ const llm: LlmConfig | null = c.env.OPENAI_API_KEY?.trim()
71
+ ? {
72
+ provider: "openai",
73
+ apiKey: c.env.OPENAI_API_KEY,
74
+ model: narrativeModel ?? "gpt-4o-mini",
75
+ apiUrl: openaiBase,
76
+ tracer: runtime.withChildSpan,
77
+ }
78
+ : c.env.ANTHROPIC_API_KEY?.trim()
79
+ ? {
80
+ provider: "anthropic",
81
+ apiKey: c.env.ANTHROPIC_API_KEY,
82
+ model: narrativeModel ?? "claude-haiku-4-5",
83
+ tracer: runtime.withChildSpan,
84
+ }
85
+ : null;
86
+ if (!llm) {
87
+ const response: AskResponse = {
88
+ answer: null,
89
+ evidence: [],
90
+ queries: [],
91
+ error:
92
+ "Ask is not configured — set OPENAI_API_KEY or ANTHROPIC_API_KEY on the collector to enable it.",
93
+ timestamp: new Date().toISOString(),
94
+ };
95
+ return c.json(response, 503);
96
+ }
97
+
98
+ const store = new AnalysesStore(sqlDbFor(c.env));
99
+
100
+ try {
101
+ const startedAt = Date.now();
102
+ runtime.logger.info("[ask] llm call starting", {
103
+ project_id: projectId,
104
+ provider: llm.provider,
105
+ model: llm.model,
106
+ question_chars: question.length,
107
+ });
108
+ const result = await runtime.withChildSpan(
109
+ "ask.runAsk",
110
+ () =>
111
+ runAsk(question, {
112
+ llm,
113
+ listAnalyses: async (filters) => {
114
+ // Pull from the live registry so the model sees Tier 1
115
+ // panels derived this tick, not stale ones in
116
+ // `analysis_definitions`.
117
+ const all = await getAllAnalysesForProject(projectId, {
118
+ db: sqlDbFor(c.env),
119
+ });
120
+ return all.filter((d) => {
121
+ if (filters?.group && d.group !== filters.group) return false;
122
+ if (filters?.view && d.view !== filters.view) return false;
123
+ return true;
124
+ });
125
+ },
126
+ getLatestResult: async (id) => {
127
+ const definitions = await store.listDefinitions(projectId);
128
+ const definition = definitions.find((d) => d.id === id);
129
+ if (!definition) return null;
130
+ const r = await store.getLatestResult(projectId, id);
131
+ return { definition, result: r };
132
+ },
133
+ }),
134
+ {
135
+ "llm.provider": llm.provider,
136
+ "llm.model": llm.model,
137
+ "openinference.span.kind": "LLM",
138
+ },
139
+ );
140
+ runtime.logger.info("[ask] llm call finished", {
141
+ project_id: projectId,
142
+ provider: llm.provider,
143
+ model: llm.model,
144
+ latency_ms: Date.now() - startedAt,
145
+ evidence_count: result.evidence.length,
146
+ query_count: result.queries.length,
147
+ answered: Boolean(result.answer),
148
+ });
149
+
150
+ // Stage 6 — log evidence citations for the auto-pin derivation.
151
+ // Only on successful answers; failed loops would skew the signal
152
+ // (the model often calls every tool it has on the way to giving
153
+ // up). Best-effort: a write failure here doesn't fail the ask.
154
+ if (result.answer && result.evidence.length > 0) {
155
+ await store
156
+ .recordAskEvidence(
157
+ projectId,
158
+ result.evidence.map((e) => e.analysisId),
159
+ )
160
+ .catch((err) =>
161
+ runtime.logger.warn("[ask] recordAskEvidence failed", {
162
+ error: err instanceof Error ? err.message : String(err),
163
+ }),
164
+ );
165
+ }
166
+
167
+ return c.json(result);
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ runtime.logger.error("[ask] failed", { error: message });
171
+ const response: AskResponse = {
172
+ answer: null,
173
+ evidence: [],
174
+ queries: [],
175
+ error: message,
176
+ timestamp: new Date().toISOString(),
177
+ };
178
+ return c.json(response, 500);
179
+ }
180
+ });
181
+ },
182
+ };
@@ -0,0 +1,19 @@
1
+ import type {
2
+ CollectorPlugin,
3
+ UsageEventProcessorPlugin,
4
+ } from "../framework/collector";
5
+ import { isBot } from "../lib/ua-parser";
6
+
7
+ const processor: UsageEventProcessorPlugin = {
8
+ name: "bot-filter",
9
+ process(events) {
10
+ return events.map((event) => ({ ...event, isBot: isBot(event.userAgent) }));
11
+ },
12
+ };
13
+
14
+ export const botFilterPlugin: CollectorPlugin = {
15
+ name: "bot-filter",
16
+ register(_app, runtime) {
17
+ runtime.addUsageEventProcessor(processor);
18
+ },
19
+ };