@obsunified/collector 2.2.3 → 2.3.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 (32) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
  3. package/dist/chunk-RASSTGNB.js.map +1 -0
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2562 -2311
  6. package/dist/index.js.map +1 -1
  7. package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -1
  8. package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
  9. package/package.json +4 -2
  10. package/src/__tests__/autosre-closed-loop.test.ts +110 -0
  11. package/src/__tests__/db-parity.integration.test.ts +961 -0
  12. package/src/__tests__/internal-contracts-routes.test.ts +451 -0
  13. package/src/framework/env.ts +105 -0
  14. package/src/index.ts +13 -4
  15. package/src/lib/ai-store/evaluations.test.ts +3 -3
  16. package/src/lib/analyses-store.test.ts +6 -6
  17. package/src/lib/db-parity.ts +883 -0
  18. package/src/lib/eval-cases-store.test.ts +1 -1
  19. package/src/lib/eval-cases-store.ts +16 -4
  20. package/src/lib/evidence-references.ts +30 -22
  21. package/src/lib/stage6.test.ts +1 -1
  22. package/src/migrations/parity.test.ts +105 -3
  23. package/src/plugins/ai-receiver.ts +6 -1
  24. package/src/plugins/autosre-routes.ts +251 -140
  25. package/src/plugins/connected-routes/manifest.test.ts +114 -32
  26. package/src/plugins/connected-routes/manifest.ts +59 -76
  27. package/src/plugins/connected-routes.test.ts +10 -8
  28. package/src/plugins/connected-routes.ts +42 -12
  29. package/src/plugins/eval-cases-routes.ts +33 -10
  30. package/src/plugins/evidence-retrieval-routes.ts +60 -25
  31. package/dist/chunk-ZSF5XP3B.js.map +0 -1
  32. /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
@@ -91,7 +91,7 @@ describe("EvalCasesStore", () => {
91
91
  expect.objectContaining({
92
92
  entityKind: "span",
93
93
  entityId: "trace-1:span-1",
94
- route: "#/traces/trace-1#span=span-1",
94
+ route: "#/traces?trace=trace-1&span=span-1",
95
95
  }),
96
96
  ]),
97
97
  );
@@ -3,7 +3,10 @@ import type {
3
3
  EvidenceReferenceContract as EvidenceReferenceContractType,
4
4
  JsonValue,
5
5
  } from "@obsunified/types";
6
- import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
6
+ import {
7
+ dashboardRouteToHref,
8
+ EVIDENCE_REFERENCE_CONTRACT,
9
+ } from "@obsunified/types";
7
10
  import { sourceLinkEvidenceReferences } from "./evidence-references";
8
11
  import { randomHex } from "./hash";
9
12
  import { parseJsonArray, parseJsonValue } from "./json";
@@ -345,7 +348,10 @@ const evalCaseEvidenceReferences = (evalCase: EvalCase): EvidenceReference[] =>
345
348
  sourceLabel: `Eval case "${evalCase.name}"`,
346
349
  sourceId: evalCase.id,
347
350
  sourceKind: "eval_case",
348
- sourceRoute: `#/evaluations?case=${encodeURIComponent(evalCase.id)}`,
351
+ sourceRoute: dashboardRouteToHref({
352
+ tab: "evaluations",
353
+ evaluationId: evalCase.id,
354
+ }),
349
355
  sourceName: evalCase.name,
350
356
  },
351
357
  evalCase,
@@ -384,7 +390,10 @@ const evalRunEvidenceReferences = (run: EvalRun): EvidenceReference[] => {
384
390
  sourceLabel: `Eval run "${run.id}"`,
385
391
  sourceId: run.id,
386
392
  sourceKind: "eval_run",
387
- sourceRoute: `#/evaluations?run=${encodeURIComponent(run.id)}`,
393
+ sourceRoute: dashboardRouteToHref({
394
+ tab: "evaluations",
395
+ evaluationId: run.evalCaseId ?? run.id,
396
+ }),
388
397
  sourceName: run.sourceEvalCase.name,
389
398
  },
390
399
  run.sourceEvalCase,
@@ -413,7 +422,10 @@ const rowToEvalRun = (row: EvalRunRow): EvalRun => {
413
422
  sourceLabel: `Eval case "${sourceEvalCase.name}"`,
414
423
  sourceId: sourceEvalCase.id,
415
424
  sourceKind: "eval_case",
416
- sourceRoute: `#/evaluations?case=${encodeURIComponent(sourceEvalCase.id)}`,
425
+ sourceRoute: dashboardRouteToHref({
426
+ tab: "evaluations",
427
+ evaluationId: sourceEvalCase.id,
428
+ }),
417
429
  sourceName: sourceEvalCase.name,
418
430
  },
