@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
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContractMismatchError,
|
|
3
|
+
createInternalClient,
|
|
4
|
+
InternalClientHttpError,
|
|
5
|
+
type InternalEndpoints,
|
|
6
|
+
} from "@obsunified/types";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { CollectorRuntime } from "../framework/collector";
|
|
10
|
+
import type { CollectorEnv } from "../framework/env";
|
|
11
|
+
import { upsertOntologyFromSpans } from "../lib/autosre-ontology";
|
|
12
|
+
import {
|
|
13
|
+
createMigratedSqliteDb,
|
|
14
|
+
type NodeSqliteDb,
|
|
15
|
+
} from "../lib/test-utils/node-sqlite-db";
|
|
16
|
+
import { aiReceiverPlugin } from "../plugins/ai-receiver";
|
|
17
|
+
import {
|
|
18
|
+
autosreRoutesPlugin,
|
|
19
|
+
respondWithContract,
|
|
20
|
+
} from "../plugins/autosre-routes";
|
|
21
|
+
import { connectedRoutesPlugin } from "../plugins/connected-routes";
|
|
22
|
+
import { evalCasesRoutesPlugin } from "../plugins/eval-cases-routes";
|
|
23
|
+
import { evidenceRetrievalRoutesPlugin } from "../plugins/evidence-retrieval-routes";
|
|
24
|
+
|
|
25
|
+
const createTestApp = (db: NodeSqliteDb) => {
|
|
26
|
+
const app = new Hono<{ Bindings: CollectorEnv }>();
|
|
27
|
+
const runtime = new CollectorRuntime();
|
|
28
|
+
autosreRoutesPlugin.register(app, runtime);
|
|
29
|
+
evalCasesRoutesPlugin.register(app, runtime);
|
|
30
|
+
connectedRoutesPlugin.register(app, runtime);
|
|
31
|
+
evidenceRetrievalRoutesPlugin.register(app, runtime);
|
|
32
|
+
aiReceiverPlugin.register(app, runtime);
|
|
33
|
+
const env: CollectorEnv = {
|
|
34
|
+
DB: db as unknown as D1Database,
|
|
35
|
+
};
|
|
36
|
+
return { app, env };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("Collector internal HTTP contract integration (RFC 0014 WS5)", () => {
|
|
40
|
+
it("respondWithContract throws ContractMismatchError if a handler attempts to return a mistyped payload", () => {
|
|
41
|
+
const fakeContext = {
|
|
42
|
+
json: (data: unknown, status = 200) =>
|
|
43
|
+
new Response(JSON.stringify(data), { status }),
|
|
44
|
+
};
|
|
45
|
+
expect(() =>
|
|
46
|
+
respondWithContract(
|
|
47
|
+
fakeContext,
|
|
48
|
+
"GET /internal/incidents",
|
|
49
|
+
// Intentionally mistyped payload
|
|
50
|
+
{
|
|
51
|
+
incidents: "not-an-array",
|
|
52
|
+
} as unknown as InternalEndpoints["GET /internal/incidents"]["response"],
|
|
53
|
+
),
|
|
54
|
+
).toThrow(ContractMismatchError);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("createInternalClient round-trips changes, incidents, findings, counterfactuals, remediations, evals, connected, AI calls, and evidence against collector routes", async () => {
|
|
58
|
+
const db = createMigratedSqliteDb();
|
|
59
|
+
const { app, env } = createTestApp(db);
|
|
60
|
+
|
|
61
|
+
const makeClient = (defaultHeaders?: Record<string, string>) =>
|
|
62
|
+
createInternalClient({
|
|
63
|
+
baseUrl: "http://localhost",
|
|
64
|
+
defaultHeaders,
|
|
65
|
+
fetchImpl: (input, init) => app.request(input, init, env),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const client = makeClient();
|
|
69
|
+
|
|
70
|
+
// Seed a trace + span so eval cases and ontology have a source entity
|
|
71
|
+
db.raw
|
|
72
|
+
.prepare(
|
|
73
|
+
`INSERT INTO telemetry_spans (
|
|
74
|
+
project_id, trace_id, span_id, parent_span_id, service_name,
|
|
75
|
+
span_name, span_kind, start_time, end_time, duration_ms,
|
|
76
|
+
status_code, status_message, attributes_json, resource_attributes_json,
|
|
77
|
+
events_json, links_json, received_at, expires_at,
|
|
78
|
+
environment, service_version, commit_sha, target_node_id, error_signature
|
|
79
|
+
) VALUES (
|
|
80
|
+
'default', 'trace-ws5', 'span-ws5', NULL, 'checkout',
|
|
81
|
+
'POST /charge', 2, '2026-04-16T00:00:00.000Z', '2026-04-16T00:00:00.500Z', 500,
|
|
82
|
+
2, 'Timeout', '{}', '{}',
|
|
83
|
+
'[]', '[]', '2026-04-16T00:00:00.500Z', '2099-01-01T00:00:00.000Z',
|
|
84
|
+
'production', '2.1.0', 'abc1234', 'service:default:checkout', 'Timeout'
|
|
85
|
+
)`,
|
|
86
|
+
)
|
|
87
|
+
.run();
|
|
88
|
+
await upsertOntologyFromSpans(db, [
|
|
89
|
+
{
|
|
90
|
+
projectId: "default",
|
|
91
|
+
traceId: "trace-ws5",
|
|
92
|
+
spanId: "span-ws5",
|
|
93
|
+
parentSpanId: null,
|
|
94
|
+
traceState: null,
|
|
95
|
+
serviceName: "checkout",
|
|
96
|
+
spanName: "POST /charge",
|
|
97
|
+
spanKind: 2,
|
|
98
|
+
statusCode: 2,
|
|
99
|
+
statusMessage: "Timeout",
|
|
100
|
+
startTime: "2026-04-16T00:00:00.000Z",
|
|
101
|
+
endTime: "2026-04-16T00:00:00.500Z",
|
|
102
|
+
durationMs: 500,
|
|
103
|
+
environment: "prod",
|
|
104
|
+
scopeName: null,
|
|
105
|
+
scopeVersion: null,
|
|
106
|
+
attributesJson: "{}",
|
|
107
|
+
resourceAttributesJson: "{}",
|
|
108
|
+
eventsJson: "[]",
|
|
109
|
+
linksJson: "[]",
|
|
110
|
+
droppedAttributesCount: 0,
|
|
111
|
+
droppedEventsCount: 0,
|
|
112
|
+
droppedLinksCount: 0,
|
|
113
|
+
receivedAt: "2026-04-16T00:00:00.500Z",
|
|
114
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
// 1. Changes
|
|
119
|
+
const createdChange = await client.request("POST /v1/changes", {
|
|
120
|
+
body: {
|
|
121
|
+
environment: "prod",
|
|
122
|
+
kind: "deploy",
|
|
123
|
+
serviceName: "checkout",
|
|
124
|
+
version: "2.1.0",
|
|
125
|
+
previousVersion: "2.0.0",
|
|
126
|
+
commitSha: "abc1234",
|
|
127
|
+
summary: "Deploy checkout 2.1.0",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
expect(createdChange.id).toBeTruthy();
|
|
131
|
+
expect(createdChange.change.id).toBe(createdChange.id);
|
|
132
|
+
expect(createdChange.changeEvent?.id).toBe(createdChange.id);
|
|
133
|
+
|
|
134
|
+
const listedChanges = await client.request("GET /internal/changes", {
|
|
135
|
+
query: { environment: "prod", serviceName: "checkout" },
|
|
136
|
+
});
|
|
137
|
+
expect(listedChanges.changes.some((c) => c.id === createdChange.id)).toBe(
|
|
138
|
+
true,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const fetchedChange = await client.request("GET /internal/changes/:id", {
|
|
142
|
+
params: { id: createdChange.id },
|
|
143
|
+
});
|
|
144
|
+
expect(fetchedChange.change.id).toBe(createdChange.id);
|
|
145
|
+
|
|
146
|
+
const blastRadius = await client.request(
|
|
147
|
+
"GET /internal/changes/:id/blast-radius",
|
|
148
|
+
{
|
|
149
|
+
params: { id: createdChange.id },
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
expect(blastRadius.change.id).toBe(createdChange.id);
|
|
153
|
+
expect(blastRadius.blastRadius.change.id).toBe(createdChange.id);
|
|
154
|
+
|
|
155
|
+
// 2. Ontology
|
|
156
|
+
const ontology = await client.request("GET /internal/ontology", {
|
|
157
|
+
query: { environment: "prod", traceId: "trace-ws5" },
|
|
158
|
+
});
|
|
159
|
+
expect(Array.isArray(ontology.nodes)).toBe(true);
|
|
160
|
+
|
|
161
|
+
// 3. Incidents (both /internal/incidents and /internal/incidents/open)
|
|
162
|
+
const openedIncident = await client.request("POST /internal/incidents", {
|
|
163
|
+
body: {
|
|
164
|
+
environment: "prod",
|
|
165
|
+
targetNodeId: "service:default:checkout",
|
|
166
|
+
symptomAnchorKind: "trace",
|
|
167
|
+
symptomAnchorId: "trace-ws5",
|
|
168
|
+
symptomFingerprint: "fp-checkout-timeout",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
expect(openedIncident.deduplicated).toBe(false);
|
|
172
|
+
|
|
173
|
+
const dedupedIncident = await client.request(
|
|
174
|
+
"POST /internal/incidents/open",
|
|
175
|
+
{
|
|
176
|
+
body: {
|
|
177
|
+
environment: "prod",
|
|
178
|
+
targetNodeId: "service:default:checkout",
|
|
179
|
+
symptomAnchorKind: "trace",
|
|
180
|
+
symptomAnchorId: "trace-ws5",
|
|
181
|
+
symptomFingerprint: "fp-checkout-timeout",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
expect(dedupedIncident.deduplicated).toBe(true);
|
|
186
|
+
expect(dedupedIncident.incident.id).toBe(openedIncident.incident.id);
|
|
187
|
+
|
|
188
|
+
const leaseResult = await client.request(
|
|
189
|
+
"POST /internal/incidents/:id/lease",
|
|
190
|
+
{
|
|
191
|
+
params: { id: openedIncident.incident.id },
|
|
192
|
+
body: {
|
|
193
|
+
runnerId: "runner-ws5",
|
|
194
|
+
leaseDurationSeconds: 120,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
expect(leaseResult.acquired).toBe(true);
|
|
199
|
+
|
|
200
|
+
const incidentsList = await client.request("GET /internal/incidents", {
|
|
201
|
+
query: { environment: "prod" },
|
|
202
|
+
});
|
|
203
|
+
expect(
|
|
204
|
+
incidentsList.incidents.some((i) => i.id === openedIncident.incident.id),
|
|
205
|
+
).toBe(true);
|
|
206
|
+
|
|
207
|
+
// 4. Findings & Counterfactuals
|
|
208
|
+
const createdFinding = await client.request("POST /internal/findings", {
|
|
209
|
+
body: {
|
|
210
|
+
environment: "prod",
|
|
211
|
+
incidentId: openedIncident.incident.id,
|
|
212
|
+
title: "Checkout timeout after 2.1.0 deploy",
|
|
213
|
+
summary: "Cohort isolated to version 2.1.0",
|
|
214
|
+
symptomEntityKind: "trace",
|
|
215
|
+
symptomEntityId: "trace-ws5",
|
|
216
|
+
faultLocusEntityKind: "span",
|
|
217
|
+
faultLocusEntityId: "trace-ws5:span-ws5",
|
|
218
|
+
rootCauseEntityKind: "change",
|
|
219
|
+
rootCauseEntityId: createdChange.id,
|
|
220
|
+
contributingChangeId: createdChange.id,
|
|
221
|
+
causalChain: [
|
|
222
|
+
{
|
|
223
|
+
subjectKind: "change",
|
|
224
|
+
subjectId: createdChange.id,
|
|
225
|
+
predicate: "degraded_after_change",
|
|
226
|
+
objectKind: "span",
|
|
227
|
+
objectId: "trace-ws5:span-ws5",
|
|
228
|
+
relationshipCertainty: "exact",
|
|
229
|
+
causalProofTier: "corroborated_candidate",
|
|
230
|
+
confidence: 0.9,
|
|
231
|
+
reason: "Version 2.1.0 deployed immediately before errors",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
citedEvidenceIds: [`change:${createdChange.id}`, "trace:trace-ws5"],
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
expect(createdFinding.finding.id).toBe(createdFinding.id);
|
|
238
|
+
|
|
239
|
+
const fetchedFinding = await client.request("GET /internal/findings/:id", {
|
|
240
|
+
params: { id: createdFinding.id },
|
|
241
|
+
});
|
|
242
|
+
expect(fetchedFinding.finding.id).toBe(createdFinding.id);
|
|
243
|
+
|
|
244
|
+
const findingsList = await client.request("GET /internal/findings", {
|
|
245
|
+
query: { incidentId: openedIncident.incident.id },
|
|
246
|
+
});
|
|
247
|
+
expect(findingsList.findings.some((f) => f.id === createdFinding.id)).toBe(
|
|
248
|
+
true,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Eval case + runs for counterfactual verification
|
|
252
|
+
const createdCase = await client.request("POST /internal/eval-cases", {
|
|
253
|
+
body: {
|
|
254
|
+
sourceEntityType: "trace",
|
|
255
|
+
sourceEntityId: "trace-ws5",
|
|
256
|
+
name: "Checkout timeout regression case",
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
expect(createdCase.evalCase.id).toBeTruthy();
|
|
260
|
+
|
|
261
|
+
const listedCases = await client.request("GET /internal/eval-cases", {
|
|
262
|
+
query: { sourceEntityType: "trace" },
|
|
263
|
+
});
|
|
264
|
+
expect(
|
|
265
|
+
listedCases.evalCases.some((ec) => ec.id === createdCase.evalCase.id),
|
|
266
|
+
).toBe(true);
|
|
267
|
+
|
|
268
|
+
const failingRun = await client.request("POST /internal/eval-runs", {
|
|
269
|
+
body: {
|
|
270
|
+
evalCaseId: createdCase.evalCase.id,
|
|
271
|
+
status: "failed",
|
|
272
|
+
environment: "prod",
|
|
273
|
+
serviceVersion: "2.1.0",
|
|
274
|
+
commitSha: "abc1234",
|
|
275
|
+
inputPayloadHash: "hash-ws5",
|
|
276
|
+
failureFingerprint: createdFinding.finding.fingerprint,
|
|
277
|
+
totalCount: 1,
|
|
278
|
+
passCount: 0,
|
|
279
|
+
failCount: 1,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
await client.request("POST /internal/eval-cases/:id/results", {
|
|
283
|
+
params: { id: createdCase.evalCase.id },
|
|
284
|
+
body: {
|
|
285
|
+
runId: failingRun.evalRun.id,
|
|
286
|
+
passed: false,
|
|
287
|
+
inputPayloadHash: "hash-ws5",
|
|
288
|
+
failureFingerprint: createdFinding.finding.fingerprint,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const passingRunIds: string[] = [];
|
|
293
|
+
for (let i = 0; i < 3; i += 1) {
|
|
294
|
+
const passRun = await client.request("POST /internal/eval-runs", {
|
|
295
|
+
body: {
|
|
296
|
+
evalCaseId: createdCase.evalCase.id,
|
|
297
|
+
status: "completed",
|
|
298
|
+
environment: "prod",
|
|
299
|
+
serviceVersion: "2.0.0",
|
|
300
|
+
commitSha: "prev000",
|
|
301
|
+
inputPayloadHash: "hash-ws5",
|
|
302
|
+
totalCount: 1,
|
|
303
|
+
passCount: 1,
|
|
304
|
+
failCount: 0,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
await client.request("POST /internal/eval-cases/:id/results", {
|
|
308
|
+
params: { id: createdCase.evalCase.id },
|
|
309
|
+
body: {
|
|
310
|
+
runId: passRun.evalRun.id,
|
|
311
|
+
passed: true,
|
|
312
|
+
inputPayloadHash: "hash-ws5",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
passingRunIds.push(passRun.evalRun.id);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const evalRunSummary = await client.request(
|
|
319
|
+
"GET /internal/evals/runs/:id",
|
|
320
|
+
{
|
|
321
|
+
params: { id: failingRun.evalRun.id },
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
expect(evalRunSummary.run.id).toBe(failingRun.evalRun.id);
|
|
325
|
+
|
|
326
|
+
const counterfactual = await client.request(
|
|
327
|
+
"POST /internal/findings/:id/counterfactual",
|
|
328
|
+
{
|
|
329
|
+
params: { id: createdFinding.id },
|
|
330
|
+
body: {
|
|
331
|
+
environment: "prod",
|
|
332
|
+
evalCaseId: createdCase.evalCase.id,
|
|
333
|
+
failingRunId: failingRun.evalRun.id,
|
|
334
|
+
passingRunIds,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
expect(counterfactual.verified).toBe(true);
|
|
339
|
+
expect(counterfactual.finding.causalProofTier).toBe(
|
|
340
|
+
"counterfactual_verified",
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// 5. Remediations
|
|
344
|
+
const proposedHuman = await client.request(
|
|
345
|
+
"POST /internal/remediations/propose",
|
|
346
|
+
{
|
|
347
|
+
body: {
|
|
348
|
+
environment: "prod",
|
|
349
|
+
incidentId: openedIncident.incident.id,
|
|
350
|
+
findingId: createdFinding.id,
|
|
351
|
+
idempotencyKey: "idem-ws5-human",
|
|
352
|
+
actionKind: "rollback_deploy",
|
|
353
|
+
targetNodeId: "service:default:checkout",
|
|
354
|
+
expectedCurrentState: { version: "2.1.0" },
|
|
355
|
+
desiredTargetState: { version: "2.0.0" },
|
|
356
|
+
requestedAutonomy: "human_approved_write",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
expect(proposedHuman.proposal.status).toBe("pending_human_approval");
|
|
361
|
+
|
|
362
|
+
// Bearer token approval must fail with 403 InternalClientHttpError
|
|
363
|
+
const bearerClient = makeClient({
|
|
364
|
+
Authorization: "Bearer obs_secret_agent_token",
|
|
365
|
+
});
|
|
366
|
+
await expect(
|
|
367
|
+
bearerClient.request("POST /internal/remediations/:id/approve", {
|
|
368
|
+
params: { id: proposedHuman.proposal.id },
|
|
369
|
+
body: {},
|
|
370
|
+
}),
|
|
371
|
+
).rejects.toThrow(InternalClientHttpError);
|
|
372
|
+
|
|
373
|
+
// Cookie session approval succeeds and mints grantToken
|
|
374
|
+
const humanSessionClient = makeClient({
|
|
375
|
+
Cookie: "obs_session=valid_human_session_cookie",
|
|
376
|
+
});
|
|
377
|
+
const approved = await humanSessionClient.request(
|
|
378
|
+
"POST /internal/remediations/:id/approve",
|
|
379
|
+
{
|
|
380
|
+
params: { id: proposedHuman.proposal.id },
|
|
381
|
+
body: {},
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
expect(approved.proposal.status).toBe("grant_issued");
|
|
385
|
+
expect(typeof approved.grantToken).toBe("string");
|
|
386
|
+
|
|
387
|
+
const claimed = await client.request(
|
|
388
|
+
"POST /internal/remediations/:id/claim",
|
|
389
|
+
{
|
|
390
|
+
params: { id: proposedHuman.proposal.id },
|
|
391
|
+
body: {
|
|
392
|
+
grantToken: approved.grantToken as string,
|
|
393
|
+
runnerId: "runner-ws5",
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
expect(claimed.claimed).toBe(true);
|
|
398
|
+
expect(claimed.proposal.status).toBe("claimed_executing");
|
|
399
|
+
|
|
400
|
+
const completed = await client.request(
|
|
401
|
+
"POST /internal/remediations/:id/complete",
|
|
402
|
+
{
|
|
403
|
+
params: { id: proposedHuman.proposal.id },
|
|
404
|
+
body: {
|
|
405
|
+
runnerId: "runner-ws5",
|
|
406
|
+
resultingChangeId: createdChange.id,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
expect(completed.proposal.status).toBe("completed");
|
|
411
|
+
|
|
412
|
+
const watchdog = await client.request(
|
|
413
|
+
"POST /internal/remediations/watchdog",
|
|
414
|
+
{
|
|
415
|
+
body: {},
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
expect(typeof watchdog.summary.evaluated).toBe("number");
|
|
419
|
+
|
|
420
|
+
const incidentDetail = await client.request("GET /internal/incidents/:id", {
|
|
421
|
+
params: { id: openedIncident.incident.id },
|
|
422
|
+
});
|
|
423
|
+
expect(incidentDetail.incident.id).toBe(openedIncident.incident.id);
|
|
424
|
+
expect(incidentDetail.findings.length).toBeGreaterThanOrEqual(1);
|
|
425
|
+
expect(incidentDetail.proposals.length).toBeGreaterThanOrEqual(1);
|
|
426
|
+
|
|
427
|
+
// 6. Connected, Evidence Bundle, Evidence Stats
|
|
428
|
+
const connected = await client.request(
|
|
429
|
+
"GET /internal/connected/:kind/:id",
|
|
430
|
+
{
|
|
431
|
+
params: { kind: "incident", id: openedIncident.incident.id },
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
expect(connected.entity.id).toBe(openedIncident.incident.id);
|
|
435
|
+
|
|
436
|
+
const bundle = await client.request("POST /internal/evidence/bundle", {
|
|
437
|
+
body: {
|
|
438
|
+
anchor: {
|
|
439
|
+
entityKind: "incident",
|
|
440
|
+
entityId: openedIncident.incident.id,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
expect(bundle.schemaVersion).toBe("obs-unified.evidence-bundle.v1");
|
|
445
|
+
|
|
446
|
+
const stats = await client.request("GET /internal/evidence/stats", {
|
|
447
|
+
query: { limit: 10 },
|
|
448
|
+
});
|
|
449
|
+
expect(stats.projectId).toBe("default");
|
|
450
|
+
});
|
|
451
|
+
});
|
package/src/framework/env.ts
CHANGED
|
@@ -89,6 +89,111 @@ export interface CollectorEnv {
|
|
|
89
89
|
TELEMETRY_QUERY_TOKEN?: string;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Optional string settings on `CollectorEnv` that host adapters (such as
|
|
94
|
+
* `apps/collector-node`) forward directly from the process environment after
|
|
95
|
+
* trimming whitespace and coercing blank strings to `undefined`.
|
|
96
|
+
*/
|
|
97
|
+
export const COLLECTOR_OPTIONAL_STRING_ENV_KEYS = [
|
|
98
|
+
"RETENTION_HOURS",
|
|
99
|
+
"ANTHROPIC_API_KEY",
|
|
100
|
+
"OPENAI_API_KEY",
|
|
101
|
+
"OPENAI_BASE_URL",
|
|
102
|
+
"NARRATIVE_MODEL",
|
|
103
|
+
"NARRATIVE_BUDGET_PER_HOUR",
|
|
104
|
+
"ALERT_WEBHOOK_ALLOW_PRIVATE",
|
|
105
|
+
"MAX_BODY_BYTES",
|
|
106
|
+
"TELEMETRY_REDACT_FIELDS",
|
|
107
|
+
"OBS_PAYLOAD_CAPTURE_DEFAULT",
|
|
108
|
+
] as const;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* String settings on `CollectorEnv` that require explicit host handling (e.g.
|
|
112
|
+
* required auth secrets, CORS origin parsing, Worker-only self-instrumentation
|
|
113
|
+
* vars, or deprecated Worker tokens) and must not be blindly forwarded by the
|
|
114
|
+
* generic optional-env loop.
|
|
115
|
+
*/
|
|
116
|
+
export const COLLECTOR_HOST_MANAGED_ENV_KEYS = [
|
|
117
|
+
"INGEST_KEY",
|
|
118
|
+
"DASHBOARD_PASSWORD",
|
|
119
|
+
"DASHBOARD_TOKEN",
|
|
120
|
+
"ALLOWED_ORIGINS",
|
|
121
|
+
"ALLOW_UNAUTHENTICATED",
|
|
122
|
+
"OBS_DASHBOARD_INGEST_KEY",
|
|
123
|
+
"OBS_COLLECTOR_SELF_URL",
|
|
124
|
+
"OBS_SELF_AWAIT_EXPORTS",
|
|
125
|
+
"TELEMETRY_INGEST_TOKEN",
|
|
126
|
+
"USAGE_INGEST_TOKEN",
|
|
127
|
+
"TELEMETRY_QUERY_TOKEN",
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
130
|
+
export const COLLECTOR_STRING_ENV_MANIFEST = {
|
|
131
|
+
forwarded: COLLECTOR_OPTIONAL_STRING_ENV_KEYS,
|
|
132
|
+
hostManaged: COLLECTOR_HOST_MANAGED_ENV_KEYS,
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
export type CollectorOptionalStringEnvKey =
|
|
136
|
+
(typeof COLLECTOR_OPTIONAL_STRING_ENV_KEYS)[number];
|
|
137
|
+
|
|
138
|
+
export type CollectorHostManagedEnvKey =
|
|
139
|
+
(typeof COLLECTOR_HOST_MANAGED_ENV_KEYS)[number];
|
|
140
|
+
|
|
141
|
+
export type CollectorManifestStringEnvKey =
|
|
142
|
+
| CollectorOptionalStringEnvKey
|
|
143
|
+
| CollectorHostManagedEnvKey;
|
|
144
|
+
|
|
145
|
+
export type CollectorObjectBindingKey =
|
|
146
|
+
| "DB"
|
|
147
|
+
| "REPLAYS_BUCKET"
|
|
148
|
+
| "PROFILES_BUCKET"
|
|
149
|
+
| "TAIL_HUB";
|
|
150
|
+
|
|
151
|
+
export type CollectorStringEnvKey = Exclude<
|
|
152
|
+
keyof CollectorEnv,
|
|
153
|
+
CollectorObjectBindingKey
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
type AssertNever<T extends never> = T;
|
|
157
|
+
type _UnaccountedCollectorStringEnvKeys = AssertNever<
|
|
158
|
+
Exclude<CollectorStringEnvKey, CollectorManifestStringEnvKey>
|
|
159
|
+
>;
|
|
160
|
+
type _UnknownManifestStringEnvKeys = AssertNever<
|
|
161
|
+
Exclude<CollectorManifestStringEnvKey, CollectorStringEnvKey>
|
|
162
|
+
>;
|
|
163
|
+
type _OverlappingManifestStringEnvKeys = AssertNever<
|
|
164
|
+
Extract<CollectorOptionalStringEnvKey, CollectorHostManagedEnvKey>
|
|
165
|
+
>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Verifies that every string key in `declaredKeys` is categorized in either
|
|
169
|
+
* `COLLECTOR_OPTIONAL_STRING_ENV_KEYS` or `COLLECTOR_HOST_MANAGED_ENV_KEYS`
|
|
170
|
+
* without overlap. Throws if any key is uncategorized or duplicated.
|
|
171
|
+
*/
|
|
172
|
+
export function assertCollectorEnvManifestCompleteness(
|
|
173
|
+
declaredKeys: readonly string[],
|
|
174
|
+
manifest: {
|
|
175
|
+
readonly forwarded: readonly string[];
|
|
176
|
+
readonly hostManaged: readonly string[];
|
|
177
|
+
} = COLLECTOR_STRING_ENV_MANIFEST,
|
|
178
|
+
): void {
|
|
179
|
+
const forwardedSet = new Set(manifest.forwarded);
|
|
180
|
+
const hostManagedSet = new Set(manifest.hostManaged);
|
|
181
|
+
const overlap = manifest.forwarded.filter((k) => hostManagedSet.has(k));
|
|
182
|
+
if (overlap.length > 0) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`CollectorEnv manifest keys cannot be both forwarded and host-managed: ${overlap.join(", ")}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const unaccounted = declaredKeys.filter(
|
|
188
|
+
(k) => !forwardedSet.has(k) && !hostManagedSet.has(k),
|
|
189
|
+
);
|
|
190
|
+
if (unaccounted.length > 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Uncategorized CollectorEnv string setting(s): ${unaccounted.join(", ")}. Add each key to COLLECTOR_OPTIONAL_STRING_ENV_KEYS or COLLECTOR_HOST_MANAGED_ENV_KEYS.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
92
197
|
export interface CollectorRouteContext {
|
|
93
198
|
hono: Context<{ Bindings: CollectorEnv }>;
|
|
94
199
|
env: CollectorEnv;
|
package/src/index.ts
CHANGED
|
@@ -68,10 +68,19 @@ export {
|
|
|
68
68
|
type UsageEventProcessorPlugin,
|
|
69
69
|
} from "./framework/collector";
|
|
70
70
|
|
|
71
|
-
export
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
export {
|
|
72
|
+
assertCollectorEnvManifestCompleteness,
|
|
73
|
+
COLLECTOR_HOST_MANAGED_ENV_KEYS,
|
|
74
|
+
COLLECTOR_OPTIONAL_STRING_ENV_KEYS,
|
|
75
|
+
COLLECTOR_STRING_ENV_MANIFEST,
|
|
76
|
+
type CollectorApp,
|
|
77
|
+
type CollectorEnv,
|
|
78
|
+
type CollectorHostManagedEnvKey,
|
|
79
|
+
type CollectorManifestStringEnvKey,
|
|
80
|
+
type CollectorObjectBindingKey,
|
|
81
|
+
type CollectorOptionalStringEnvKey,
|
|
82
|
+
type CollectorRouteContext,
|
|
83
|
+
type CollectorStringEnvKey,
|
|
75
84
|
} from "./framework/env";
|
|
76
85
|
|
|
77
86
|
export type { Logger } from "./framework/logger";
|
|
@@ -25,20 +25,20 @@ describe("mapEvaluationRows", () => {
|
|
|
25
25
|
evidenceId: "ai-evaluation:eval-1",
|
|
26
26
|
entityKind: "eval",
|
|
27
27
|
entityId: "eval-1",
|
|
28
|
-
route: "#/
|
|
28
|
+
route: "#/evaluations/eval-1",
|
|
29
29
|
source: "ai_span_evaluations.llm_judge",
|
|
30
30
|
citations: [
|
|
31
31
|
{
|
|
32
32
|
label: "span span-1",
|
|
33
33
|
entityKind: "span",
|
|
34
34
|
entityId: "trace-1:span-1",
|
|
35
|
-
route: "#/traces
|
|
35
|
+
route: "#/traces?trace=trace-1&span=span-1",
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
label: "trace trace-1",
|
|
39
39
|
entityKind: "trace",
|
|
40
40
|
entityId: "trace-1",
|
|
41
|
-
route: "#/traces
|
|
41
|
+
route: "#/traces?trace=trace-1",
|
|
42
42
|
},
|
|
43
43
|
],
|
|
44
44
|
}),
|
|
@@ -118,7 +118,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
118
118
|
"analysis:investigate.latency:tail_offenders:0:span:trace-1:span-1",
|
|
119
119
|
entityKind: "span",
|
|
120
120
|
entityId: "trace-1:span-1",
|
|
121
|
-
route: "#/traces
|
|
121
|
+
route: "#/traces?trace=trace-1&span=span-1",
|
|
122
122
|
source: "analysis.payload.evidence.tail_offenders",
|
|
123
123
|
confidence: 0.95,
|
|
124
124
|
citations: [
|
|
@@ -126,7 +126,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
126
126
|
label: "trace trace-1",
|
|
127
127
|
entityKind: "trace",
|
|
128
128
|
entityId: "trace-1",
|
|
129
|
-
route: "#/traces
|
|
129
|
+
route: "#/traces?trace=trace-1",
|
|
130
130
|
},
|
|
131
131
|
],
|
|
132
132
|
}),
|
|
@@ -134,7 +134,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
134
134
|
evidenceId: "analysis:investigate.latency:tail_offenders:1:service:api",
|
|
135
135
|
entityKind: "service",
|
|
136
136
|
entityId: "api",
|
|
137
|
-
route: "#/
|
|
137
|
+
route: "#/service-map?service=api",
|
|
138
138
|
source: "analysis.payload.evidence.tail_offenders",
|
|
139
139
|
confidence: 0.7,
|
|
140
140
|
}),
|
|
@@ -219,7 +219,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
219
219
|
"analysis:trace.instrumentation_gaps:instrumentation-gap:0:span:trace-gap:span-root",
|
|
220
220
|
entityKind: "span",
|
|
221
221
|
entityId: "trace-gap:span-root",
|
|
222
|
-
route: "#/traces
|
|
222
|
+
route: "#/traces?trace=trace-gap&span=span-root",
|
|
223
223
|
source: "analysis.payload.instrumentationGaps",
|
|
224
224
|
confidence: 0.9,
|
|
225
225
|
citations: [
|
|
@@ -227,7 +227,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
227
227
|
label: "trace trace-gap",
|
|
228
228
|
entityKind: "trace",
|
|
229
229
|
entityId: "trace-gap",
|
|
230
|
-
route: "#/traces
|
|
230
|
+
route: "#/traces?trace=trace-gap",
|
|
231
231
|
},
|
|
232
232
|
],
|
|
233
233
|
suggestedNextPivots: expect.arrayContaining([
|
|
@@ -235,7 +235,7 @@ describe("AnalysesStore.getLatestResult evidence references", () => {
|
|
|
235
235
|
label: "Open profiler setup docs",
|
|
236
236
|
entityKind: "docs",
|
|
237
237
|
entityId: "howto/ebpf",
|
|
238
|
-
route: "#/docs
|
|
238
|
+
route: "#/evidence?kind=docs&id=howto%2Febpf",
|
|
239
239
|
}),
|
|
240
240
|
]),
|
|
241
241
|
});
|