@obsunified/collector 2.2.1 → 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/auth.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
- package/dist/chunk-RASSTGNB.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +2892 -2240
- 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 -3
- package/src/__tests__/autosre-closed-loop.test.ts +700 -0
- package/src/__tests__/db-parity.integration.test.ts +961 -0
- package/src/__tests__/internal-contracts-routes.test.ts +451 -0
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/framework/env.ts +105 -0
- package/src/index.ts +13 -4
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store/evaluations.test.ts +3 -3
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/analyses-store.test.ts +6 -6
- package/src/lib/autosre-findings.ts +169 -14
- package/src/lib/autosre-remediation-gate.ts +156 -26
- 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/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/stage6.test.ts +1 -1
- package/src/lib/usage-store.ts +9 -3
- package/src/migrations/052_remediation_callback.sql +10 -0
- package/src/migrations/053_remediation_callback_delivery.sql +7 -0
- package/src/migrations/parity.test.ts +105 -3
- 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 +15 -0
- package/src/plugins/autosre-routes.ts +409 -147
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +224 -0
- package/src/plugins/connected-routes/manifest.ts +131 -28
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +13 -11
- package/src/plugins/connected-routes.ts +53 -20
- package/src/plugins/eval-cases-routes.ts +33 -10
- package/src/plugins/evidence-retrieval-routes.ts +89 -24
- package/dist/chunk-KMEZKGSO.js.map +0 -1
- package/dist/chunk-ZSF5XP3B.js.map +0 -1
- /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
|
@@ -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,224 @@
|
|
|
1
|
+
import { KNOWN_TABS, parseDashboardRouteHash } from "@obsunified/types";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
hrefForEntity,
|
|
5
|
+
linksFromActions,
|
|
6
|
+
linksFromAi,
|
|
7
|
+
linksFromArtifacts,
|
|
8
|
+
linksFromEvalResults,
|
|
9
|
+
linksFromLogs,
|
|
10
|
+
linksFromMetricExemplars,
|
|
11
|
+
linksFromRetrievalEvents,
|
|
12
|
+
linksFromSpans,
|
|
13
|
+
linksFromToolCalls,
|
|
14
|
+
linksFromUsage,
|
|
15
|
+
linkToAction,
|
|
16
|
+
linkToAgentRun,
|
|
17
|
+
linkToSession,
|
|
18
|
+
linkToTrace,
|
|
19
|
+
MAX_LINKS_INLINE,
|
|
20
|
+
resolveFaultLocusTarget,
|
|
21
|
+
} from "./manifest";
|
|
22
|
+
|
|
23
|
+
const tabOf = (href: string) =>
|
|
24
|
+
href.replace(/^#\//, "").split(/[/?#]/)[0] ?? "";
|
|
25
|
+
|
|
26
|
+
describe("connected manifest hrefs target real router tabs", () => {
|
|
27
|
+
it("tool call overflow, retrievals, evals and artifacts avoid unknown tabs", () => {
|
|
28
|
+
const many = Array.from({ length: MAX_LINKS_INLINE + 1 }, (_, i) => ({
|
|
29
|
+
id: `t${i}`,
|
|
30
|
+
actionId: "a1",
|
|
31
|
+
toolName: "search",
|
|
32
|
+
}));
|
|
33
|
+
const hrefs = [
|
|
34
|
+
linksFromToolCalls(many as never, "Tools").links[0]?.href,
|
|
35
|
+
linksFromRetrievalEvents(
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
id: "r1",
|
|
39
|
+
actionId: "a1",
|
|
40
|
+
retrieverName: "kb",
|
|
41
|
+
totalResults: 3,
|
|
42
|
+
},
|
|
43
|
+
] as never,
|
|
44
|
+
"R",
|
|
45
|
+
).links[0]?.href,
|
|
46
|
+
linksFromEvalResults(
|
|
47
|
+
[{ id: "e1", actionId: "a1", evaluatorName: "g", passed: 1 }] as never,
|
|
48
|
+
"E",
|
|
49
|
+
).links[0]?.href,
|
|
50
|
+
linksFromArtifacts(
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
id: "x1",
|
|
54
|
+
actionId: "a1",
|
|
55
|
+
artifactName: "plan",
|
|
56
|
+
artifactType: "json",
|
|
57
|
+
},
|
|
58
|
+
] as never,
|
|
59
|
+
"A",
|
|
60
|
+
).links[0]?.href,
|
|
61
|
+
];
|
|
62
|
+
expect(hrefs).toEqual([
|
|
63
|
+
"#/tool-calls",
|
|
64
|
+
"#/actions/a1",
|
|
65
|
+
"#/evaluations/e1",
|
|
66
|
+
"#/actions/a1",
|
|
67
|
+
]);
|
|
68
|
+
for (const href of hrefs) {
|
|
69
|
+
expect(KNOWN_TABS.has(tabOf(href ?? ""))).toBe(true);
|
|
70
|
+
expect(parseDashboardRouteHash(href ?? "").ok).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("emits canonical routes from all link builders and round-trips through parseDashboardRouteHash", () => {
|
|
75
|
+
expect(linkToTrace("trace-1").href).toBe("#/traces?trace=trace-1");
|
|
76
|
+
expect(linkToSession("sess/1").href).toBe("#/replay?session=sess%2F1");
|
|
77
|
+
expect(linkToAction("act/1").href).toBe("#/actions/act%2F1");
|
|
78
|
+
expect(linkToAgentRun("run/1").href).toBe("#/agent-runs/run%2F1");
|
|
79
|
+
|
|
80
|
+
const inlineSpans = linksFromSpans(
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
traceId: "tr-1",
|
|
84
|
+
spanId: "sp-1",
|
|
85
|
+
serviceName: "api",
|
|
86
|
+
spanName: "GET /",
|
|
87
|
+
} as never,
|
|
88
|
+
],
|
|
89
|
+
"Spans",
|
|
90
|
+
);
|
|
91
|
+
expect(inlineSpans.links[0]?.href).toBe("#/traces?trace=tr-1&span=sp-1");
|
|
92
|
+
expect(parseDashboardRouteHash(inlineSpans.links[0]?.href ?? "")).toEqual({
|
|
93
|
+
ok: true,
|
|
94
|
+
route: { tab: "traces", traceId: "tr-1", spanId: "sp-1" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const manySpans = linksFromSpans(
|
|
98
|
+
Array.from({ length: MAX_LINKS_INLINE + 1 }, (_, i) => ({
|
|
99
|
+
traceId: "tr-many",
|
|
100
|
+
spanId: `sp-${i}`,
|
|
101
|
+
serviceName: "api",
|
|
102
|
+
spanName: "op",
|
|
103
|
+
})) as never,
|
|
104
|
+
"Spans",
|
|
105
|
+
);
|
|
106
|
+
expect(manySpans.links[0]?.href).toBe("#/traces?q=tr-many");
|
|
107
|
+
|
|
108
|
+
const inlineLogs = linksFromLogs(
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
logId: "log-1",
|
|
112
|
+
traceId: "tr-1",
|
|
113
|
+
severity: "ERROR",
|
|
114
|
+
message: "boom",
|
|
115
|
+
} as never,
|
|
116
|
+
],
|
|
117
|
+
"Logs",
|
|
118
|
+
);
|
|
119
|
+
expect(inlineLogs.links[0]?.href).toBe("#/logs?id=log-1");
|
|
120
|
+
|
|
121
|
+
const inlineUsage = linksFromUsage(
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
eventId: "ev-1",
|
|
125
|
+
eventType: "click",
|
|
126
|
+
eventName: "buy",
|
|
127
|
+
} as never,
|
|
128
|
+
],
|
|
129
|
+
"Usage",
|
|
130
|
+
);
|
|
131
|
+
expect(inlineUsage.links[0]?.href).toBe("#/usage?id=ev-1");
|
|
132
|
+
|
|
133
|
+
const inlineExemplars = linksFromMetricExemplars(
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
traceId: "tr-1",
|
|
137
|
+
spanId: "sp-2",
|
|
138
|
+
serviceName: "api",
|
|
139
|
+
metricName: "latency",
|
|
140
|
+
value: 42,
|
|
141
|
+
tsNs: "100",
|
|
142
|
+
} as never,
|
|
143
|
+
],
|
|
144
|
+
"Exemplars",
|
|
145
|
+
);
|
|
146
|
+
expect(inlineExemplars.links[0]?.href).toBe(
|
|
147
|
+
"#/traces?trace=tr-1&span=sp-2",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const inlineAi = linksFromAi(
|
|
151
|
+
[
|
|
152
|
+
{
|
|
153
|
+
callId: "call/1",
|
|
154
|
+
provider: "openai",
|
|
155
|
+
modelName: "gpt-4o",
|
|
156
|
+
} as never,
|
|
157
|
+
],
|
|
158
|
+
"AI",
|
|
159
|
+
);
|
|
160
|
+
expect(inlineAi.links[0]?.href).toBe("#/ai/calls/call%2F1");
|
|
161
|
+
|
|
162
|
+
const inlineActions = linksFromActions(
|
|
163
|
+
[{ id: "act-1", actionKind: "step", name: "plan" } as never],
|
|
164
|
+
"Actions",
|
|
165
|
+
);
|
|
166
|
+
expect(inlineActions.links[0]?.href).toBe("#/actions/act-1");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("hrefForEntity only emits router tabs for every evidence kind", () => {
|
|
170
|
+
for (const kind of [
|
|
171
|
+
"trace",
|
|
172
|
+
"span",
|
|
173
|
+
"alert",
|
|
174
|
+
"analysis",
|
|
175
|
+
"service",
|
|
176
|
+
"agent_run",
|
|
177
|
+
"action",
|
|
178
|
+
"tool_call",
|
|
179
|
+
"ai_call",
|
|
180
|
+
"profile",
|
|
181
|
+
"change",
|
|
182
|
+
"finding",
|
|
183
|
+
"incident",
|
|
184
|
+
"eval",
|
|
185
|
+
"log",
|
|
186
|
+
"replay",
|
|
187
|
+
"docs",
|
|
188
|
+
]) {
|
|
189
|
+
const href = hrefForEntity(kind, "id-1");
|
|
190
|
+
expect(KNOWN_TABS.has(tabOf(href)), kind).toBe(true);
|
|
191
|
+
expect(parseDashboardRouteHash(href).ok, kind).toBe(true);
|
|
192
|
+
}
|
|
193
|
+
expect(hrefForEntity("trace", "abc")).toBe("#/traces?trace=abc");
|
|
194
|
+
expect(hrefForEntity("alert", "al 1")).toBe("#/alerts?id=al+1");
|
|
195
|
+
expect(hrefForEntity("ai_call", "c1")).toBe("#/ai/calls/c1");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("resolveFaultLocusTarget", () => {
|
|
200
|
+
const trace = "a".repeat(32);
|
|
201
|
+
const span = "b".repeat(16);
|
|
202
|
+
it("classifies service nodes, bare services and spans", () => {
|
|
203
|
+
expect(
|
|
204
|
+
resolveFaultLocusTarget("service:prod:default:checkout-api"),
|
|
205
|
+
).toEqual({
|
|
206
|
+
entityKind: "service",
|
|
207
|
+
entityId: "service:prod:default:checkout-api",
|
|
208
|
+
href: "#/service-map?service=checkout-api",
|
|
209
|
+
});
|
|
210
|
+
expect(resolveFaultLocusTarget("checkout-api")).toMatchObject({
|
|
211
|
+
entityKind: "service",
|
|
212
|
+
href: "#/service-map?service=checkout-api",
|
|
213
|
+
});
|
|
214
|
+
expect(resolveFaultLocusTarget(`${trace}:${span}`)).toEqual({
|
|
215
|
+
entityKind: "span",
|
|
216
|
+
entityId: `${trace}:${span}`,
|
|
217
|
+
href: `#/traces?trace=${trace}&span=${span}`,
|
|
218
|
+
});
|
|
219
|
+
expect(resolveFaultLocusTarget(span)).toMatchObject({
|
|
220
|
+
entityKind: "span",
|
|
221
|
+
href: `#/evidence?kind=span&id=${span}`,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type CausalProofTier,
|
|
3
|
+
dashboardRouteToHref,
|
|
4
|
+
type EvidenceEntityKind,
|
|
5
|
+
type RelationshipCertainty,
|
|
6
|
+
routeForEntity,
|
|
7
|
+
hrefForEntity as sharedHrefForEntity,
|
|
8
|
+
} from "@obsunified/types";
|
|
2
9
|
import type {
|
|
3
10
|
ActionRef,
|
|
4
11
|
AICallRef,
|
|
@@ -15,6 +22,8 @@ import type {
|
|
|
15
22
|
import type { CausalConfidence } from "../../lib/identity-index/types";
|
|
16
23
|
import type { SqlDb } from "../../lib/sql-db";
|
|
17
24
|
|
|
25
|
+
export { routeForEntity };
|
|
26
|
+
|
|
18
27
|
export type ConnectedEntityKind =
|
|
19
28
|
| "span"
|
|
20
29
|
| "profile"
|
|
@@ -55,7 +64,7 @@ export interface ConnectedLink {
|
|
|
55
64
|
href: string;
|
|
56
65
|
count?: number;
|
|
57
66
|
sample?: string;
|
|
58
|
-
entityKind?: ConnectedEntityKind |
|
|
67
|
+
entityKind?: ConnectedEntityKind | EvidenceEntityKind;
|
|
59
68
|
entityId?: string;
|
|
60
69
|
source?: string;
|
|
61
70
|
confidence?: number;
|
|
@@ -90,7 +99,7 @@ export const linkToTrace = (
|
|
|
90
99
|
label?: string,
|
|
91
100
|
): ConnectedLink => ({
|
|
92
101
|
label: label ?? `trace ${traceId.slice(0, 12)}`,
|
|
93
|
-
href:
|
|
102
|
+
href: dashboardRouteToHref({ tab: "traces", traceId }),
|
|
94
103
|
entityKind: "trace",
|
|
95
104
|
entityId: traceId,
|
|
96
105
|
});
|
|
@@ -104,7 +113,7 @@ export const linkToSession = (sessionId: string): ConnectedLink => ({
|
|
|
104
113
|
sessionId.length > 12
|
|
105
114
|
? `session …${sessionId.slice(-12)}`
|
|
106
115
|
: `session ${sessionId}`,
|
|
107
|
-
href:
|
|
116
|
+
href: dashboardRouteToHref({ tab: "replay", sessionId }),
|
|
108
117
|
sample: sessionId,
|
|
109
118
|
entityKind: "replay",
|
|
110
119
|
entityId: sessionId,
|
|
@@ -127,7 +136,10 @@ export const linksFromSpans = (
|
|
|
127
136
|
links: [
|
|
128
137
|
{
|
|
129
138
|
label: `${spans.length} spans`,
|
|
130
|
-
href:
|
|
139
|
+
href: dashboardRouteToHref({
|
|
140
|
+
tab: "traces",
|
|
141
|
+
query: spans[0].traceId,
|
|
142
|
+
}),
|
|
131
143
|
count: spans.length,
|
|
132
144
|
sample: truncate(
|
|
133
145
|
`${spans[0].serviceName ?? "?"} · ${spans[0].spanName}`,
|
|
@@ -140,7 +152,11 @@ export const linksFromSpans = (
|
|
|
140
152
|
label: prefix,
|
|
141
153
|
links: spans.map((s) => ({
|
|
142
154
|
label: `${s.serviceName ?? "?"} · ${truncate(s.spanName, 40)}`,
|
|
143
|
-
href:
|
|
155
|
+
href: dashboardRouteToHref({
|
|
156
|
+
tab: "traces",
|
|
157
|
+
traceId: s.traceId,
|
|
158
|
+
spanId: s.spanId,
|
|
159
|
+
}),
|
|
144
160
|
})),
|
|
145
161
|
};
|
|
146
162
|
};
|
|
@@ -163,7 +179,10 @@ export const linksFromLogs = (
|
|
|
163
179
|
links: [
|
|
164
180
|
{
|
|
165
181
|
label: `${logs.length} logs`,
|
|
166
|
-
href:
|
|
182
|
+
href: dashboardRouteToHref({
|
|
183
|
+
tab: "logs",
|
|
184
|
+
...(sample.traceId ? { query: sample.traceId } : {}),
|
|
185
|
+
}),
|
|
167
186
|
count: logs.length,
|
|
168
187
|
sample: truncate(sample.message),
|
|
169
188
|
},
|
|
@@ -174,7 +193,7 @@ export const linksFromLogs = (
|
|
|
174
193
|
label: prefix,
|
|
175
194
|
links: logs.map((l) => ({
|
|
176
195
|
label: `[${l.severity}] ${truncate(l.message, 60)}`,
|
|
177
|
-
href:
|
|
196
|
+
href: dashboardRouteToHref({ tab: "logs", logId: l.logId }),
|
|
178
197
|
})),
|
|
179
198
|
};
|
|
180
199
|
};
|
|
@@ -196,7 +215,7 @@ export const linksFromUsage = (
|
|
|
196
215
|
links: [
|
|
197
216
|
{
|
|
198
217
|
label: `${events.length} usage events`,
|
|
199
|
-
href:
|
|
218
|
+
href: dashboardRouteToHref({ tab: "usage" }),
|
|
200
219
|
count: events.length,
|
|
201
220
|
sample: truncate(`${events[0].eventType} · ${events[0].eventName}`),
|
|
202
221
|
},
|
|
@@ -207,7 +226,7 @@ export const linksFromUsage = (
|
|
|
207
226
|
label: prefix,
|
|
208
227
|
links: events.map((e) => ({
|
|
209
228
|
label: `${e.eventType} · ${truncate(e.eventName, 50)}`,
|
|
210
|
-
href:
|
|
229
|
+
href: dashboardRouteToHref({ tab: "usage", usageId: e.eventId }),
|
|
211
230
|
})),
|
|
212
231
|
};
|
|
213
232
|
};
|
|
@@ -231,8 +250,8 @@ export const linksFromMetricExemplars = (
|
|
|
231
250
|
{
|
|
232
251
|
label: `${exemplars.length} metric exemplars`,
|
|
233
252
|
href: sample.traceId
|
|
234
|
-
?
|
|
235
|
-
:
|
|
253
|
+
? dashboardRouteToHref({ tab: "traces", traceId: sample.traceId })
|
|
254
|
+
: dashboardRouteToHref({ tab: "traces", query: sample.metricName }),
|
|
236
255
|
count: exemplars.length,
|
|
237
256
|
sample: truncate(
|
|
238
257
|
`${sample.serviceName ?? "?"} · ${sample.metricName}=${sample.value}`,
|
|
@@ -246,10 +265,12 @@ export const linksFromMetricExemplars = (
|
|
|
246
265
|
links: exemplars.map((e) => ({
|
|
247
266
|
label: `${e.serviceName ?? "?"} · ${truncate(e.metricName, 40)}=${e.value}`,
|
|
248
267
|
href: e.traceId
|
|
249
|
-
?
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
? dashboardRouteToHref({
|
|
269
|
+
tab: "traces",
|
|
270
|
+
traceId: e.traceId,
|
|
271
|
+
...(e.spanId ? { spanId: e.spanId } : {}),
|
|
272
|
+
})
|
|
273
|
+
: dashboardRouteToHref({ tab: "traces", query: e.metricName }),
|
|
253
274
|
sample: e.tsNs,
|
|
254
275
|
})),
|
|
255
276
|
};
|
|
@@ -309,7 +330,11 @@ export const profileLinksForTrace = async (
|
|
|
309
330
|
label: `${icon} ${type === "offcpu" ? "Off-CPU profiles" : `${type[0].toUpperCase()}${type.slice(1)} profiles`}`,
|
|
310
331
|
links: list.map((r) => ({
|
|
311
332
|
label: `${r.service_name ?? "?"} · ${r.duration_ms}ms`,
|
|
312
|
-
href:
|
|
333
|
+
href: dashboardRouteToHref({
|
|
334
|
+
tab: "profiles",
|
|
335
|
+
profileId: r.id,
|
|
336
|
+
traceId,
|
|
337
|
+
}),
|
|
313
338
|
})),
|
|
314
339
|
});
|
|
315
340
|
}
|
|
@@ -333,7 +358,7 @@ export const linksFromAi = (
|
|
|
333
358
|
links: [
|
|
334
359
|
{
|
|
335
360
|
label: `${calls.length} AI calls`,
|
|
336
|
-
href:
|
|
361
|
+
href: dashboardRouteToHref({ tab: "ai" }),
|
|
337
362
|
count: calls.length,
|
|
338
363
|
sample: truncate(`${calls[0].provider} · ${calls[0].modelName}`),
|
|
339
364
|
},
|
|
@@ -344,7 +369,9 @@ export const linksFromAi = (
|
|
|
344
369
|
label: prefix,
|
|
345
370
|
links: calls.map((c) => ({
|
|
346
371
|
label: `${c.provider} · ${c.modelName}`,
|
|
347
|
-
href:
|
|
372
|
+
href: dashboardRouteToHref({ tab: "ai", aiCallId: c.callId }),
|
|
373
|
+
entityKind: "ai_call",
|
|
374
|
+
entityId: c.callId,
|
|
348
375
|
})),
|
|
349
376
|
};
|
|
350
377
|
};
|
|
@@ -354,7 +381,7 @@ export const linkToAction = (
|
|
|
354
381
|
label?: string,
|
|
355
382
|
): ConnectedLink => ({
|
|
356
383
|
label: label ?? `action ${actionId.slice(0, 12)}`,
|
|
357
|
-
href:
|
|
384
|
+
href: dashboardRouteToHref({ tab: "actions", actionId }),
|
|
358
385
|
entityKind: "action",
|
|
359
386
|
entityId: actionId,
|
|
360
387
|
});
|
|
@@ -364,7 +391,7 @@ export const linkToAgentRun = (
|
|
|
364
391
|
label?: string,
|
|
365
392
|
): ConnectedLink => ({
|
|
366
393
|
label: label ?? `agent run ${runId.slice(0, 12)}`,
|
|
367
|
-
href:
|
|
394
|
+
href: dashboardRouteToHref({ tab: "agent-runs", agentRunId: runId }),
|
|
368
395
|
entityKind: "agent_run",
|
|
369
396
|
entityId: runId,
|
|
370
397
|
});
|
|
@@ -407,7 +434,7 @@ export const linksFromActions = (
|
|
|
407
434
|
links: [
|
|
408
435
|
{
|
|
409
436
|
label: `${actions.length} actions`,
|
|
410
|
-
href:
|
|
437
|
+
href: dashboardRouteToHref({ tab: "actions" }),
|
|
411
438
|
count: actions.length,
|
|
412
439
|
sample: truncate(
|
|
413
440
|
`${actions[0].actionKind} · ${actions[0].name ?? "unnamed"}`,
|
|
@@ -423,7 +450,7 @@ export const linksFromActions = (
|
|
|
423
450
|
label: prefix,
|
|
424
451
|
links: actions.map((a) => ({
|
|
425
452
|
label: `[${a.actionKind}] ${truncate(a.name ?? "unnamed", 40)}`,
|
|
426
|
-
href:
|
|
453
|
+
href: dashboardRouteToHref({ tab: "actions", actionId: a.id }),
|
|
427
454
|
entityKind: "action",
|
|
428
455
|
entityId: a.id,
|
|
429
456
|
...actionContextMetadata(a, metadata),
|
|
@@ -451,7 +478,7 @@ export const linksFromToolCalls = (
|
|
|
451
478
|
links: [
|
|
452
479
|
{
|
|
453
480
|
label: `${calls.length} tool calls`,
|
|
454
|
-
href:
|
|
481
|
+
href: dashboardRouteToHref({ tab: "tool-calls" }),
|
|
455
482
|
count: calls.length,
|
|
456
483
|
sample: truncate(calls[0].toolName),
|
|
457
484
|
entityKind: "tool_call",
|
|
@@ -465,7 +492,7 @@ export const linksFromToolCalls = (
|
|
|
465
492
|
label: prefix,
|
|
466
493
|
links: calls.map((t) => ({
|
|
467
494
|
label: `tool: ${t.toolName}`,
|
|
468
|
-
href:
|
|
495
|
+
href: dashboardRouteToHref({ tab: "tool-calls", toolCallId: t.id }),
|
|
469
496
|
entityKind: "tool_call",
|
|
470
497
|
entityId: t.id,
|
|
471
498
|
...metadataForAction?.(t.actionId),
|
|
@@ -488,7 +515,8 @@ export const linksFromRetrievalEvents = (
|
|
|
488
515
|
label: prefix,
|
|
489
516
|
links: events.map((r) => ({
|
|
490
517
|
label: `retrieve: ${r.retrieverName} (${r.totalResults} docs)`,
|
|
491
|
-
|
|
518
|
+
// No dedicated retrieval tab: the owning action's detail page lists it.
|
|
519
|
+
href: dashboardRouteToHref({ tab: "actions", actionId: r.actionId }),
|
|
492
520
|
})),
|
|
493
521
|
};
|
|
494
522
|
};
|
|
@@ -511,7 +539,9 @@ export const linksFromEvalResults = (
|
|
|
511
539
|
label: prefix,
|
|
512
540
|
links: results.map((e) => ({
|
|
513
541
|
label: `eval: ${e.evaluatorName} (${e.passed ? "passed" : "failed"})`,
|
|
514
|
-
|
|
542
|
+
// The Evaluations tab resolves this id to an eval case, falling back
|
|
543
|
+
// to the case saved from the same action.
|
|
544
|
+
href: dashboardRouteToHref({ tab: "evaluations", evaluationId: e.id }),
|
|
515
545
|
entityKind: "eval",
|
|
516
546
|
entityId: e.id,
|
|
517
547
|
...metadataForAction?.(e.actionId),
|
|
@@ -534,7 +564,80 @@ export const linksFromArtifacts = (
|
|
|
534
564
|
label: prefix,
|
|
535
565
|
links: arts.map((a) => ({
|
|
536
566
|
label: `artifact: ${a.artifactName} (${a.artifactType})`,
|
|
537
|
-
|
|
567
|
+
// No dedicated artifact tab: the owning action's detail page lists it.
|
|
568
|
+
href: dashboardRouteToHref({ tab: "actions", actionId: a.actionId }),
|
|
538
569
|
})),
|
|
539
570
|
};
|
|
540
571
|
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Dashboard destination for an evidence entity, delegating to the shared
|
|
575
|
+
* `@obsunified/types` route contract. Unknown kinds fall back to the
|
|
576
|
+
* evidence explorer so a link never lands on the Health tab.
|
|
577
|
+
*/
|
|
578
|
+
export const hrefForEntity = (
|
|
579
|
+
kind: string,
|
|
580
|
+
id: string,
|
|
581
|
+
options?: { traceId?: string; spanId?: string },
|
|
582
|
+
): string => sharedHrefForEntity(kind, id, options);
|
|
583
|
+
|
|
584
|
+
export interface FaultLocusTarget {
|
|
585
|
+
entityKind: "service" | "span";
|
|
586
|
+
entityId: string;
|
|
587
|
+
href: string;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const HEX_SPAN_PAIR = /^[0-9a-f]{32}:[0-9a-f]{16}$/i;
|
|
591
|
+
const HEX_SPAN_ID = /^[0-9a-f]{16}$/i;
|
|
592
|
+
const NODE_KINDS = new Set([
|
|
593
|
+
"service",
|
|
594
|
+
"endpoint",
|
|
595
|
+
"database",
|
|
596
|
+
"queue",
|
|
597
|
+
"messaging_topic",
|
|
598
|
+
"topic",
|
|
599
|
+
"cache",
|
|
600
|
+
"external",
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* A finding's `faultLocusEntityId` is a canonical ontology node id
|
|
605
|
+
* (`service:<env>:<ns>:<name>`, `endpoint:<env>:<ns>:<svc>:...`), a
|
|
606
|
+
* `traceId:spanId` pair, a bare span id, or a bare service name. Resolve it to
|
|
607
|
+
* the entity kind it really is and the surface that shows that kind.
|
|
608
|
+
*/
|
|
609
|
+
export const resolveFaultLocusTarget = (
|
|
610
|
+
faultLocusEntityId: string,
|
|
611
|
+
): FaultLocusTarget => {
|
|
612
|
+
const id = faultLocusEntityId;
|
|
613
|
+
if (HEX_SPAN_PAIR.test(id)) {
|
|
614
|
+
return {
|
|
615
|
+
entityKind: "span",
|
|
616
|
+
entityId: id,
|
|
617
|
+
href: hrefForEntity("span", id),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (HEX_SPAN_ID.test(id)) {
|
|
621
|
+
return {
|
|
622
|
+
entityKind: "span",
|
|
623
|
+
entityId: id,
|
|
624
|
+
href: hrefForEntity("span", id),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const segments = id.split(":");
|
|
628
|
+
if (segments.length > 1 && NODE_KINDS.has(segments[0] ?? "")) {
|
|
629
|
+
// service:<env>:<ns>:<name> and endpoint:<env>:<ns>:<svc>:... both carry
|
|
630
|
+
// the owning service name at index 3.
|
|
631
|
+
const name = segments[3] || segments[segments.length - 1] || id;
|
|
632
|
+
return {
|
|
633
|
+
entityKind: "service",
|
|
634
|
+
entityId: id,
|
|
635
|
+
href: hrefForEntity("service", name),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
entityKind: "service",
|
|
640
|
+
entityId: id,
|
|
641
|
+
href: hrefForEntity("service", id),
|
|
642
|
+
};
|
|
643
|
+
};
|
|
@@ -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
|
+
});
|