@obsunified/collector 2.2.0 → 2.2.3
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.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/index.d.ts +61 -3
- package/dist/index.js +10933 -9769
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/autosre-closed-loop.test.ts +1554 -17
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +237 -44
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +52 -28
- package/src/lib/autosre-remediation-gate.ts +942 -348
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/projects-store.test.ts +38 -1
- package/src/lib/projects-store.ts +23 -4
- package/src/lib/sql-db-postgres.integration.test.ts +75 -2
- package/src/lib/store/ingest.ts +8 -56
- package/src/lib/usage-store.ts +9 -3
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/052_remediation_callback.sql +10 -0
- package/src/migrations/053_remediation_callback_delivery.sql +7 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations-postgres/052_remediation_callback.sql +10 -0
- package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
- package/src/plugins/ai-call-by-id.test.ts +73 -0
- package/src/plugins/ai-receiver.ts +10 -0
- package/src/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +374 -108
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +142 -0
- package/src/plugins/connected-routes/manifest.ts +123 -6
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +3 -3
- package/src/plugins/connected-routes.ts +56 -83
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +397 -25
- package/dist/chunk-KMEZKGSO.js.map +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { hrefForEntity, linksFromAi, linksFromEvalResults } from "./manifest";
|
|
3
|
+
|
|
4
|
+
describe("connected-routes manifest link shapes", () => {
|
|
5
|
+
it("routes eval, service and ai_call entities to deep-linkable hashes", () => {
|
|
6
|
+
expect(hrefForEntity("eval", "case 1")).toBe("#/evaluations/case%201");
|
|
7
|
+
expect(hrefForEntity("service", "checkout")).toBe(
|
|
8
|
+
"#/service-map?service=checkout",
|
|
9
|
+
);
|
|
10
|
+
expect(hrefForEntity("ai_call", "call-1")).toBe("#/ai/calls/call-1");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("carries the eval id and the ai call id in section links", () => {
|
|
14
|
+
const evals = linksFromEvalResults(
|
|
15
|
+
[
|
|
16
|
+
{
|
|
17
|
+
id: "eval-1",
|
|
18
|
+
actionId: "act-1",
|
|
19
|
+
projectId: "default",
|
|
20
|
+
evaluatorName: "groundedness",
|
|
21
|
+
evaluatorVersion: "1",
|
|
22
|
+
score: 1,
|
|
23
|
+
passed: 1,
|
|
24
|
+
reasoning: null,
|
|
25
|
+
rubricJson: null,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
"Evals",
|
|
29
|
+
);
|
|
30
|
+
expect(evals.links[0].href).toBe("#/evaluations/eval-1");
|
|
31
|
+
const ai = linksFromAi(
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
callId: "call/1",
|
|
35
|
+
provider: "openai",
|
|
36
|
+
modelName: "gpt",
|
|
37
|
+
} as unknown as Parameters<typeof linksFromAi>[0][number],
|
|
38
|
+
],
|
|
39
|
+
"AI",
|
|
40
|
+
);
|
|
41
|
+
expect(ai.links[0].href).toBe("#/ai/calls/call%2F1");
|
|
42
|
+
expect(ai.links[0].entityId).toBe("call/1");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
hrefForEntity,
|
|
4
|
+
linksFromArtifacts,
|
|
5
|
+
linksFromEvalResults,
|
|
6
|
+
linksFromRetrievalEvents,
|
|
7
|
+
linksFromToolCalls,
|
|
8
|
+
MAX_LINKS_INLINE,
|
|
9
|
+
resolveFaultLocusTarget,
|
|
10
|
+
} from "./manifest";
|
|
11
|
+
|
|
12
|
+
// Tabs served by apps/web/src/app/router.ts (KNOWN_TABS). Generated hrefs
|
|
13
|
+
// must start with one of these or the app falls back to Health.
|
|
14
|
+
const ROUTER_TABS = new Set([
|
|
15
|
+
"health",
|
|
16
|
+
"investigate",
|
|
17
|
+
"traces",
|
|
18
|
+
"service-map",
|
|
19
|
+
"issues",
|
|
20
|
+
"logs",
|
|
21
|
+
"ai",
|
|
22
|
+
"evidence",
|
|
23
|
+
"usage",
|
|
24
|
+
"replay",
|
|
25
|
+
"alerts",
|
|
26
|
+
"users",
|
|
27
|
+
"agent-runs",
|
|
28
|
+
"actions",
|
|
29
|
+
"tool-calls",
|
|
30
|
+
"profiles",
|
|
31
|
+
"tool-reliability",
|
|
32
|
+
"evaluations",
|
|
33
|
+
"changes",
|
|
34
|
+
"findings",
|
|
35
|
+
"incidents",
|
|
36
|
+
"remediations",
|
|
37
|
+
]);
|
|
38
|
+
const tabOf = (href: string) =>
|
|
39
|
+
href.replace(/^#\//, "").split(/[/?#]/)[0] ?? "";
|
|
40
|
+
|
|
41
|
+
describe("connected manifest hrefs target real router tabs", () => {
|
|
42
|
+
it("tool call overflow, retrievals, evals and artifacts avoid unknown tabs", () => {
|
|
43
|
+
const many = Array.from({ length: MAX_LINKS_INLINE + 1 }, (_, i) => ({
|
|
44
|
+
id: `t${i}`,
|
|
45
|
+
actionId: "a1",
|
|
46
|
+
toolName: "search",
|
|
47
|
+
}));
|
|
48
|
+
const hrefs = [
|
|
49
|
+
linksFromToolCalls(many as never, "Tools").links[0]?.href,
|
|
50
|
+
linksFromRetrievalEvents(
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
id: "r1",
|
|
54
|
+
actionId: "a1",
|
|
55
|
+
retrieverName: "kb",
|
|
56
|
+
totalResults: 3,
|
|
57
|
+
},
|
|
58
|
+
] as never,
|
|
59
|
+
"R",
|
|
60
|
+
).links[0]?.href,
|
|
61
|
+
linksFromEvalResults(
|
|
62
|
+
[{ id: "e1", actionId: "a1", evaluatorName: "g", passed: 1 }] as never,
|
|
63
|
+
"E",
|
|
64
|
+
).links[0]?.href,
|
|
65
|
+
linksFromArtifacts(
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
id: "x1",
|
|
69
|
+
actionId: "a1",
|
|
70
|
+
artifactName: "plan",
|
|
71
|
+
artifactType: "json",
|
|
72
|
+
},
|
|
73
|
+
] as never,
|
|
74
|
+
"A",
|
|
75
|
+
).links[0]?.href,
|
|
76
|
+
];
|
|
77
|
+
expect(hrefs).toEqual([
|
|
78
|
+
"#/tool-calls",
|
|
79
|
+
"#/actions/a1",
|
|
80
|
+
"#/evaluations/e1",
|
|
81
|
+
"#/actions/a1",
|
|
82
|
+
]);
|
|
83
|
+
for (const href of hrefs)
|
|
84
|
+
expect(ROUTER_TABS.has(tabOf(href ?? ""))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("hrefForEntity only emits router tabs for every evidence kind", () => {
|
|
88
|
+
for (const kind of [
|
|
89
|
+
"trace",
|
|
90
|
+
"span",
|
|
91
|
+
"alert",
|
|
92
|
+
"analysis",
|
|
93
|
+
"service",
|
|
94
|
+
"agent_run",
|
|
95
|
+
"action",
|
|
96
|
+
"tool_call",
|
|
97
|
+
"ai_call",
|
|
98
|
+
"profile",
|
|
99
|
+
"change",
|
|
100
|
+
"finding",
|
|
101
|
+
"incident",
|
|
102
|
+
"eval",
|
|
103
|
+
"log",
|
|
104
|
+
"replay",
|
|
105
|
+
"docs",
|
|
106
|
+
]) {
|
|
107
|
+
expect(ROUTER_TABS.has(tabOf(hrefForEntity(kind, "id-1"))), kind).toBe(
|
|
108
|
+
true,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
expect(hrefForEntity("trace", "abc")).toBe("#/traces?trace=abc");
|
|
112
|
+
expect(hrefForEntity("alert", "al 1")).toBe("#/alerts?id=al%201");
|
|
113
|
+
expect(hrefForEntity("ai_call", "c1")).toBe("#/ai/calls/c1");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("resolveFaultLocusTarget", () => {
|
|
118
|
+
const trace = "a".repeat(32);
|
|
119
|
+
const span = "b".repeat(16);
|
|
120
|
+
it("classifies service nodes, bare services and spans", () => {
|
|
121
|
+
expect(
|
|
122
|
+
resolveFaultLocusTarget("service:prod:default:checkout-api"),
|
|
123
|
+
).toEqual({
|
|
124
|
+
entityKind: "service",
|
|
125
|
+
entityId: "service:prod:default:checkout-api",
|
|
126
|
+
href: "#/service-map?service=checkout-api",
|
|
127
|
+
});
|
|
128
|
+
expect(resolveFaultLocusTarget("checkout-api")).toMatchObject({
|
|
129
|
+
entityKind: "service",
|
|
130
|
+
href: "#/service-map?service=checkout-api",
|
|
131
|
+
});
|
|
132
|
+
expect(resolveFaultLocusTarget(`${trace}:${span}`)).toEqual({
|
|
133
|
+
entityKind: "span",
|
|
134
|
+
entityId: `${trace}:${span}`,
|
|
135
|
+
href: `#/traces?trace=${trace}&span=${span}`,
|
|
136
|
+
});
|
|
137
|
+
expect(resolveFaultLocusTarget(span)).toMatchObject({
|
|
138
|
+
entityKind: "span",
|
|
139
|
+
href: `#/evidence?kind=span&id=${span}`,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CausalProofTier,
|
|
3
|
+
EvidenceEntityKind,
|
|
3
4
|
RelationshipCertainty,
|
|
4
5
|
} from "@obsunified/types";
|
|
5
6
|
import type {
|
|
@@ -58,7 +59,7 @@ export interface ConnectedLink {
|
|
|
58
59
|
href: string;
|
|
59
60
|
count?: number;
|
|
60
61
|
sample?: string;
|
|
61
|
-
entityKind?: ConnectedEntityKind |
|
|
62
|
+
entityKind?: ConnectedEntityKind | EvidenceEntityKind;
|
|
62
63
|
entityId?: string;
|
|
63
64
|
source?: string;
|
|
64
65
|
confidence?: number;
|
|
@@ -347,7 +348,9 @@ export const linksFromAi = (
|
|
|
347
348
|
label: prefix,
|
|
348
349
|
links: calls.map((c) => ({
|
|
349
350
|
label: `${c.provider} · ${c.modelName}`,
|
|
350
|
-
href: `#/ai
|
|
351
|
+
href: `#/ai/calls/${encodeURIComponent(c.callId)}`,
|
|
352
|
+
entityKind: "ai_call",
|
|
353
|
+
entityId: c.callId,
|
|
351
354
|
})),
|
|
352
355
|
};
|
|
353
356
|
};
|
|
@@ -454,7 +457,7 @@ export const linksFromToolCalls = (
|
|
|
454
457
|
links: [
|
|
455
458
|
{
|
|
456
459
|
label: `${calls.length} tool calls`,
|
|
457
|
-
href: `#/
|
|
460
|
+
href: `#/tool-calls`,
|
|
458
461
|
count: calls.length,
|
|
459
462
|
sample: truncate(calls[0].toolName),
|
|
460
463
|
entityKind: "tool_call",
|
|
@@ -491,7 +494,8 @@ export const linksFromRetrievalEvents = (
|
|
|
491
494
|
label: prefix,
|
|
492
495
|
links: events.map((r) => ({
|
|
493
496
|
label: `retrieve: ${r.retrieverName} (${r.totalResults} docs)`,
|
|
494
|
-
|
|
497
|
+
// No dedicated retrieval tab: the owning action's detail page lists it.
|
|
498
|
+
href: `#/actions/${encodeURIComponent(r.actionId)}`,
|
|
495
499
|
})),
|
|
496
500
|
};
|
|
497
501
|
};
|
|
@@ -514,7 +518,9 @@ export const linksFromEvalResults = (
|
|
|
514
518
|
label: prefix,
|
|
515
519
|
links: results.map((e) => ({
|
|
516
520
|
label: `eval: ${e.evaluatorName} (${e.passed ? "passed" : "failed"})`,
|
|
517
|
-
|
|
521
|
+
// The Evaluations tab resolves this id to an eval case, falling back
|
|
522
|
+
// to the case saved from the same action.
|
|
523
|
+
href: `#/evaluations/${encodeURIComponent(e.id)}`,
|
|
518
524
|
entityKind: "eval",
|
|
519
525
|
entityId: e.id,
|
|
520
526
|
...metadataForAction?.(e.actionId),
|
|
@@ -537,7 +543,118 @@ export const linksFromArtifacts = (
|
|
|
537
543
|
label: prefix,
|
|
538
544
|
links: arts.map((a) => ({
|
|
539
545
|
label: `artifact: ${a.artifactName} (${a.artifactType})`,
|
|
540
|
-
|
|
546
|
+
// No dedicated artifact tab: the owning action's detail page lists it.
|
|
547
|
+
href: `#/actions/${encodeURIComponent(a.actionId)}`,
|
|
541
548
|
})),
|
|
542
549
|
};
|
|
543
550
|
};
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Dashboard destination for an evidence entity, using only routes the web
|
|
554
|
+
* router (`apps/web/src/app/router.ts`) actually serves. Unknown kinds fall
|
|
555
|
+
* back to the evidence explorer so a link never lands on the Health tab.
|
|
556
|
+
*/
|
|
557
|
+
export const hrefForEntity = (kind: string, id: string): string => {
|
|
558
|
+
const enc = encodeURIComponent;
|
|
559
|
+
switch (kind) {
|
|
560
|
+
case "trace":
|
|
561
|
+
return `#/traces?trace=${enc(id)}`;
|
|
562
|
+
case "span": {
|
|
563
|
+
const [traceId, spanId] = id.split(":");
|
|
564
|
+
return spanId && traceId
|
|
565
|
+
? `#/traces?trace=${enc(traceId)}&span=${enc(spanId)}`
|
|
566
|
+
: `#/evidence?kind=span&id=${enc(id)}`;
|
|
567
|
+
}
|
|
568
|
+
case "alert":
|
|
569
|
+
return `#/alerts?id=${enc(id)}`;
|
|
570
|
+
case "analysis":
|
|
571
|
+
return "#/health";
|
|
572
|
+
case "service":
|
|
573
|
+
return `#/service-map?service=${enc(id)}`;
|
|
574
|
+
case "agent_run":
|
|
575
|
+
return `#/agent-runs/${enc(id)}`;
|
|
576
|
+
case "action":
|
|
577
|
+
return `#/actions/${enc(id)}`;
|
|
578
|
+
case "tool_call":
|
|
579
|
+
return `#/tool-calls/${enc(id)}`;
|
|
580
|
+
case "ai_call":
|
|
581
|
+
return `#/ai/calls/${enc(id)}`;
|
|
582
|
+
case "profile":
|
|
583
|
+
return `#/profiles/${enc(id)}`;
|
|
584
|
+
case "change":
|
|
585
|
+
return `#/changes/${enc(id)}`;
|
|
586
|
+
case "finding":
|
|
587
|
+
return `#/findings/${enc(id)}`;
|
|
588
|
+
case "incident":
|
|
589
|
+
return `#/incidents/${enc(id)}`;
|
|
590
|
+
case "eval":
|
|
591
|
+
return `#/evaluations/${enc(id)}`;
|
|
592
|
+
case "log":
|
|
593
|
+
return `#/logs?id=${enc(id)}`;
|
|
594
|
+
case "replay":
|
|
595
|
+
return `#/replay?session=${enc(id)}`;
|
|
596
|
+
default:
|
|
597
|
+
return `#/evidence?kind=${enc(kind)}&id=${enc(id)}`;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
export interface FaultLocusTarget {
|
|
602
|
+
entityKind: "service" | "span";
|
|
603
|
+
entityId: string;
|
|
604
|
+
href: string;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const HEX_SPAN_PAIR = /^[0-9a-f]{32}:[0-9a-f]{16}$/i;
|
|
608
|
+
const HEX_SPAN_ID = /^[0-9a-f]{16}$/i;
|
|
609
|
+
const NODE_KINDS = new Set([
|
|
610
|
+
"service",
|
|
611
|
+
"endpoint",
|
|
612
|
+
"database",
|
|
613
|
+
"queue",
|
|
614
|
+
"messaging_topic",
|
|
615
|
+
"topic",
|
|
616
|
+
"cache",
|
|
617
|
+
"external",
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* A finding's `faultLocusEntityId` is a canonical ontology node id
|
|
622
|
+
* (`service:<env>:<ns>:<name>`, `endpoint:<env>:<ns>:<svc>:...`), a
|
|
623
|
+
* `traceId:spanId` pair, a bare span id, or a bare service name. Resolve it to
|
|
624
|
+
* the entity kind it really is and the surface that shows that kind.
|
|
625
|
+
*/
|
|
626
|
+
export const resolveFaultLocusTarget = (
|
|
627
|
+
faultLocusEntityId: string,
|
|
628
|
+
): FaultLocusTarget => {
|
|
629
|
+
const id = faultLocusEntityId;
|
|
630
|
+
if (HEX_SPAN_PAIR.test(id)) {
|
|
631
|
+
return {
|
|
632
|
+
entityKind: "span",
|
|
633
|
+
entityId: id,
|
|
634
|
+
href: hrefForEntity("span", id),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (HEX_SPAN_ID.test(id)) {
|
|
638
|
+
return {
|
|
639
|
+
entityKind: "span",
|
|
640
|
+
entityId: id,
|
|
641
|
+
href: hrefForEntity("span", id),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const segments = id.split(":");
|
|
645
|
+
if (segments.length > 1 && NODE_KINDS.has(segments[0] ?? "")) {
|
|
646
|
+
// service:<env>:<ns>:<name> and endpoint:<env>:<ns>:<svc>:... both carry
|
|
647
|
+
// the owning service name at index 3.
|
|
648
|
+
const name = segments[3] || segments[segments.length - 1] || id;
|
|
649
|
+
return {
|
|
650
|
+
entityKind: "service",
|
|
651
|
+
entityId: id,
|
|
652
|
+
href: hrefForEntity("service", name),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
entityKind: "service",
|
|
657
|
+
entityId: id,
|
|
658
|
+
href: hrefForEntity("service", id),
|
|
659
|
+
};
|
|
660
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { CollectorRuntime } from "../framework/collector";
|
|
4
|
+
import type { CollectorEnv } from "../framework/env";
|
|
5
|
+
import { recordInvestigationFinding } from "../lib/autosre-findings";
|
|
6
|
+
import { openOrDeduplicateIncident } from "../lib/autosre-remediation-gate";
|
|
7
|
+
import { createMigratedSqliteDb } from "../lib/test-utils/node-sqlite-db";
|
|
8
|
+
import { connectedRoutesPlugin } from "./connected-routes";
|
|
9
|
+
|
|
10
|
+
type Link = { href: string; entityKind?: string; entityId?: string };
|
|
11
|
+
type Manifest = { up: { links: Link[] }[]; across: { links: Link[] }[] };
|
|
12
|
+
|
|
13
|
+
const SERVICE_NODE = "service:prod:default:checkout-api";
|
|
14
|
+
|
|
15
|
+
const setup = () => {
|
|
16
|
+
const db = createMigratedSqliteDb();
|
|
17
|
+
const app = new Hono<{ Bindings: CollectorEnv }>();
|
|
18
|
+
connectedRoutesPlugin.register(app, new CollectorRuntime());
|
|
19
|
+
const env = { DB: db as unknown as D1Database } as CollectorEnv;
|
|
20
|
+
const get = async (path: string) =>
|
|
21
|
+
(await (await app.request(path, {}, env)).json()) as Manifest;
|
|
22
|
+
return { db, get };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const openIncident = async (
|
|
26
|
+
db: ReturnType<typeof setup>["db"],
|
|
27
|
+
kind: "trace" | "alert",
|
|
28
|
+
anchorId: string,
|
|
29
|
+
) =>
|
|
30
|
+
(
|
|
31
|
+
await openOrDeduplicateIncident(db, "default", {
|
|
32
|
+
environment: "prod",
|
|
33
|
+
targetNodeId: SERVICE_NODE,
|
|
34
|
+
symptomAnchorKind: kind,
|
|
35
|
+
symptomAnchorId: anchorId,
|
|
36
|
+
symptomFingerprint: `fp_${kind}`,
|
|
37
|
+
symptomCategory: `fp_${kind}`,
|
|
38
|
+
})
|
|
39
|
+
).incident;
|
|
40
|
+
|
|
41
|
+
describe("connected links carry the real entity kind and a matching href", () => {
|
|
42
|
+
it("incident trigger signal for a trace anchor is a trace link", async () => {
|
|
43
|
+
const { db, get } = setup();
|
|
44
|
+
const incident = await openIncident(db, "trace", "trace-123");
|
|
45
|
+
const m = await get(
|
|
46
|
+
`/internal/connected/incident/${incident.id}?environment=prod`,
|
|
47
|
+
);
|
|
48
|
+
expect(m.up[0]?.links[0]).toMatchObject({
|
|
49
|
+
entityKind: "trace",
|
|
50
|
+
entityId: "trace-123",
|
|
51
|
+
href: "#/traces?trace=trace-123",
|
|
52
|
+
});
|
|
53
|
+
db.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("incident trigger signal for an alert anchor stays an alert link", async () => {
|
|
57
|
+
const { db, get } = setup();
|
|
58
|
+
const incident = await openIncident(db, "alert", "rule-9");
|
|
59
|
+
const m = await get(
|
|
60
|
+
`/internal/connected/incident/${incident.id}?environment=prod`,
|
|
61
|
+
);
|
|
62
|
+
expect(m.up[0]?.links[0]).toMatchObject({
|
|
63
|
+
entityKind: "alert",
|
|
64
|
+
entityId: "rule-9",
|
|
65
|
+
href: "#/alerts?id=rule-9",
|
|
66
|
+
});
|
|
67
|
+
db.close();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
[SERVICE_NODE, "service", "#/service-map?service=checkout-api"],
|
|
72
|
+
["checkout-api", "service", "#/service-map?service=checkout-api"],
|
|
73
|
+
])("finding fault locus %s links as %s", async (locus, kind, href) => {
|
|
74
|
+
const { db, get } = setup();
|
|
75
|
+
const incident = await openIncident(db, "trace", "t1");
|
|
76
|
+
const finding = await recordInvestigationFinding(db, "default", {
|
|
77
|
+
environment: "prod",
|
|
78
|
+
incidentId: incident.id,
|
|
79
|
+
title: "Regression",
|
|
80
|
+
summary: "Errors after deploy",
|
|
81
|
+
causalProofTier: "corroborated_candidate",
|
|
82
|
+
symptomEntityKind: "trace",
|
|
83
|
+
symptomEntityId: "t1",
|
|
84
|
+
faultLocusEntityKind: "service",
|
|
85
|
+
faultLocusEntityId: locus,
|
|
86
|
+
causalChain: [],
|
|
87
|
+
citedEvidenceIds: [],
|
|
88
|
+
});
|
|
89
|
+
const m = await get(
|
|
90
|
+
`/internal/connected/finding/${finding.id}?environment=prod`,
|
|
91
|
+
);
|
|
92
|
+
const link = m.across[0]?.links[0];
|
|
93
|
+
expect(link).toMatchObject({ entityKind: kind, entityId: locus, href });
|
|
94
|
+
db.close();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -470,7 +470,7 @@ describe("ConnectedRail manifest — raw signal action back-links", () => {
|
|
|
470
470
|
m.down.find((s) => s.label === "Evaluations for this span")?.links[0],
|
|
471
471
|
).toMatchObject({
|
|
472
472
|
label: "eval: groundedness (passed)",
|
|
473
|
-
href: "#/
|
|
473
|
+
href: "#/evaluations/eval-explicit",
|
|
474
474
|
});
|
|
475
475
|
expect(
|
|
476
476
|
m.related.find((s) => s.label === "Agent runs for this span")?.links[0],
|
|
@@ -749,7 +749,7 @@ describe("ConnectedRail manifest — raw signal action back-links", () => {
|
|
|
749
749
|
).toMatchObject({ href: "#/tool-calls/tool-explicit" });
|
|
750
750
|
expect(
|
|
751
751
|
m.down.find((s) => s.label === "Evaluations for this AI call")?.links[0],
|
|
752
|
-
).toMatchObject({ href: "#/
|
|
752
|
+
).toMatchObject({ href: "#/evaluations/eval-explicit" });
|
|
753
753
|
expect(
|
|
754
754
|
m.related.find((s) => s.label === "Agent runs for this AI call")
|
|
755
755
|
?.links[0],
|
|
@@ -1280,7 +1280,7 @@ describe("ConnectedRail scenario acceptance contracts", () => {
|
|
|
1280
1280
|
const aiSection = userManifest.across.find((s) =>
|
|
1281
1281
|
s.label.toLowerCase().includes("ai call"),
|
|
1282
1282
|
);
|
|
1283
|
-
expect(aiSection?.links[0].href).toBe("#/ai
|
|
1283
|
+
expect(aiSection?.links[0].href).toBe("#/ai/calls/ai-heavy-1");
|
|
1284
1284
|
|
|
1285
1285
|
const sessionManifest = await fetch(
|
|
1286
1286
|
"/internal/connected/usage/sess-heavy?session_id=sess-heavy",
|