@obsunified/collector 2.1.1 → 2.2.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 (33) hide show
  1. package/dist/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
  2. package/dist/chunk-ZSF5XP3B.js.map +1 -0
  3. package/dist/index.d.ts +309 -2
  4. package/dist/index.js +4159 -256
  5. package/dist/index.js.map +1 -1
  6. package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
  7. package/package.json +2 -2
  8. package/src/__tests__/autosre-closed-loop.test.ts +1275 -0
  9. package/src/index.ts +43 -0
  10. package/src/lib/analyses-runner.ts +9 -0
  11. package/src/lib/autosre-change-correlation.ts +780 -0
  12. package/src/lib/autosre-findings.ts +725 -0
  13. package/src/lib/autosre-ontology.ts +914 -0
  14. package/src/lib/autosre-remediation-gate.ts +1663 -0
  15. package/src/lib/eval-cases-store.ts +63 -1
  16. package/src/lib/evidence-references.ts +6 -0
  17. package/src/lib/housekeeping.test.ts +80 -0
  18. package/src/lib/housekeeping.ts +101 -1
  19. package/src/lib/store/ingest.ts +94 -6
  20. package/src/migrations/050_autosre_causal_ontology.sql +216 -0
  21. package/src/migrations/alert-state-notified.test.ts +3 -3
  22. package/src/migrations/parity.test.ts +4 -0
  23. package/src/migrations/project-scoped-keys.test.ts +1 -1
  24. package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
  25. package/src/plugins/alerts-evaluator.ts +17 -1
  26. package/src/plugins/autosre-routes.ts +1019 -0
  27. package/src/plugins/connected-routes/manifest.ts +13 -1
  28. package/src/plugins/connected-routes.ts +271 -0
  29. package/src/plugins/default-span-enrichment.ts +13 -1
  30. package/src/plugins/eval-cases-routes.ts +57 -2
  31. package/src/plugins/evidence-retrieval-routes.ts +300 -4
  32. package/dist/chunk-XKQTVIRE.js.map +0 -1
  33. /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
@@ -76,6 +76,10 @@ export interface EvalRunCandidate {
76
76
  modelProvider: string | null;
77
77
  model: string | null;
78
78
  modelVersion: string | null;
79
+ environment?: string | null;
80
+ serviceVersion?: string | null;
81
+ commitSha?: string | null;
82
+ configDigest?: string | null;
79
83
  }
80
84
 