419
431
  sourceEvalCase,
@@ -10,9 +10,10 @@ import type {
10
10
  EvidenceReference,
11
11
  JsonValue,
12
12
  } from "@obsunified/types";
13
+ import { dashboardRouteToHref, hrefForEntity } from "@obsunified/types";
13
14
 
14
15
  const analysisRoute = (analysisId: string) =>
15
- `#/investigate/${encodeURIComponent(analysisId)}`;
16
+ dashboardRouteToHref({ tab: "investigate", investigationId: analysisId });
16
17
 
17
18
  export const analysisEvidenceReference = (
18
19
  evidence: AskEvidence,
@@ -103,10 +104,10 @@ const valueFor = (
103
104
  };
104
105
 
105
106
  const traceRoute = (traceId: string) =>
106
- `#/traces/${encodeURIComponent(traceId)}`;
107
+ dashboardRouteToHref({ tab: "traces", traceId });
107
108
 
108
109
  const spanRoute = (traceId: string, spanId: string) =>
109
- `${traceRoute(traceId)}#span=${encodeURIComponent(spanId)}`;
110
+ dashboardRouteToHref({ tab: "traces", traceId, spanId });
110
111
 
111
112
  export const evidenceRouteFor = (
112
113
  entityKind: EvidenceEntityKind,
@@ -117,41 +118,48 @@ export const evidenceRouteFor = (
117
118
  case "analysis":
118
119
  return analysisRoute(entityId);
119
120
  case "action":
120
- return `#/actions/${encodeURIComponent(entityId)}`;
121
+ return dashboardRouteToHref({ tab: "actions", actionId: entityId });
121
122
  case "alert":
122
- return `#/alerts?alert=${encodeURIComponent(entityId)}`;
123
+ return dashboardRouteToHref({ tab: "alerts", alertId: entityId });
123
124
  case "agent_run":
124
- return `#/agent-runs/${encodeURIComponent(entityId)}`;
125
+ return dashboardRouteToHref({ tab: "agent-runs", agentRunId: entityId });
125
126
  case "ai_call":
126
- return `#/ai/calls/${encodeURIComponent(entityId)}`;
127
+ return dashboardRouteToHref({ tab: "ai", aiCallId: entityId });
127
128
  case "eval":
128
- return `#/evals/${encodeURIComponent(entityId)}`;
129
+ return dashboardRouteToHref({
130
+ tab: "evaluations",
131
+ evaluationId: entityId,
132
+ });
129
133
  case "log":
130
- return `#/logs?id=${encodeURIComponent(entityId)}`;
134
+ return dashboardRouteToHref({ tab: "logs", logId: entityId });
131
135
  case "profile":
132
- return traceId
133
- ? `#/profiles/${encodeURIComponent(entityId)}?trace_id=${encodeURIComponent(traceId)}`
134
- : `#/profiles/${encodeURIComponent(entityId)}`;
136
+ return dashboardRouteToHref({
137
+ tab: "profiles",
138
+ profileId: entityId,
139
+ ...(traceId ? { traceId } : {}),
140
+ });
135
141
  case "replay":
136
- return `#/replay?session=${encodeURIComponent(entityId)}`;
142
+ return dashboardRouteToHref({ tab: "replay", sessionId: entityId });
137
143
  case "service":
138
- return `#/traces?service=${encodeURIComponent(entityId)}`;
144
+ return dashboardRouteToHref({ tab: "service-map", service: entityId });
139
145
  case "span": {
140
146
  const [tracePart, spanPart] = entityId.split(":");
141
- return spanPart ? spanRoute(tracePart, spanPart) : `#/spans/${entityId}`;
147
+ return spanPart
148
+ ? spanRoute(tracePart, spanPart)
149
+ : hrefForEntity("span", entityId, traceId ? { traceId } : undefined);
142
150
  }
143
151
  case "tool_call":
144
- return `#/tool-calls/${encodeURIComponent(entityId)}`;
152
+ return dashboardRouteToHref({ tab: "tool-calls", toolCallId: entityId });
145
153
  case "trace":
146
154
  return traceRoute(entityId);
147
155
  case "docs":
148
- return entityId.startsWith("#/") ? entityId : `#/docs/${entityId}`;
156
+ return hrefForEntity("docs", entityId);
149
157
  case "change":
150
- return `#/changes/${encodeURIComponent(entityId)}`;
158
+ return dashboardRouteToHref({ tab: "changes", changeId: entityId });
151
159
  case "finding":
152
- return `#/findings/${encodeURIComponent(entityId)}`;
160
+ return dashboardRouteToHref({ tab: "findings", findingId: entityId });
153
161
  case "incident":
154
- return `#/incidents/${encodeURIComponent(entityId)}`;
162
+ return dashboardRouteToHref({ tab: "incidents", incidentId: entityId });
155
163
  }
156
164
  const exhaustive: never = entityKind;
157
165
  return exhaustive;
@@ -232,7 +240,7 @@ const instrumentationGapEvidenceReferences = (
232
240
  const thresholdText = thresholdVersion
233
241
  ? ` Thresholds: ${thresholdVersion}.`
234
242
  : "";
235
- const docsRoute = "#/docs/howto/ebpf";
243
+ const docsRoute = hrefForEntity("docs", "howto/ebpf");
236
244
  return {
237
245
  evidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,
238
246
  entityKind: "span",
@@ -262,7 +270,7 @@ const instrumentationGapEvidenceReferences = (
262
270
  label: "Open trace gap data",
263
271
  entityKind: "trace",
264
272
  entityId: traceId,
265
- route: `${traceRoute(traceId)}/gaps`,
273
+ route: traceRoute(traceId),
266
274
  reason:
267
275
  "Review all structured instrumentation blindspots for this trace.",
268
276
  },
@@ -201,7 +201,7 @@ describe("AlertsStore.listEvaluations evidence references", () => {
201
201
  evidenceId: "alert:rule-1:rule",
202
202
  entityKind: "alert",
203
203
  entityId: "rule-1",
204
- route: "#/alerts?alert=rule-1",
204
+ route: "#/alerts?id=rule-1",
205
205
  source: "alerts.rule_evaluation_preview",
206
206
  }),
207
207
  expect.objectContaining({
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { readdirSync, readFileSync } from "node:fs";
10
10
  import path from "node:path";
11
+ import { DatabaseSync } from "node:sqlite";
11
12
  import { fileURLToPath } from "node:url";
12
13
  import { describe, expect, it } from "vitest";
13
14
 
@@ -34,13 +35,74 @@ const RETENTION_TABLES = [
34
35
  "remediation_proposals",
35
36
  ];
36
37
 
38
+ export function checkMigrationFilePairings(
39
+ sqliteFiles: readonly string[],
40
+ postgresFiles: readonly string[],
41
+ ): string[] {
42
+ const errors: string[] = [];
43
+ const sqliteSet = new Set(sqliteFiles);
44
+ const pgSet = new Set(postgresFiles);
45
+ for (const f of sqliteFiles) {
46
+ if (!pgSet.has(f)) {
47
+ errors.push(`Missing Postgres migration counterpart for ${f}`);
48
+ }
49
+ }
50
+ for (const f of postgresFiles) {
51
+ if (!sqliteSet.has(f)) {
52
+ errors.push(`Missing SQLite migration counterpart for ${f}`);
53
+ }
54
+ }
55
+ const seenPrefixes = new Set<string>();
56
+ for (const f of sqliteFiles) {
57
+ const prefix = f.slice(0, 3);
58
+ if (seenPrefixes.has(prefix)) {
59
+ errors.push(`Duplicate migration prefix ${prefix} in ${f}`);
60
+ }
61
+ seenPrefixes.add(prefix);
62
+ }
63
+ return errors;
64
+ }
65
+
66
+ const extractCreatedTables = (sql: string): string[] => {
67
+ const names: string[] = [];
68
+ for (const m of sql.matchAll(
69
+ /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-zA-Z0-9_]+)/gi,
70
+ )) {
71
+ const table = m[1].toLowerCase();
72
+ if (!table.endsWith("_new")) {
73
+ names.push(table);
74
+ }
75
+ }
76
+ return [...new Set(names)].sort();
77
+ };
78
+
79
+ const extractAddedColumns = (sql: string): string[] => {
80
+ const cols: string[] = [];
81
+ for (const m of sql.matchAll(
82
+ /ALTER\s+TABLE\s+([a-zA-Z0-9_]+)\s+ADD\s+COLUMN(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-zA-Z0-9_]+)/gi,
83
+ )) {
84
+ cols.push(`${m[1].toLowerCase()}.${m[2].toLowerCase()}`);
85
+ }
86
+ return [...new Set(cols)].sort();
87
+ };
88
+
37
89
  describe("migration parity", () => {
38
- it("has the same numbered files on SQLite/D1 and Postgres", () => {
90
+ it("has the same numbered files on SQLite/D1 and Postgres and catches missing pairs", () => {
39
91
  const sqlite = sqlFiles(sqliteDir);
40
92
  const postgres = sqlFiles(postgresDir);
41
93
  expect(postgres).toEqual(sqlite);
42
- const prefixes = sqlite.map((f) => f.slice(0, 3));
43
- expect(new Set(prefixes).size).toBe(prefixes.length);
94
+ expect(sqlite.length).toBeGreaterThanOrEqual(53);
95
+ expect(checkMigrationFilePairings(sqlite, postgres)).toEqual([]);
96
+
97
+ // Regression check: missing or duplicate migration files must be flagged.
98
+ expect(
99
+ checkMigrationFilePairings([...sqlite, "099_new_feature.sql"], postgres),
100
+ ).toEqual([
101
+ "Missing Postgres migration counterpart for 099_new_feature.sql",
102
+ ]);
103
+ expect(
104
+ checkMigrationFilePairings(sqlite, [...postgres, "099_pg_only.sql"]),
105
+ ).toEqual(["Missing SQLite migration counterpart for 099_pg_only.sql"]);
44
106
  });
45
107
 
46
108
  it("declares retention timestamps as TEXT in every Postgres CREATE TABLE", () => {
@@ -65,4 +127,44 @@ describe("migration parity", () => {
65
127
  }
66
128
  expect(offenders).toEqual([]);
67
129
  });
130
+
131
+ it("creates and alters the same tables and columns per migration file", () => {
132
+ const mismatches: string[] = [];
133
+ for (const file of sqlFiles(sqliteDir)) {
134
+ const sqliteSql = readFileSync(path.join(sqliteDir, file), "utf8");
135
+ const pgSql = readFileSync(path.join(postgresDir, file), "utf8");
136
+ const sTables = extractCreatedTables(sqliteSql);
137
+ const pTables = extractCreatedTables(pgSql);
138
+ if (JSON.stringify(sTables) !== JSON.stringify(pTables)) {
139
+ mismatches.push(
140
+ `${file} CREATE TABLE mismatch: sqlite=${JSON.stringify(sTables)} pg=${JSON.stringify(pTables)}`,
141
+ );
142
+ }
143
+ const sCols = extractAddedColumns(sqliteSql);
144
+ const pCols = extractAddedColumns(pgSql);
145
+ if (JSON.stringify(sCols) !== JSON.stringify(pCols)) {
146
+ mismatches.push(
147
+ `${file} ALTER TABLE ADD COLUMN mismatch: sqlite=${JSON.stringify(sCols)} pg=${JSON.stringify(pCols)}`,
148
+ );
149
+ }
150
+ }
151
+ expect(mismatches).toEqual([]);
152
+ });
153
+
154
+ it("applies all SQLite migrations cleanly in-memory without leftover staging tables", () => {
155
+ const db = new DatabaseSync(":memory:");
156
+ try {
157
+ for (const file of sqlFiles(sqliteDir)) {
158
+ db.exec(readFileSync(path.join(sqliteDir, file), "utf8"));
159
+ }
160
+ const leftover = db
161
+ .prepare(
162
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '%_new'",
163
+ )
164
+ .all();
165
+ expect(leftover).toEqual([]);
166
+ } finally {
167
+ db.close();
168
+ }
169
+ });
68
170
  });
@@ -16,6 +16,7 @@ import { AIStore, type IngestEvaluation } from "../lib/ai-store";
16
16
  import { sqlDbFor } from "../lib/sql-db";
17
17
  import { getProjectId } from "./_context";
18
18
  import { readJsonObject } from "./_json";
19
+ import { respondWithContract } from "./autosre-routes";
19
20
 
20
21
  export const aiReceiverPlugin: CollectorPlugin = {
21
22
  name: "ai-receiver",
@@ -104,7 +105,11 @@ export const aiReceiverPlugin: CollectorPlugin = {
104
105
  const store = new AIStore(sqlDbFor(c.env));
105
106
  const call = await store.getAICall(projectId, c.req.param("callId"));
106
107
  if (!call) return c.json({ error: "AI call not found" }, 404);
107
- return c.json({ call, traceId: call.traceId, spanId: call.spanId });
108
+ return respondWithContract(c, "GET /internal/ai/calls/:callId", {
109
+ call,
110
+ traceId: call.traceId,
111
+ spanId: call.spanId,
112
+ });
108
113
  });
109
114
 
110
115
  // OpenInference-kind spans (LLM/TOOL/RETRIEVER/CHAIN/AGENT/...) joined