@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.
- package/dist/auth.d.ts +1 -1
- package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
- package/dist/chunk-RASSTGNB.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2562 -2311
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BmP5-f96.d.ts} +32 -1
- package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
- package/package.json +4 -2
- package/src/__tests__/autosre-closed-loop.test.ts +110 -0
- package/src/__tests__/db-parity.integration.test.ts +961 -0
- package/src/__tests__/internal-contracts-routes.test.ts +451 -0
- package/src/framework/env.ts +105 -0
- package/src/index.ts +13 -4
- package/src/lib/ai-store/evaluations.test.ts +3 -3
- package/src/lib/analyses-store.test.ts +6 -6
- package/src/lib/db-parity.ts +883 -0
- package/src/lib/eval-cases-store.test.ts +1 -1
- package/src/lib/eval-cases-store.ts +16 -4
- package/src/lib/evidence-references.ts +30 -22
- package/src/lib/stage6.test.ts +1 -1
- package/src/migrations/parity.test.ts +105 -3
- package/src/plugins/ai-receiver.ts +6 -1
- package/src/plugins/autosre-routes.ts +251 -140
- package/src/plugins/connected-routes/manifest.test.ts +114 -32
- package/src/plugins/connected-routes/manifest.ts +59 -76
- package/src/plugins/connected-routes.test.ts +10 -8
- package/src/plugins/connected-routes.ts +42 -12
- package/src/plugins/eval-cases-routes.ts +33 -10
- package/src/plugins/evidence-retrieval-routes.ts +60 -25
- package/dist/chunk-ZSF5XP3B.js.map +0 -1
- /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
EvidenceReferenceContract as EvidenceReferenceContractType,
|
|
4
4
|
JsonValue,
|
|
5
5
|
} from "@obsunified/types";
|
|
6
|
-
import {
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
dashboardRouteToHref({ tab: "traces", traceId });
|
|
107
108
|
|
|
108
109
|
const spanRoute = (traceId: string, spanId: string) =>
|
|
109
|
-
|
|
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
|
|
121
|
+
return dashboardRouteToHref({ tab: "actions", actionId: entityId });
|
|
121
122
|
case "alert":
|
|
122
|
-
return
|
|
123
|
+
return dashboardRouteToHref({ tab: "alerts", alertId: entityId });
|
|
123
124
|
case "agent_run":
|
|
124
|
-
return
|
|
125
|
+
return dashboardRouteToHref({ tab: "agent-runs", agentRunId: entityId });
|
|
125
126
|
case "ai_call":
|
|
126
|
-
return
|
|
127
|
+
return dashboardRouteToHref({ tab: "ai", aiCallId: entityId });
|
|
127
128
|
case "eval":
|
|
128
|
-
return
|
|
129
|
+
return dashboardRouteToHref({
|
|
130
|
+
tab: "evaluations",
|
|
131
|
+
evaluationId: entityId,
|
|
132
|
+
});
|
|
129
133
|
case "log":
|
|
130
|
-
return
|
|
134
|
+
return dashboardRouteToHref({ tab: "logs", logId: entityId });
|
|
131
135
|
case "profile":
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
:
|
|
136
|
+
return dashboardRouteToHref({
|
|
137
|
+
tab: "profiles",
|
|
138
|
+
profileId: entityId,
|
|
139
|
+
...(traceId ? { traceId } : {}),
|
|
140
|
+
});
|
|
135
141
|
case "replay":
|
|
136
|
-
return
|
|
142
|
+
return dashboardRouteToHref({ tab: "replay", sessionId: entityId });
|
|
137
143
|
case "service":
|
|
138
|
-
return
|
|
144
|
+
return dashboardRouteToHref({ tab: "service-map", service: entityId });
|
|
139
145
|
case "span": {
|
|
140
146
|
const [tracePart, spanPart] = entityId.split(":");
|
|
141
|
-
return spanPart
|
|
147
|
+
return spanPart
|
|
148
|
+
? spanRoute(tracePart, spanPart)
|
|
149
|
+
: hrefForEntity("span", entityId, traceId ? { traceId } : undefined);
|
|
142
150
|
}
|
|
143
151
|
case "tool_call":
|
|
144
|
-
return
|
|
152
|
+
return dashboardRouteToHref({ tab: "tool-calls", toolCallId: entityId });
|
|
145
153
|
case "trace":
|
|
146
154
|
return traceRoute(entityId);
|
|
147
155
|
case "docs":
|
|
148
|
-
return
|
|
156
|
+
return hrefForEntity("docs", entityId);
|
|
149
157
|
case "change":
|
|
150
|
-
return
|
|
158
|
+
return dashboardRouteToHref({ tab: "changes", changeId: entityId });
|
|
151
159
|
case "finding":
|
|
152
|
-
return
|
|
160
|
+
return dashboardRouteToHref({ tab: "findings", findingId: entityId });
|
|
153
161
|
case "incident":
|
|
154
|
-
return
|
|
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 = "
|
|
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:
|
|
273
|
+
route: traceRoute(traceId),
|
|
266
274
|
reason:
|
|
267
275
|
"Review all structured instrumentation blindspots for this trace.",
|
|
268
276
|
},
|
package/src/lib/stage6.test.ts
CHANGED
|
@@ -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?
|
|
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
|
-
|
|
43
|
-
expect(
|
|
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
|
|
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
|