81
85
  export interface EvalRunSourceCase {
@@ -99,6 +103,12 @@ export interface EvalRun {
99
103
  evalCaseId: string | null;
100
104
  status: EvalRunStatus;
101
105
  candidate: EvalRunCandidate;
106
+ environment?: string;
107
+ serviceVersion?: string | null;
108
+ commitSha?: string | null;
109
+ configDigest?: string | null;
110
+ inputPayloadHash?: string | null;
111
+ failureFingerprint?: string | null;
102
112
  startedAt: string | null;
103
113
  endedAt: string | null;
104
114
  totalCount: number;
@@ -117,6 +127,12 @@ export interface EvalRunInput {
117
127
  evalCaseId?: string | null;
118
128
  status?: EvalRunStatus;
119
129
  candidate?: Partial<EvalRunCandidate>;
130
+ environment?: string | null;
131
+ serviceVersion?: string | null;
132
+ commitSha?: string | null;
133
+ configDigest?: string | null;
134
+ inputPayloadHash?: string | null;
135
+ failureFingerprint?: string | null;
120
136
  startedAt?: string | null;
121
137
  endedAt?: string | null;
122
138
  totalCount?: number;
@@ -144,6 +160,12 @@ interface EvalRunRow {
144
160
  candidate_model_provider: string | null;
145
161
  candidate_model: string | null;
146
162
  candidate_model_version: string | null;
163
+ environment?: string | null;
164
+ service_version?: string | null;
165
+ commit_sha?: string | null;
166
+ config_digest?: string | null;
167
+ input_payload_hash?: string | null;
168
+ failure_fingerprint?: string | null;
147
169
  status: string;
148
170
  started_at: string | null;
149
171
  ended_at: string | null;
@@ -412,7 +434,17 @@ const rowToEvalRun = (row: EvalRunRow): EvalRun => {
412
434
  modelProvider: row.candidate_model_provider ?? null,
413
435
  model: row.candidate_model ?? null,
414
436
  modelVersion: row.candidate_model_version ?? null,
437
+ environment: row.environment ?? "default",
438
+ serviceVersion: row.service_version ?? null,
439
+ commitSha: row.commit_sha ?? null,
440
+ configDigest: row.config_digest ?? null,
415
441
  },
442
+ environment: row.environment ?? "default",
443
+ serviceVersion: row.service_version ?? null,
444
+ commitSha: row.commit_sha ?? null,
445
+ configDigest: row.config_digest ?? null,
446
+ inputPayloadHash: row.input_payload_hash ?? null,
447
+ failureFingerprint: row.failure_fingerprint ?? null,
416
448
  startedAt: row.started_at ?? null,
417
449
  endedAt: row.ended_at ?? null,
418
450
  totalCount: row.total_count ?? 0,
@@ -560,6 +592,16 @@ export class EvalCasesStore {
560
592
  }
561
593
 
562
594
  const candidate = input.candidate ?? {};
595
+ const environment =
596
+ input.environment?.trim() || candidate.environment?.trim() || "default";
597
+ const serviceVersion =
598
+ input.serviceVersion?.trim() || candidate.serviceVersion?.trim() || null;
599
+ const commitSha =
600
+ input.commitSha?.trim() || candidate.commitSha?.trim() || null;
601
+ const configDigest =
602
+ input.configDigest?.trim() || candidate.configDigest?.trim() || null;
603
+ const inputPayloadHash = input.inputPayloadHash?.trim() || null;
604
+ const failureFingerprint = input.failureFingerprint?.trim() || null;
563
605
  const totalCount = nonNegativeInteger(input.totalCount);
564
606
  const passCount = nonNegativeInteger(input.passCount);
565
607
  const failCount = nonNegativeInteger(input.failCount);
@@ -580,10 +622,12 @@ export class EvalCasesStore {
580
622
  candidate_agent_id, candidate_agent_version,
581
623
  candidate_prompt_id, candidate_prompt_version,
582
624
  candidate_model_provider, candidate_model, candidate_model_version,
625
+ environment, service_version, commit_sha, config_digest,
626
+ input_payload_hash, failure_fingerprint,
583
627
  status, started_at, ended_at,
584
628
  total_count, pass_count, fail_count, average_score,
585
629
  metadata_json, created_at
586
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
630
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
587
631
  )
588
632
  .bind(
589
633
  id,
@@ -596,6 +640,12 @@ export class EvalCasesStore {
596
640
  candidate.modelProvider ?? null,
597
641
  candidate.model ?? null,
598
642
  candidate.modelVersion ?? null,
643
+ environment,
644
+ serviceVersion,
645
+ commitSha,
646
+ configDigest,
647
+ inputPayloadHash,
648
+ failureFingerprint,
599
649
  status,
600
650
  startedAt,
601
651
  endedAt,
@@ -679,6 +729,14 @@ export class EvalCasesStore {
679
729
  average_score = (
680
730
  SELECT AVG(score) FROM eval_case_results
681
731
  WHERE project_id = ? AND run_id = ? AND score IS NOT NULL
732
+ ),
733
+ input_payload_hash = COALESCE(
734
+ input_payload_hash,
735
+ (SELECT input_payload_hash FROM eval_case_results WHERE project_id = ? AND run_id = ? AND input_payload_hash IS NOT NULL LIMIT 1)
736
+ ),
737
+ failure_fingerprint = COALESCE(
738
+ failure_fingerprint,
739
+ (SELECT failure_fingerprint FROM eval_case_results WHERE project_id = ? AND run_id = ? AND failure_fingerprint IS NOT NULL LIMIT 1)
682
740
  )
683
741
  WHERE project_id = ? AND id = ?`,
684
742
  )
@@ -693,6 +751,10 @@ export class EvalCasesStore {
693
751
  id,
694
752
  projectId,
695
753
  id,
754
+ projectId,
755
+ id,
756
+ projectId,
757
+ id,
696
758
  )
697
759
  .run();
698
760
  }
@@ -146,6 +146,12 @@ export const evidenceRouteFor = (
146
146
  return traceRoute(entityId);
147
147
  case "docs":
148
148
  return entityId.startsWith("#/") ? entityId : `#/docs/${entityId}`;
149
+ case "change":
150
+ return `#/changes/${encodeURIComponent(entityId)}`;
151
+ case "finding":
152
+ return `#/findings/${encodeURIComponent(entityId)}`;
153
+ case "incident":
154
+ return `#/incidents/${encodeURIComponent(entityId)}`;
149
155
  }
150
156
  const exhaustive: never = entityKind;
151
157
  return exhaustive;
@@ -58,6 +58,12 @@ describe("purgeSecondaryRows", () => {
58
58
  metric_series: 1,
59
59
  evidence_refs: 1,
60
60
  evidence_ref_expansions: 1,
61
+ ontology_edges: 0,
62
+ ontology_nodes: 0,
63
+ change_events: 0,
64
+ incident_investigations: 0,
65
+ remediation_proposals: 0,
66
+ investigation_findings: 0,
61
67
  });
62
68
  const ids = (sql: string) =>
63
69
  (raw.prepare(sql).all() as Array<{ id: string }>).map((r) => r.id).sort();
@@ -70,4 +76,78 @@ describe("purgeSecondaryRows", () => {
70
76
  expect(ids("SELECT id FROM evidence_ref_expansions")).toEqual(["e2"]);
71
77
  db.close();
72
78
  });
79
+
80
+ it("enforces RFC 0013 AutoSRE retention windows while preserving counterfactual_verified and operationally_corroborated findings", async () => {
81
+ const db = createMigratedSqliteDb();
82
+ const raw = db.raw;
83
+ raw.exec(`
84
+ INSERT INTO projects (id, name, slug, created_at) VALUES ('alpha', 'A', 'a', '${iso(3000)}');
85
+ `);
86
+ // Ontology edges/nodes (30d = 720h)
87
+ raw
88
+ .prepare(
89
+ `INSERT INTO ontology_nodes (id, project_id, environment, kind, name, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'service', ?, ?, ?)`,
90
+ )
91
+ .run("n-old", "old-svc", iso(800), iso(750));
92
+ raw
93
+ .prepare(
94
+ `INSERT INTO ontology_nodes (id, project_id, environment, kind, name, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'service', ?, ?, ?)`,
95
+ )
96
+ .run("n-fresh", "fresh-svc", iso(100), iso(10));
97
+ raw
98
+ .prepare(
99
+ `INSERT INTO ontology_edges (id, project_id, environment, source_node_id, predicate, target_node_id, relationship_certainty, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'n-old', 'calls', 'n-fresh', 'exact', ?, ?)`,
100
+ )
101
+ .run("edge-old", iso(800), iso(750));
102
+
103
+ // Change events (90d = 2160h)
104
+ raw
105
+ .prepare(
106
+ `INSERT INTO change_events (id, project_id, environment, kind, source, target_node_id, summary, occurred_at, received_at) VALUES (?, 'alpha', 'prod', 'deploy', 'ci_webhook', 'n-fresh', 'old deploy', ?, ?)`,
107
+ )
108
+ .run("chg-old", iso(2200), iso(2200));
109
+ raw
110
+ .prepare(
111
+ `INSERT INTO change_events (id, project_id, environment, kind, source, target_node_id, summary, occurred_at, received_at) VALUES (?, 'alpha', 'prod', 'deploy', 'ci_webhook', 'n-fresh', 'recent deploy', ?, ?)`,
112
+ )
113
+ .run("chg-recent", iso(100), iso(100));
114
+
115
+ // Findings (90d = 2160h, unless counterfactual_verified or operationally_corroborated)
116
+ const insFinding = raw.prepare(
117
+ `INSERT INTO investigation_findings (id, project_id, environment, fingerprint, fingerprint_input_json, title, summary, causal_proof_tier, causal_verification_status, confidence, confidence_breakdown_json, symptom_entity_kind, symptom_entity_id, fault_locus_entity_kind, fault_locus_entity_id, causal_chain_json, cited_evidence_ids_json, actor_type, actor_id, created_at, updated_at) VALUES (?, 'alpha', 'prod', 'fp_1', '{}', 't', 's', 'corroborated_candidate', ?, 0.8, '{}', 'trace', 't1', 'span', 's1', '[]', '["t1"]', 'agent', 'autosre', ?, ?)`,
118
+ );
119
+ insFinding.run("fnd-old-unverified", "unverified", iso(2200), iso(2200));
120
+ insFinding.run(
121
+ "fnd-old-verified",
122
+ "counterfactual_verified",
123
+ iso(2200),
124
+ iso(2200),
125
+ );
126
+ insFinding.run(
127
+ "fnd-old-corroborated",
128
+ "operationally_corroborated",
129
+ iso(2200),
130
+ iso(2200),
131
+ );
132
+
133
+ const summary = await purgeSecondaryRows(db, {
134
+ retentionHours: 72,
135
+ now: () => NOW,
136
+ });
137
+ expect(summary.ontology_edges).toBe(1);
138
+ expect(summary.ontology_nodes).toBe(1);
139
+ expect(summary.change_events).toBe(1);
140
+ expect(summary.investigation_findings).toBe(1);
141
+
142
+ const remainingFindings = (
143
+ raw
144
+ .prepare(`SELECT id FROM investigation_findings ORDER BY id ASC`)
145
+ .all() as Array<{ id: string }>
146
+ ).map((r) => r.id);
147
+ expect(remainingFindings).toEqual([
148
+ "fnd-old-corroborated",
149
+ "fnd-old-verified",
150
+ ]);
151
+ db.close();
152
+ });
73
153
  });
@@ -13,6 +13,7 @@
13
13
  * the signal they reference); expansions follow their ref.
14
14
  */
15
15
 
16
+ import { evaluateRemediationWatchdogs } from "./autosre-remediation-gate";
16
17
  import type { SqlDb } from "./sql-db";
17
18
 
18
19
  export const ALERT_EVALUATIONS_MAX_AGE_HOURS = 720;
@@ -24,6 +25,12 @@ export interface HousekeepingSummary {
24
25
  metric_series: number;
25
26
  evidence_refs: number;
26
27
  evidence_ref_expansions: number;
28
+ ontology_edges?: number;
29
+ ontology_nodes?: number;
30
+ change_events?: number;
31
+ incident_investigations?: number;
32
+ investigation_findings?: number;
33
+ remediation_proposals?: number;
27
34
  }
28
35
 
29
36
  export interface HousekeepingOptions {
@@ -42,6 +49,7 @@ export async function purgeSecondaryRows(
42
49
  const nowMs = options.now?.() ?? Date.now();
43
50
  const isoBefore = (hours: number) =>
44
51
  new Date(nowMs - hours * 3_600_000).toISOString();
52
+ const nowIso = isoBefore(0);
45
53
 
46
54
  const alertEvaluations = await db
47
55
  .prepare(`DELETE FROM alert_evaluations WHERE evaluated_at < ?`)
@@ -71,7 +79,7 @@ export async function purgeSecondaryRows(
71
79
  `DELETE FROM evidence_retrieval_refs
72
80
  WHERE expires_at IS NOT NULL AND expires_at < ?`,
73
81
  )
74
- .bind(isoBefore(0))
82
+ .bind(nowIso)
75
83
  .run();
76
84
  const expansions = await db
77
85
  .prepare(
@@ -83,11 +91,103 @@ export async function purgeSecondaryRows(
83
91
  )
84
92
  .run();
85
93
 
94
+ // RFC 0013 — AutoSRE ontology, change events, incident investigations,
95
+ // investigation findings, and remediation proposals retention + watchdog.
96
+ const thirtyDaysAgoIso = isoBefore(30 * 24);
97
+ const ninetyDaysAgoIso = isoBefore(90 * 24);
98
+ let ontologyEdgesPurged = 0;
99
+ let ontologyNodesPurged = 0;
100
+ let changeEventsPurged = 0;
101
+ let incidentInvestigationsPurged = 0;
102
+ let investigationFindingsPurged = 0;
103
+ let remediationProposalsPurged = 0;
104
+ try {
105
+ const edgesRes = await db
106
+ .prepare(`DELETE FROM ontology_edges WHERE last_seen_at < ?`)
107
+ .bind(thirtyDaysAgoIso)
108
+ .run();
109
+ ontologyEdgesPurged = changes(edgesRes);
110
+
111
+ const nodesRes = await db
112
+ .prepare(
113
+ `DELETE FROM ontology_nodes
114
+ WHERE last_seen_at < ?
115
+ AND NOT EXISTS (
116
+ SELECT 1 FROM ontology_edges e
117
+ WHERE e.project_id = ontology_nodes.project_id
118
+ AND e.environment = ontology_nodes.environment
119
+ AND (e.source_node_id = ontology_nodes.id OR e.target_node_id = ontology_nodes.id)
120
+ )`,
121
+ )
122
+ .bind(thirtyDaysAgoIso)
123
+ .run();
124
+ ontologyNodesPurged = changes(nodesRes);
125
+
126
+ const proposalsRes = await db
127
+ .prepare(
128
+ `DELETE FROM remediation_proposals
129
+ WHERE (expires_at IS NOT NULL AND expires_at < ?)
130
+ OR (expires_at IS NULL AND created_at < ?)`,
131
+ )
132
+ .bind(nowIso, ninetyDaysAgoIso)
133
+ .run();
134
+ remediationProposalsPurged = changes(proposalsRes);
135
+
136
+ const findingsRes = await db
137
+ .prepare(
138
+ `DELETE FROM investigation_findings
139
+ WHERE causal_verification_status NOT IN ('counterfactual_verified', 'operationally_corroborated')
140
+ AND ((expires_at IS NOT NULL AND expires_at < ?)
141
+ OR (expires_at IS NULL AND created_at < ?))`,
142
+ )
143
+ .bind(nowIso, ninetyDaysAgoIso)
144
+ .run();
145
+ investigationFindingsPurged = changes(findingsRes);
146
+
147
+ // Preserve change_events referenced by active investigation_findings
148
+ const changesRes = await db
149
+ .prepare(
150
+ `DELETE FROM change_events
151
+ WHERE ((expires_at IS NOT NULL AND expires_at < ?)
152
+ OR (expires_at IS NULL AND occurred_at < ?))
153
+ AND NOT EXISTS (
154
+ SELECT 1 FROM investigation_findings f
155
+ WHERE f.project_id = change_events.project_id
156
+ AND f.environment = change_events.environment
157
+ AND f.contributing_change_id = change_events.id
158
+ )`,
159
+ )
160
+ .bind(nowIso, ninetyDaysAgoIso)
161
+ .run();
162
+ changeEventsPurged = changes(changesRes);
163
+
164
+ const incidentsRes = await db
165
+ .prepare(
166
+ `DELETE FROM incident_investigations
167
+ WHERE status IN ('resolved', 'escalated')
168
+ AND ((expires_at IS NOT NULL AND expires_at < ?)
169
+ OR (expires_at IS NULL AND updated_at < ?))`,
170
+ )
171
+ .bind(nowIso, ninetyDaysAgoIso)
172
+ .run();
173
+ incidentInvestigationsPurged = changes(incidentsRes);
174
+
175
+ await evaluateRemediationWatchdogs(db, { nowIso });
176
+ } catch {
177
+ // Gracefully ignore when running against partial test schemas without 050
178
+ }
179
+
86
180
  return {
87
181
  alert_evaluations: changes(alertEvaluations),
88
182
  ask_evidence_events: changes(askEvents),
89
183
  metric_series: changes(series),
90
184
  evidence_refs: changes(refs),
91
185
  evidence_ref_expansions: changes(expansions),
186
+ ontology_edges: ontologyEdgesPurged,
187
+ ontology_nodes: ontologyNodesPurged,
188
+ change_events: changeEventsPurged,
189
+ incident_investigations: incidentInvestigationsPurged,
190
+ investigation_findings: investigationFindingsPurged,
191
+ remediation_proposals: remediationProposalsPurged,
92
192
  };
93
193
  }
@@ -1,4 +1,8 @@
1
1
  import type { StoredSpan } from "@obsunified/types";
2
+ import {
3
+ extractSpanLineageAndTopology,
4
+ upsertOntologyFromSpans,
5
+ } from "../autosre-ontology";
2
6
  import type { SqlDb } from "../sql-db";
3
7
 
4
8
  export async function ingestTelemetrySpans(
@@ -7,10 +11,29 @@ export async function ingestTelemetrySpans(
7
11
  ): Promise<{ inserted: number; traceCount: number }> {
8
12
  if (spans.length === 0) return { inserted: 0, traceCount: 0 };
9
13
 
10
- const statements = spans.map((span) => {
14
+ const enrichedSpans = spans.map((span) => {
11
15
  if (!span.projectId)
12
16
  throw new Error("TelemetryStore.ingest: span.projectId is required");
13
- return db
17
+ if (
18
+ span.environment !== undefined &&
19
+ span.serviceVersion !== undefined &&
20
+ span.targetNodeId !== undefined
21
+ ) {
22
+ return span;
23
+ }
24
+ const lineage = extractSpanLineageAndTopology(span);
25
+ return {
26
+ ...span,
27
+ environment: span.environment ?? lineage.environment,
28
+ serviceVersion: span.serviceVersion ?? lineage.serviceVersion,
29
+ commitSha: span.commitSha ?? lineage.commitSha,
30
+ targetNodeId: span.targetNodeId ?? lineage.targetNode?.id ?? null,
31
+ errorSignature: span.errorSignature ?? lineage.errorSignature,
32
+ };
33
+ });
34
+
35
+ const statements = enrichedSpans.map((span) =>
36
+ db
14
37
  .prepare(`
15
38
  INSERT OR IGNORE INTO telemetry_spans (
16
39
  project_id, trace_id, span_id, parent_span_id, trace_state,
@@ -19,8 +42,9 @@ export async function ingestTelemetrySpans(
19
42
  attributes_json, dropped_attributes_count,
20
43
  resource_attributes_json, events_json, dropped_events_count,
21
44
  links_json, dropped_links_count, received_at, expires_at,
22
- session_id, interaction_id, telemetry_sdk_name
23
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
45
+ session_id, interaction_id, telemetry_sdk_name,
46
+ environment, service_version, commit_sha, target_node_id, error_signature
47
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
24
48
  `)
25
49
  .bind(
26
50
  span.projectId,
@@ -50,10 +74,74 @@ export async function ingestTelemetrySpans(
50
74
  span.sessionId ?? null,
51
75
  span.interactionId ?? null,
52
76
  span.telemetrySdkName ?? null,
77
+ span.environment ?? "default",
78
+ span.serviceVersion ?? null,
79
+ span.commitSha ?? null,
80
+ span.targetNodeId ?? null,
81
+ span.errorSignature ?? null,
82
+ ),
83
+ );
84
+
85
+ try {
86
+ await db.batch(statements);
87
+ } catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ if (msg.includes("no such column: environment")) {
90
+ // Fallback for unit tests running against pre-050 partial schemas
91
+ const legacyStatements = enrichedSpans.map((span) =>
92
+ db
93
+ .prepare(`
94
+ INSERT OR IGNORE INTO telemetry_spans (
95
+ project_id, trace_id, span_id, parent_span_id, trace_state,
96
+ service_name, scope_name, scope_version, span_name, span_kind,
97
+ status_code, status_message, start_time, end_time, duration_ms,
98
+ attributes_json, dropped_attributes_count,
99
+ resource_attributes_json, events_json, dropped_events_count,
100
+ links_json, dropped_links_count, received_at, expires_at,
101
+ session_id, interaction_id, telemetry_sdk_name
102
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
103
+ `)
104
+ .bind(
105
+ span.projectId,
106
+ span.traceId,
107
+ span.spanId,
108
+ span.parentSpanId,
109
+ span.traceState,
110
+ span.serviceName,
111
+ span.scopeName,
112
+ span.scopeVersion,
113
+ span.spanName,
114
+ span.spanKind,
115
+ span.statusCode,
116
+ span.statusMessage,
117
+ span.startTime,
118
+ span.endTime,
119
+ span.durationMs,
120
+ span.attributesJson,
121
+ span.droppedAttributesCount,
122
+ span.resourceAttributesJson,
123
+ span.eventsJson,
124
+ span.droppedEventsCount,
125
+ span.linksJson,
126
+ span.droppedLinksCount,
127
+ span.receivedAt,
128
+ span.expiresAt,
129
+ span.sessionId ?? null,
130
+ span.interactionId ?? null,
131
+ span.telemetrySdkName ?? null,
132
+ ),
53
133
  );
54
- });
134
+ await db.batch(legacyStatements);
135
+ } else {
136
+ throw err;
137
+ }
138
+ }
55
139
 
56
- await db.batch(statements);
140
+ try {
141
+ await upsertOntologyFromSpans(db, enrichedSpans);
142
+ } catch {
143
+ // Non-fatal on partial-schema unit test DBs
144
+ }
57
145
 
58
146
  // Trace instrumentation gaps are computed lazily at read time (see
59
147
  // `getTelemetryTraceGaps`), not materialized on ingest. This keeps the