@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,410 @@
1
+ import type { TelemetrySpanDetail } from "@obsunified/types";
2
+ import { describe, expect, it } from "vitest";
3
+ import { MemSqlDb } from "../test-utils/mem-sql-db";
4
+ import { ingestTelemetrySpans } from "./ingest";
5
+ import {
6
+ buildTraceInstrumentationGaps,
7
+ calibrateTelemetryInstrumentationGaps,
8
+ extractCodeReference,
9
+ getTelemetryTraceGaps,
10
+ } from "./trace-detail";
11
+
12
+ const span = (
13
+ overrides: Partial<TelemetrySpanDetail>,
14
+ ): TelemetrySpanDetail => ({
15
+ traceId: "trace-1",
16
+ spanId: "span-1",
17
+ parentSpanId: null,
18
+ serviceName: "api",
19
+ scopeName: null,
20
+ scopeVersion: null,
21
+ spanName: "GET /checkout",
22
+ spanKind: 2,
23
+ statusCode: 1,
24
+ statusMessage: null,
25
+ startTime: "2026-06-01T00:00:00.000Z",
26
+ endTime: "2026-06-01T00:00:00.500Z",
27
+ durationMs: 500,
28
+ attributes: {},
29
+ resourceAttributes: {},
30
+ events: [],
31
+ links: [],
32
+ ...overrides,
33
+ });
34
+
35
+ describe("buildTraceInstrumentationGaps", () => {
36
+ it("surfaces high self-time spans as structured blindspots", () => {
37
+ const gaps = buildTraceInstrumentationGaps(
38
+ "trace-1",
39
+ [
40
+ span({
41
+ spanId: "root",
42
+ spanName: "inventory.reserve",
43
+ durationMs: 540,
44
+ }),
45
+ span({
46
+ spanId: "child",
47
+ parentSpanId: "root",
48
+ spanName: "cache.get",
49
+ durationMs: 40,
50
+ }),
51
+ ],
52
+ 540,
53
+ "2026-06-01T00:00:01.000Z",
54
+ );
55
+
56
+ expect(gaps).toMatchObject({
57
+ traceId: "trace-1",
58
+ totalDurationMs: 540,
59
+ uninstrumentedTimeMs: 500,
60
+ ratio: 500 / 540,
61
+ timestamp: "2026-06-01T00:00:01.000Z",
62
+ });
63
+ expect(gaps.blindspots).toEqual([
64
+ expect.objectContaining({
65
+ parentSpanId: "root",
66
+ parentServiceName: "api",
67
+ parentSpanName: "inventory.reserve",
68
+ durationMs: 500,
69
+ childSpanCount: 1,
70
+ asyncParent: false,
71
+ thresholdVersion: "demo-calibrated-2026-06-03",
72
+ }),
73
+ ]);
74
+ expect(gaps.thresholds).toMatchObject({
75
+ version: "demo-calibrated-2026-06-03",
76
+ minDurationMs: 100,
77
+ minSelfRatio: 0.7,
78
+ maxChildSpanCount: 1,
79
+ excludedSpanKinds: [3, 4, 5],
80
+ });
81
+ });
82
+
83
+ it("does not flag async fan-out parents where child wall time exceeds parent", () => {
84
+ const gaps = buildTraceInstrumentationGaps(
85
+ "trace-1",
86
+ [
87
+ span({ spanId: "root", durationMs: 100 }),
88
+ span({ spanId: "child-a", parentSpanId: "root", durationMs: 80 }),
89
+ span({ spanId: "child-b", parentSpanId: "root", durationMs: 80 }),
90
+ ],
91
+ 100,
92
+ );
93
+
94
+ expect(gaps.uninstrumentedTimeMs).toBe(0);
95
+ expect(gaps.blindspots).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe("calibrateTelemetryInstrumentationGaps", () => {
100
+ it("runs the calibrated rule over recent demo traces and reports firing rate", async () => {
101
+ const db = new MemSqlDb({
102
+ all: (sql) => {
103
+ if (!sql.includes("FROM telemetry_spans")) return [];
104
+ return [
105
+ {
106
+ project_id: "p1",
107
+ trace_id: "trace-missing",
108
+ span_id: "root",
109
+ parent_span_id: null,
110
+ service_name: "checkout-api",
111
+ scope_name: null,
112
+ scope_version: null,
113
+ span_name: "POST /api/checkout",
114
+ span_kind: 2,
115
+ status_code: 1,
116
+ status_message: null,
117
+ duration_ms: 540,
118
+ start_time: "2026-06-01T00:00:00.000Z",
119
+ end_time: "2026-06-01T00:00:00.540Z",
120
+ received_at: "2026-06-01T00:00:01.000Z",
121
+ attributes_json: "{}",
122
+ resource_attributes_json: "{}",
123
+ events_json: "[]",
124
+ links_json: "[]",
125
+ },
126
+ {
127
+ project_id: "p1",
128
+ trace_id: "trace-missing",
129
+ span_id: "child",
130
+ parent_span_id: "root",
131
+ service_name: "checkout-api",
132
+ scope_name: null,
133
+ scope_version: null,
134
+ span_name: "cache.get",
135
+ span_kind: 3,
136
+ status_code: 1,
137
+ status_message: null,
138
+ duration_ms: 40,
139
+ start_time: "2026-06-01T00:00:00.010Z",
140
+ end_time: "2026-06-01T00:00:00.050Z",
141
+ received_at: "2026-06-01T00:00:01.000Z",
142
+ attributes_json: "{}",
143
+ resource_attributes_json: "{}",
144
+ events_json: "[]",
145
+ links_json: "[]",
146
+ },
147
+ {
148
+ project_id: "p1",
149
+ trace_id: "trace-dense",
150
+ span_id: "root",
151
+ parent_span_id: null,
152
+ service_name: "checkout-api",
153
+ scope_name: null,
154
+ scope_version: null,
155
+ span_name: "POST /api/checkout",
156
+ span_kind: 2,
157
+ status_code: 1,
158
+ status_message: null,
159
+ duration_ms: 540,
160
+ start_time: "2026-06-01T00:01:00.000Z",
161
+ end_time: "2026-06-01T00:01:00.540Z",
162
+ received_at: "2026-06-01T00:01:01.000Z",
163
+ attributes_json: "{}",
164
+ resource_attributes_json: "{}",
165
+ events_json: "[]",
166
+ links_json: "[]",
167
+ },
168
+ {
169
+ project_id: "p1",
170
+ trace_id: "trace-dense",
171
+ span_id: "child-a",
172
+ parent_span_id: "root",
173
+ service_name: "checkout-api",
174
+ scope_name: null,
175
+ scope_version: null,
176
+ span_name: "payment.charge",
177
+ span_kind: 3,
178
+ status_code: 1,
179
+ status_message: null,
180
+ duration_ms: 220,
181
+ start_time: "2026-06-01T00:01:00.010Z",
182
+ end_time: "2026-06-01T00:01:00.230Z",
183
+ received_at: "2026-06-01T00:01:01.000Z",
184
+ attributes_json: "{}",
185
+ resource_attributes_json: "{}",
186
+ events_json: "[]",
187
+ links_json: "[]",
188
+ },
189
+ {
190
+ project_id: "p1",
191
+ trace_id: "trace-dense",
192
+ span_id: "child-b",
193
+ parent_span_id: "root",
194
+ service_name: "checkout-api",
195
+ scope_name: null,
196
+ scope_version: null,
197
+ span_name: "db.query",
198
+ span_kind: 3,
199
+ status_code: 1,
200
+ status_message: null,
201
+ duration_ms: 180,
202
+ start_time: "2026-06-01T00:01:00.240Z",
203
+ end_time: "2026-06-01T00:01:00.420Z",
204
+ received_at: "2026-06-01T00:01:01.000Z",
205
+ attributes_json: "{}",
206
+ resource_attributes_json: "{}",
207
+ events_json: "[]",
208
+ links_json: "[]",
209
+ },
210
+ ];
211
+ },
212
+ });
213
+
214
+ const calibration = await calibrateTelemetryInstrumentationGaps(db, {
215
+ projectId: "p1",
216
+ hours: 72,
217
+ });
218
+
219
+ expect(calibration).toMatchObject({
220
+ thresholds: { version: "demo-calibrated-2026-06-03" },
221
+ sampledSpanCount: 5,
222
+ traceCount: 2,
223
+ flaggedTraceCount: 1,
224
+ blindspotCount: 1,
225
+ flaggedTraceRate: 0.5,
226
+ status: "noisy",
227
+ });
228
+ expect(calibration.topTraces[0]).toMatchObject({
229
+ traceId: "trace-missing",
230
+ blindspotCount: 1,
231
+ topBlindspot: expect.objectContaining({
232
+ parentSpanName: "POST /api/checkout",
233
+ durationMs: 500,
234
+ }),
235
+ });
236
+ });
237
+ });
238
+
239
+ describe("extractCodeReference", () => {
240
+ it("extracts environment-neutral code metadata from span attributes", () => {
241
+ expect(
242
+ extractCodeReference({
243
+ "code.filepath": "packages/api/src/checkout.ts",
244
+ "code.function": "reserveInventory",
245
+ "code.lineno": "42",
246
+ "code.column": 7,
247
+ "code.repository": "obs-unified",
248
+ }),
249
+ ).toEqual({
250
+ repoName: "obs-unified",
251
+ originalPath: "packages/api/src/checkout.ts",
252
+ relativePath: "packages/api/src/checkout.ts",
253
+ symbolName: "reserveInventory",
254
+ lineNumber: 42,
255
+ columnNumber: 7,
256
+ });
257
+ });
258
+
259
+ it("keeps absolute paths separate from repo-relative paths", () => {
260
+ expect(
261
+ extractCodeReference({
262
+ "code.filepath": "/Users/example/project/src/index.ts",
263
+ "code.lineno": 12,
264
+ }),
265
+ ).toEqual({
266
+ originalPath: "/Users/example/project/src/index.ts",
267
+ absolutePath: "/Users/example/project/src/index.ts",
268
+ lineNumber: 12,
269
+ });
270
+ });
271
+ });
272
+
273
+ describe("lazy (read-time) gaps", () => {
274
+ it("does not materialize gaps on ingest, and computes them on read", async () => {
275
+ const insertedSpans: unknown[][] = [];
276
+ const deletedTraces: string[] = [];
277
+ const insertedGaps: Array<{
278
+ traceId: string;
279
+ parentSpanId: string;
280
+ parentServiceName: string;
281
+ parentSpanName: string;
282
+ offsetMs: number;
283
+ durationMs: number;
284
+ ratioOfParent: number;
285
+ childSpanCount: number;
286
+ asyncParent: boolean;
287
+ recommendation: string;
288
+ }> = [];
289
+
290
+ const db = new MemSqlDb({
291
+ all: (sql) => {
292
+ if (sql.includes("FROM telemetry_spans")) {
293
+ return [
294
+ {
295
+ project_id: "p1",
296
+ trace_id: "trace-ingest",
297
+ span_id: "root",
298
+ parent_span_id: null,
299
+ service_name: "api",
300
+ span_name: "inventory.reserve",
301
+ duration_ms: 540,
302
+ start_time: "2026-06-01T00:00:00.000Z",
303
+ end_time: "2026-06-01T00:00:00.540Z",
304
+ attributes_json: "{}",
305
+ resource_attributes_json: "{}",
306
+ events_json: "[]",
307
+ links_json: "[]",
308
+ },
309
+ {
310
+ project_id: "p1",
311
+ trace_id: "trace-ingest",
312
+ span_id: "child",
313
+ parent_span_id: "root",
314
+ service_name: "api",
315
+ span_name: "cache.get",
316
+ duration_ms: 40,
317
+ start_time: "2026-06-01T00:00:00.000Z",
318
+ end_time: "2026-06-01T00:00:00.040Z",
319
+ attributes_json: "{}",
320
+ resource_attributes_json: "{}",
321
+ events_json: "[]",
322
+ links_json: "[]",
323
+ },
324
+ ];
325
+ }
326
+ if (sql.includes("FROM trace_instrumentation_gaps")) {
327
+ return insertedGaps.map((g) => ({
328
+ parent_span_id: g.parentSpanId,
329
+ parent_service_name: g.parentServiceName,
330
+ parent_span_name: g.parentSpanName,
331
+ offset_ms: g.offsetMs,
332
+ duration_ms: g.durationMs,
333
+ ratio_of_parent: g.ratioOfParent,
334
+ child_span_count: g.childSpanCount,
335
+ async_parent: g.asyncParent ? 1 : 0,
336
+ recommendation: g.recommendation,
337
+ }));
338
+ }
339
+ return [];
340
+ },
341
+ run: (sql, binds: unknown[]) => {
342
+ if (sql.includes("INSERT OR IGNORE INTO telemetry_spans")) {
343
+ insertedSpans.push(binds);
344
+ } else if (sql.includes("DELETE FROM trace_instrumentation_gaps")) {
345
+ deletedTraces.push(String(binds[0]));
346
+ } else if (sql.includes("INSERT INTO trace_instrumentation_gaps")) {
347
+ insertedGaps.push({
348
+ traceId: String(binds[0]),
349
+ parentSpanId: String(binds[1]),
350
+ parentServiceName: String(binds[2]),
351
+ parentSpanName: String(binds[3]),
352
+ offsetMs: Number(binds[4]),
353
+ durationMs: Number(binds[5]),
354
+ ratioOfParent: Number(binds[6]),
355
+ childSpanCount: Number(binds[7]),
356
+ asyncParent: binds[8] === 1,
357
+ recommendation: String(binds[9]),
358
+ });
359
+ }
360
+ return { changes: 1 };
361
+ },
362
+ });
363
+
364
+ const spansToIngest = [
365
+ {
366
+ projectId: "p1",
367
+ traceId: "trace-ingest",
368
+ spanId: "root",
369
+ parentSpanId: null,
370
+ traceState: null,
371
+ serviceName: "api",
372
+ scopeName: null,
373
+ scopeVersion: null,
374
+ spanName: "inventory.reserve",
375
+ spanKind: 2,
376
+ statusCode: 1,
377
+ statusMessage: null,
378
+ startTime: "2026-06-01T00:00:00.000Z",
379
+ endTime: "2026-06-01T00:00:00.540Z",
380
+ durationMs: 540,
381
+ attributesJson: "{}",
382
+ droppedAttributesCount: 0,
383
+ resourceAttributesJson: "{}",
384
+ eventsJson: "[]",
385
+ droppedEventsCount: 0,
386
+ linksJson: "[]",
387
+ droppedLinksCount: 0,
388
+ receivedAt: "2026-06-01T00:00:01.000Z",
389
+ expiresAt: "2026-06-02T00:00:00.000Z",
390
+ },
391
+ ];
392
+
393
+ await ingestTelemetrySpans(db, spansToIngest);
394
+
395
+ // Assert spans were written
396
+ expect(insertedSpans).toHaveLength(1);
397
+ expect(insertedSpans[0][1]).toBe("trace-ingest");
398
+
399
+ // Gaps are no longer materialized on the ingest hot path: ingest must
400
+ // not touch the trace_instrumentation_gaps table at all.
401
+ expect(deletedTraces).toHaveLength(0);
402
+ expect(insertedGaps).toHaveLength(0);
403
+
404
+ // They are computed on demand from the trace's spans at read time.
405
+ const gaps = await getTelemetryTraceGaps(db, "trace-ingest", "p1");
406
+ expect(gaps).toBeDefined();
407
+ expect(gaps?.blindspots).toHaveLength(1);
408
+ expect(gaps?.blindspots[0].durationMs).toBe(500);
409
+ });
410
+ });