@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
@@ -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
+ });
@@ -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 type {
72
- CollectorApp,
73
- CollectorEnv,
74
- CollectorRouteContext,
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: "#/evals/eval-1",
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/trace-1#span=span-1",
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/trace-1",
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/trace-1#span=span-1",
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/trace-1",
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: "#/traces?service=api",
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/trace-gap#span=span-root",
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/trace-gap",
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/howto/ebpf",
238
+ route: "#/evidence?kind=docs&id=howto%2Febpf",
239
239
  }),
240
240
  ]),
241
241
  });