@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,624 @@
1
+ /**
2
+ * IdentityIndex tests — the SQL primitive every connected-rail and
3
+ * timeline section is built on. We use MemSqlDb to assert which queries
4
+ * the index issues and how it maps rows back into the EntityManifest
5
+ * shape. Real D1 grammar is covered by integration tests; this is
6
+ * decision coverage.
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+ import { IdentityIndex } from "./identity-index";
11
+ import { MemSqlDb } from "./test-utils/mem-sql-db";
12
+
13
+ const noRows = () => [];
14
+ const noFirst = () => null;
15
+
16
+ describe("IdentityIndex.bySession", () => {
17
+ it("issues one query per signal table + replay metadata and maps rows", async () => {
18
+ const db = new MemSqlDb({
19
+ all: (sql) => {
20
+ if (sql.includes("FROM telemetry_spans")) {
21
+ return [
22
+ {
23
+ trace_id: "t1",
24
+ span_id: "s1",
25
+ parent_span_id: null,
26
+ service_name: "checkout",
27
+ span_name: "POST /buy",
28
+ status_code: "OK",
29
+ status_message: null,
30
+ start_time: "2026-05-01T00:00:00Z",
31
+ duration_ms: 12,
32
+ interaction_id: null,
33
+ },
34
+ ];
35
+ }
36
+ if (sql.includes("FROM logs")) {
37
+ return [
38
+ {
39
+ log_id: "l1",
40
+ trace_id: "t1",
41
+ span_id: "s1",
42
+ service_name: "checkout",
43
+ logger_name: "app",
44
+ severity: "ERROR",
45
+ message: "boom",
46
+ occurred_at: "2026-05-01T00:00:01Z",
47
+ interaction_id: null,
48
+ },
49
+ ];
50
+ }
51
+ if (sql.includes("FROM usage_events")) return [];
52
+ if (sql.includes("FROM ai_calls")) return [];
53
+ return [];
54
+ },
55
+ first: (sql) => {
56
+ if (sql.includes("session_replay_metadata")) {
57
+ return {
58
+ first_chunk_at: "2026-05-01T00:00:00Z",
59
+ last_chunk_at: "2026-05-01T00:00:30Z",
60
+ chunk_count: 3,
61
+ events_count: 42,
62
+ };
63
+ }
64
+ return null;
65
+ },
66
+ });
67
+ const index = new IdentityIndex(db);
68
+ const manifest = await index.bySession("proj-1", "sess-123");
69
+
70
+ expect(manifest.spans).toHaveLength(1);
71
+ expect(manifest.spans[0].traceId).toBe("t1");
72
+ expect(manifest.logs).toHaveLength(1);
73
+ expect(manifest.logs[0].severity).toBe("ERROR");
74
+ expect(manifest.usageEvents).toEqual([]);
75
+ expect(manifest.aiCalls).toEqual([]);
76
+ expect(manifest.replay).toEqual({
77
+ sessionId: "sess-123",
78
+ firstChunkAt: "2026-05-01T00:00:00Z",
79
+ lastChunkAt: "2026-05-01T00:00:30Z",
80
+ chunkCount: 3,
81
+ eventsCount: 42,
82
+ });
83
+
84
+ // All five queries pin (project_id, session_id) — replay lookup uses
85
+ // just session_id since replay is global.
86
+ const allCalls = db.calls.filter((c) => c.op !== "first" || c.binds.length);
87
+ const sessionQueries = allCalls.filter(
88
+ (c) => c.sql.includes("session_id") && c.binds.includes("proj-1"),
89
+ );
90
+ expect(sessionQueries.length).toBeGreaterThanOrEqual(4);
91
+ });
92
+
93
+ it("returns null replay when session_replay_metadata is empty", async () => {
94
+ const db = new MemSqlDb({ all: noRows, first: noFirst });
95
+ const index = new IdentityIndex(db);
96
+ const manifest = await index.bySession("proj-1", "sess-empty");
97
+ expect(manifest.replay).toBeNull();
98
+ });
99
+ });
100
+
101
+ describe("IdentityIndex.byTrace", () => {
102
+ it("queries three tables, returns null replay (replays are session-scoped)", async () => {
103
+ const db = new MemSqlDb({
104
+ all: (sql) => {
105
+ if (sql.includes("FROM telemetry_spans")) {
106
+ return [
107
+ {
108
+ trace_id: "tx",
109
+ span_id: "s1",
110
+ parent_span_id: null,
111
+ service_name: "api",
112
+ span_name: "GET /",
113
+ status_code: "OK",
114
+ status_message: null,
115
+ start_time: "2026-05-01T00:00:00Z",
116
+ duration_ms: 5,
117
+ interaction_id: "click-1",
118
+ },
119
+ ];
120
+ }
121
+ return [];
122
+ },
123
+ first: noFirst,
124
+ });
125
+ const index = new IdentityIndex(db);
126
+ const manifest = await index.byTrace("proj-1", "tx");
127
+
128
+ expect(manifest.spans).toHaveLength(1);
129
+ expect(manifest.spans[0].interactionId).toBe("click-1");
130
+ expect(manifest.replay).toBeNull();
131
+
132
+ const traceCalls = db.callsMatching("trace_id = ?");
133
+ expect(traceCalls.length).toBeGreaterThanOrEqual(3);
134
+ });
135
+ });
136
+
137
+ describe("IdentityIndex.byInteraction", () => {
138
+ it("queries the four interaction-bearing tables", async () => {
139
+ const db = new MemSqlDb({ all: noRows, first: noFirst });
140
+ const index = new IdentityIndex(db);
141
+ await index.byInteraction("proj-1", "click-abc");
142
+ // spans + logs + usage + ai_calls — all keyed on interaction_id.
143
+ const interactionCalls = db.callsMatching("interaction_id = ?");
144
+ expect(interactionCalls.length).toBe(4);
145
+ for (const c of interactionCalls) {
146
+ expect(c.binds).toContain("proj-1");
147
+ expect(c.binds).toContain("click-abc");
148
+ }
149
+ });
150
+ });
151
+
152
+ describe("IdentityIndex.byUser", () => {
153
+ it("returns an empty manifest when no user_profiles row matches", async () => {
154
+ const db = new MemSqlDb({ all: noRows, first: noFirst });
155
+ const index = new IdentityIndex(db);
156
+ const manifest = await index.byUser("proj-1", "user-nope");
157
+ expect(manifest.spans).toEqual([]);
158
+ expect(manifest.usageEvents).toEqual([]);
159
+ expect(manifest.replay).toBeNull();
160
+ // Only the user_profiles lookup should fire; we never get to the
161
+ // session enumeration step.
162
+ expect(db.callsMatching("FROM user_profiles")).toHaveLength(1);
163
+ expect(db.callsMatching("FROM telemetry_spans")).toHaveLength(0);
164
+ });
165
+
166
+ it("returns empty (but does not error) when user exists with no usage sessions", async () => {
167
+ const db = new MemSqlDb({
168
+ first: (sql) => {
169
+ if (sql.includes("FROM user_profiles")) {
170
+ return { visitor_id: "v-1" };
171
+ }
172
+ return null;
173
+ },
174
+ all: () => [],
175
+ });
176
+ const index = new IdentityIndex(db);
177
+ const manifest = await index.byUser("proj-1", "user-1");
178
+ expect(manifest.usageEvents).toEqual([]);
179
+ expect(manifest.spans).toEqual([]);
180
+ // User_profiles + session enumeration ran; signal tables did not.
181
+ expect(db.callsMatching("FROM user_profiles")).toHaveLength(1);
182
+ expect(db.callsMatching("DISTINCT session_id")).toHaveLength(1);
183
+ expect(db.callsMatching("FROM telemetry_spans")).toHaveLength(0);
184
+ });
185
+
186
+ it("fans out across the user's recent sessions with IN-list binds", async () => {
187
+ const db = new MemSqlDb({
188
+ first: (sql) => {
189
+ if (sql.includes("FROM user_profiles")) return { visitor_id: "v-1" };
190
+ return null;
191
+ },
192
+ all: (sql) => {
193
+ if (sql.includes("DISTINCT session_id")) {
194
+ return [
195
+ { session_id: "sess-A", last_at: "2026-05-04T12:00:00Z" },
196
+ { session_id: "sess-B", last_at: "2026-05-04T11:00:00Z" },
197
+ ];
198
+ }
199
+ if (sql.includes("FROM telemetry_spans")) {
200
+ return [
201
+ {
202
+ trace_id: "t1",
203
+ span_id: "s1",
204
+ parent_span_id: null,
205
+ service_name: "api",
206
+ span_name: "POST /buy",
207
+ status_code: 1,
208
+ status_message: null,
209
+ start_time: "2026-05-04T12:00:00Z",
210
+ duration_ms: 200,
211
+ interaction_id: null,
212
+ },
213
+ ];
214
+ }
215
+ if (sql.includes("FROM ai_calls")) {
216
+ return [
217
+ {
218
+ call_id: "ai-1",
219
+ trace_id: "t1",
220
+ model_name: "gpt-4o-mini",
221
+ provider: "openai",
222
+ total_cost_usd: 0.42,
223
+ occurred_at: "2026-05-04T12:00:01Z",
224
+ interaction_id: null,
225
+ },
226
+ ];
227
+ }
228
+ return [];
229
+ },
230
+ });
231
+ const index = new IdentityIndex(db);
232
+ const manifest = await index.byUser("proj-1", "user-1");
233
+
234
+ expect(manifest.spans).toHaveLength(1);
235
+ expect(manifest.aiCalls).toHaveLength(1);
236
+ expect(manifest.aiCalls[0].totalCostUsd).toBe(0.42);
237
+
238
+ // Spans query must include both session_ids in the IN-list binds.
239
+ const spanCall = db.callsMatching("FROM telemetry_spans")[0];
240
+ expect(spanCall).toBeDefined();
241
+ expect(spanCall.sql).toContain("session_id IN");
242
+ expect(spanCall.binds).toContain("sess-A");
243
+ expect(spanCall.binds).toContain("sess-B");
244
+ });
245
+
246
+ it("clamps sessions option to a safe upper bound", async () => {
247
+ const db = new MemSqlDb({
248
+ first: (sql) => {
249
+ if (sql.includes("FROM user_profiles")) return { visitor_id: "v-1" };
250
+ return null;
251
+ },
252
+ all: () => [],
253
+ });
254
+ const index = new IdentityIndex(db);
255
+ await index.byUser("proj-1", "user-1", { sessions: 9999 });
256
+
257
+ const sessionsCall = db.callsMatching("DISTINCT session_id")[0];
258
+ // Last bind is the LIMIT; should be clamped to ≤20.
259
+ const limit = sessionsCall.binds[sessionsCall.binds.length - 1] as number;
260
+ expect(limit).toBeLessThanOrEqual(20);
261
+ });
262
+ });
263
+
264
+ describe("IdentityIndex.byAction", () => {
265
+ it("materializes the entire causal decision tree and related signals", async () => {
266
+ const db = new MemSqlDb({
267
+ first: (sql) => {
268
+ if (sql.includes("actions") && sql.includes("id = ?")) {
269
+ return {
270
+ id: "action-1",
271
+ project_id: "proj-1",
272
+ root_action_id: "root-1",
273
+ caused_by_action_id: null,
274
+ actor_type: "agent",
275
+ actor_id: "agent-1",
276
+ action_kind: "agent.run",
277
+ name: "test-action",
278
+ status: "ok",
279
+ started_at: "2026-05-01T00:00:00Z",
280
+ ended_at: "2026-05-01T00:00:10Z",
281
+ duration_ms: 10000,
282
+ trace_id: "t1",
283
+ span_id: "s1",
284
+ session_id: "sess-1",
285
+ interaction_id: null,
286
+ user_id: "user-1",
287
+ agent_run_id: "root-1",
288
+ step_id: null,
289
+ tool_call_id: null,
290
+ prompt_version: null,
291
+ model_name: null,
292
+ provider: null,
293
+ total_cost_usd: 0.05,
294
+ attrs_json: "{}",
295
+ };
296
+ }
297
+ if (sql.includes("session_replay_metadata")) {
298
+ return {
299
+ session_id: "sess-1",
300
+ first_chunk_at: "2026-05-01T00:00:00Z",
301
+ last_chunk_at: "2026-05-01T00:00:30Z",
302
+ chunk_count: 2,
303
+ events_count: 20,
304
+ };
305
+ }
306
+ return null;
307
+ },
308
+ all: (sql) => {
309
+ if (sql.includes("actions") && sql.includes("root_action_id = ?")) {
310
+ return [
311
+ {
312
+ id: "action-1",
313
+ project_id: "proj-1",
314
+ root_action_id: "root-1",
315
+ caused_by_action_id: null,
316
+ actor_type: "agent",
317
+ actor_id: "agent-1",
318
+ action_kind: "agent.run",
319
+ name: "test-action",
320
+ status: "ok",
321
+ started_at: "2026-05-01T00:00:00Z",
322
+ ended_at: "2026-05-01T00:00:10Z",
323
+ duration_ms: 10000,
324
+ trace_id: "t1",
325
+ span_id: "s1",
326
+ session_id: "sess-1",
327
+ interaction_id: null,
328
+ user_id: "user-1",
329
+ agent_run_id: "root-1",
330
+ step_id: null,
331
+ tool_call_id: null,
332
+ prompt_version: null,
333
+ model_name: null,
334
+ provider: null,
335
+ total_cost_usd: 0.05,
336
+ attrs_json: "{}",
337
+ },
338
+ ];
339
+ }
340
+ if (sql.includes("agent_runs")) {
341
+ return [
342
+ {
343
+ id: "root-1",
344
+ project_id: "proj-1",
345
+ agent_id: "agent-1",
346
+ agent_name: "test-agent",
347
+ agent_version: "1.0.0",
348
+ goal: "test goal",
349
+ outcome: "test success",
350
+ autonomy_level: "autonomous_write",
351
+ status: "success",
352
+ error_message: null,
353
+ total_cost_usd: 0.05,
354
+ total_duration_ms: 10000,
355
+ metadata_json: "{}",
356
+ },
357
+ ];
358
+ }
359
+ if (sql.includes("tool_calls")) {
360
+ return [
361
+ {
362
+ id: "tool-1",
363
+ action_id: "action-1",
364
+ project_id: "proj-1",
365
+ tool_name: "test_tool",
366
+ args_hash: "hash1",
367
+ result_hash: "hash2",
368
+ error_type: null,
369
+ side_effect: 0,
370
+ approval_state: "suggested",
371
+ args_redacted: "{}",
372
+ result_redacted: "{}",
373
+ },
374
+ ];
375
+ }
376
+ if (sql.includes("retrieval_events")) return [];
377
+ if (sql.includes("eval_results")) return [];
378
+ if (sql.includes("artifacts")) return [];
379
+ if (sql.includes("telemetry_spans")) {
380
+ return [
381
+ {
382
+ trace_id: "t1",
383
+ span_id: "s1",
384
+ parent_span_id: null,
385
+ service_name: "agent-service",
386
+ span_name: "agent-run-span",
387
+ status_code: 1,
388
+ status_message: null,
389
+ start_time: "2026-05-01T00:00:00Z",
390
+ duration_ms: 10000,
391
+ interaction_id: null,
392
+ },
393
+ ];
394
+ }
395
+ if (sql.includes("logs")) return [];
396
+ if (sql.includes("ai_calls")) return [];
397
+ return [];
398
+ },
399
+ });
400
+
401
+ const index = new IdentityIndex(db);
402
+ const manifest = await index.byAction("proj-1", "action-1");
403
+
404
+ expect(manifest.actions).toHaveLength(1);
405
+ expect(manifest.actions[0].id).toBe("action-1");
406
+ expect(manifest.actions[0].projectId).toBe("proj-1");
407
+ expect(manifest.agentRuns).toHaveLength(1);
408
+ expect(manifest.agentRuns[0].agentId).toBe("agent-1");
409
+ expect(manifest.toolCalls).toHaveLength(1);
410
+ expect(manifest.toolCalls[0].toolName).toBe("test_tool");
411
+ expect(manifest.spans).toHaveLength(1);
412
+ expect(manifest.spans[0].traceId).toBe("t1");
413
+ expect(manifest.replay).not.toBeNull();
414
+ expect(manifest.replay?.sessionId).toBe("sess-1");
415
+ });
416
+
417
+ it("returns empty structure when action does not exist", async () => {
418
+ const db = new MemSqlDb({ first: noFirst, all: noRows });
419
+ const index = new IdentityIndex(db);
420
+ const manifest = await index.byAction("proj-1", "action-none");
421
+ expect(manifest.actions).toEqual([]);
422
+ expect(manifest.agentRuns).toEqual([]);
423
+ expect(manifest.toolCalls).toEqual([]);
424
+ expect(manifest.spans).toEqual([]);
425
+ });
426
+
427
+ it("projects a legacy interaction into a synthetic root action when no action row exists", async () => {
428
+ const db = new MemSqlDb({
429
+ first: noFirst,
430
+ all: (sql) => {
431
+ if (sql.includes("FROM telemetry_spans")) {
432
+ return [
433
+ {
434
+ trace_id: "trace-legacy",
435
+ span_id: "span-legacy",
436
+ parent_span_id: null,
437
+ service_name: "web",
438
+ span_name: "checkout click",
439
+ status_code: 1,
440
+ status_message: null,
441
+ start_time: "2026-05-01T00:00:00.000Z",
442
+ duration_ms: 25,
443
+ interaction_id: "01HLEGACYINTERACTION000000",
444
+ },
445
+ ];
446
+ }
447
+ if (sql.includes("FROM usage_events")) {
448
+ return [
449
+ {
450
+ event_id: "event-legacy",
451
+ event_type: "click",
452
+ event_name: "Checkout clicked",
453
+ page_path: "/checkout",
454
+ severity: null,
455
+ occurred_at: "2026-05-01T00:00:00.010Z",
456
+ interaction_id: "01HLEGACYINTERACTION000000",
457
+ session_id: "session-legacy",
458
+ },
459
+ ];
460
+ }
461
+ if (sql.includes("FROM logs")) return [];
462
+ if (sql.includes("FROM ai_calls")) return [];
463
+ return [];
464
+ },
465
+ });
466
+
467
+ const index = new IdentityIndex(db);
468
+ const manifest = await index.byAction(
469
+ "proj-1",
470
+ "01HLEGACYINTERACTION000000",
471
+ );
472
+
473
+ expect(manifest.actions).toHaveLength(1);
474
+ expect(manifest.actions[0]).toMatchObject({
475
+ id: "01HLEGACYINTERACTION000000",
476
+ rootActionId: "01HLEGACYINTERACTION000000",
477
+ interactionId: "01HLEGACYINTERACTION000000",
478
+ actionKind: "browser.interaction",
479
+ actorType: "human",
480
+ sessionId: "session-legacy",
481
+ });
482
+ expect(manifest.spans).toHaveLength(1);
483
+ expect(manifest.usageEvents).toHaveLength(1);
484
+ });
485
+ });
486
+
487
+ describe("IdentityIndex.byAgentRun", () => {
488
+ it("resolves all child elements for a specific agent run", async () => {
489
+ const db = new MemSqlDb({
490
+ first: (sql) => {
491
+ if (sql.includes("agent_runs")) {
492
+ return {
493
+ id: "run-123",
494
+ project_id: "proj-1",
495
+ agent_id: "my-agent",
496
+ agent_name: "Agent 007",
497
+ agent_version: "1.2.3",
498
+ goal: "save the world",
499
+ outcome: "world saved",
500
+ autonomy_level: "autonomous_write",
501
+ status: "success",
502
+ error_message: null,
503
+ total_cost_usd: 0.1,
504
+ total_duration_ms: 5000,
505
+ metadata_json: "{}",
506
+ };
507
+ }
508
+ return null;
509
+ },
510
+ all: (sql) => {
511
+ if (
512
+ sql.includes("actions") &&
513
+ (sql.includes("root_action_id") || sql.includes("agent_run_id"))
514
+ ) {
515
+ return [
516
+ {
517
+ id: "action-root",
518
+ project_id: "proj-1",
519
+ root_action_id: "run-123",
520
+ caused_by_action_id: null,
521
+ actor_type: "agent",
522
+ actor_id: "my-agent",
523
+ action_kind: "agent.run",
524
+ name: "root-run",
525
+ status: "ok",
526
+ started_at: "2026-05-01T12:00:00Z",
527
+ ended_at: "2026-05-01T12:00:05Z",
528
+ duration_ms: 5000,
529
+ trace_id: "tx-123",
530
+ span_id: "sp-123",
531
+ session_id: "sess-123",
532
+ interaction_id: null,
533
+ user_id: null,
534
+ agent_run_id: "run-123",
535
+ step_id: null,
536
+ tool_call_id: null,
537
+ prompt_version: null,
538
+ model_name: null,
539
+ provider: null,
540
+ total_cost_usd: 0.1,
541
+ attrs_json: "{}",
542
+ },
543
+ ];
544
+ }
545
+ if (sql.includes("agent_runs")) {
546
+ return [
547
+ {
548
+ id: "run-123",
549
+ project_id: "proj-1",
550
+ agent_id: "my-agent",
551
+ agent_name: "Agent 007",
552
+ agent_version: "1.2.3",
553
+ goal: "save the world",
554
+ outcome: "world saved",
555
+ autonomy_level: "autonomous_write",
556
+ status: "success",
557
+ error_message: null,
558
+ total_cost_usd: 0.1,
559
+ total_duration_ms: 5000,
560
+ metadata_json: "{}",
561
+ },
562
+ ];
563
+ }
564
+ return [];
565
+ },
566
+ });
567
+
568
+ const index = new IdentityIndex(db);
569
+ const manifest = await index.byAgentRun("proj-1", "run-123");
570
+
571
+ expect(manifest.agentRuns).toHaveLength(1);
572
+ expect(manifest.agentRuns[0].agentId).toBe("my-agent");
573
+ expect(manifest.actions).toHaveLength(1);
574
+ expect(manifest.actions[0].id).toBe("action-root");
575
+ });
576
+ });
577
+
578
+ describe("IdentityIndex.byActor", () => {
579
+ it("retrieves recent actions and sub-elements for a specific actor", async () => {
580
+ const db = new MemSqlDb({
581
+ all: (sql) => {
582
+ if (sql.includes("actions") && sql.includes("actor_type")) {
583
+ return [
584
+ {
585
+ id: "action-actor",
586
+ project_id: "proj-1",
587
+ root_action_id: "run-abc",
588
+ caused_by_action_id: null,
589
+ actor_type: "user",
590
+ actor_id: "actor-123",
591
+ action_kind: "user.command",
592
+ name: "execute command",
593
+ status: "ok",
594
+ started_at: "2026-05-01T15:00:00Z",
595
+ ended_at: "2026-05-01T15:00:01Z",
596
+ duration_ms: 1000,
597
+ trace_id: null,
598
+ span_id: null,
599
+ session_id: null,
600
+ interaction_id: null,
601
+ user_id: null,
602
+ agent_run_id: null,
603
+ step_id: null,
604
+ tool_call_id: null,
605
+ prompt_version: null,
606
+ model_name: null,
607
+ provider: null,
608
+ total_cost_usd: null,
609
+ attrs_json: "{}",
610
+ },
611
+ ];
612
+ }
613
+ return [];
614
+ },
615
+ });
616
+
617
+ const index = new IdentityIndex(db);
618
+ const manifest = await index.byActor("proj-1", "user", "actor-123");
619
+
620
+ expect(manifest.actions).toHaveLength(1);
621
+ expect(manifest.actions[0].actorId).toBe("actor-123");
622
+ expect(manifest.actions[0].actorType).toBe("user");
623
+ });
624
+ });
@@ -0,0 +1,85 @@
1
+ import {
2
+ manifestByAction,
3
+ manifestByActor,
4
+ manifestByAgentRun,
5
+ } from "./identity-index/action-lookups";
6
+ import {
7
+ manifestByInteraction,
8
+ manifestBySession,
9
+ manifestByTrace,
10
+ } from "./identity-index/key-lookups";
11
+ import type {
12
+ EntityManifest,
13
+ EntityManifestExtended,
14
+ } from "./identity-index/types";
15
+ import { manifestByUser } from "./identity-index/user-lookup";
16
+ import type { SqlDb } from "./sql-db";
17
+
18
+ export type {
19
+ ActionRef,
20
+ AgentRunRef,
21
+ AICallRef,
22
+ ArtifactRef,
23
+ EntityManifest,
24
+ EntityManifestExtended,
25
+ EvalResultRef,
26
+ LogRef,
27
+ MetricExemplarRef,
28
+ ReplayRef,
29
+ RetrievalEventRef,
30
+ SpanRef,
31
+ ToolCallRef,
32
+ UsageEventRef,
33
+ } from "./identity-index/types";
34
+
35
+ export class IdentityIndex {
36
+ constructor(private readonly db: SqlDb) {}
37
+
38
+ async bySession(
39
+ projectId: string,
40
+ sessionId: string,
41
+ ): Promise<EntityManifest> {
42
+ return manifestBySession(this.db, projectId, sessionId);
43
+ }
44
+
45
+ async byTrace(projectId: string, traceId: string): Promise<EntityManifest> {
46
+ return manifestByTrace(this.db, projectId, traceId);
47
+ }
48
+
49
+ async byInteraction(
50
+ projectId: string,
51
+ interactionId: string,
52
+ ): Promise<EntityManifest> {
53
+ return manifestByInteraction(this.db, projectId, interactionId);
54
+ }
55
+
56
+ async byUser(
57
+ projectId: string,
58
+ userId: string,
59
+ opts: { limit?: number; sessions?: number } = {},
60
+ ): Promise<EntityManifest> {
61
+ return manifestByUser(this.db, projectId, userId, opts);
62
+ }
63
+
64
+ async byAction(
65
+ projectId: string,
66
+ actionId: string,
67
+ ): Promise<EntityManifestExtended> {
68
+ return manifestByAction(this.db, projectId, actionId);
69
+ }
70
+
71
+ async byAgentRun(
72
+ projectId: string,
73
+ agentRunId: string,
74
+ ): Promise<EntityManifestExtended> {
75
+ return manifestByAgentRun(this.db, projectId, agentRunId);
76
+ }
77
+
78
+ async byActor(
79
+ projectId: string,
80
+ actorType: string,
81
+ actorId: string,
82
+ ): Promise<EntityManifestExtended> {
83
+ return manifestByActor(this.db, projectId, actorType, actorId);
84
+ }
85
+ }