@telorun/debug-ui 0.3.0 → 0.4.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/app-dist/assets/{index-DS4SK8dD.js → index-BMTuPr57.js} +22 -22
- package/app-dist/index.html +1 -1
- package/app-single/index.html +22 -22
- package/package.json +2 -2
- package/src/components/EventGraph.tsx +8 -2
- package/src/filter.ts +6 -2
- package/src/graph.test.ts +66 -38
- package/src/graph.ts +98 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/debug-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Browser-safe debug event UI for Telo: wire types, filtering, SSE client, and React components for watching a running app's event stream.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@dagrejs/dagre": "^3.0.0",
|
|
47
47
|
"@xyflow/react": "^12.10.2",
|
|
48
48
|
"lucide-react": "^1.8.0",
|
|
49
|
-
"@telorun/debug-wire": "0.
|
|
49
|
+
"@telorun/debug-wire": "0.2.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": ">=18",
|
|
@@ -114,12 +114,12 @@ function TraceFlowNode({ data }: NodeProps<Node<TraceNodeData>>) {
|
|
|
114
114
|
<Handle type="target" position={Position.Left} className="tdbg-node-handle" />
|
|
115
115
|
<div className="tdbg-node-head">
|
|
116
116
|
{node.isRoot && <span className="tdbg-node-roottag">root</span>}
|
|
117
|
-
<span className="tdbg-node-name">{node.name}</span>
|
|
117
|
+
<span className="tdbg-node-name">{node.label ?? node.name}</span>
|
|
118
118
|
<span className={badgeClass(last.outcome)}>
|
|
119
119
|
{node.invocations.length > 1 ? `${node.invocations.length}×` : last.suffix}
|
|
120
120
|
</span>
|
|
121
121
|
</div>
|
|
122
|
-
<div className="tdbg-node-kind">{node.kind}</div>
|
|
122
|
+
<div className="tdbg-node-kind">{node.label ? `${node.kind} · ${node.name}` : node.kind}</div>
|
|
123
123
|
<Handle type="source" position={Position.Right} className="tdbg-node-handle" />
|
|
124
124
|
</div>
|
|
125
125
|
);
|
|
@@ -420,6 +420,12 @@ function TraceNodeDetail({
|
|
|
420
420
|
<span className="tdbg-muted">{inv.timestamp.slice(11, 23)}</span>
|
|
421
421
|
</div>
|
|
422
422
|
<InOut inv={inv} resolveUrl={resolveUrl} />
|
|
423
|
+
{inv.context !== undefined && (
|
|
424
|
+
<>
|
|
425
|
+
<div className="tdbg-graph-detail-label">Available context</div>
|
|
426
|
+
<PayloadInspector value={inv.context} resolveUrl={resolveUrl} />
|
|
427
|
+
</>
|
|
428
|
+
)}
|
|
423
429
|
</div>
|
|
424
430
|
))}
|
|
425
431
|
</div>
|
package/src/filter.ts
CHANGED
|
@@ -23,8 +23,12 @@ export function matchesFilter(event: DebugEvent, filter: EventFilter): boolean {
|
|
|
23
23
|
if (suffixes.length > 0 && !suffixes.includes(eventSuffix(event.event))) {
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
|
-
if (filter.kind
|
|
27
|
-
|
|
26
|
+
if (filter.kind) {
|
|
27
|
+
// Lifecycle events carry the kind in the dotted name; dispatch (trace) events
|
|
28
|
+
// carry it in `payload.ref.kind` (the name dropped the kind prefix). Match either.
|
|
29
|
+
const refKind = (event.payload as { ref?: { kind?: unknown } } | undefined)?.ref?.kind;
|
|
30
|
+
const haystack = `${event.event} ${typeof refKind === "string" ? refKind : ""}`.toLowerCase();
|
|
31
|
+
if (!haystack.includes(filter.kind.toLowerCase())) return false;
|
|
28
32
|
}
|
|
29
33
|
if (filter.text) {
|
|
30
34
|
const needle = filter.text.toLowerCase();
|
package/src/graph.test.ts
CHANGED
|
@@ -12,6 +12,19 @@ const ev = (event: string, payload?: unknown): DebugFrame => ({
|
|
|
12
12
|
const created = (kind: string, name: string, deps: { kind: string; name: string }[] = []) =>
|
|
13
13
|
ev(`${kind}.${name}.Created`, { resource: { kind, name }, dependencies: deps });
|
|
14
14
|
|
|
15
|
+
type Outcome = "ok" | "failed" | "rejected" | "cancelled";
|
|
16
|
+
|
|
17
|
+
/** A terminal dispatch (trace) event: the name drops the kind, the payload carries
|
|
18
|
+
* the structured trace contract `{ capability, phase, outcome, ref, … }`. */
|
|
19
|
+
const disp = (
|
|
20
|
+
name: string,
|
|
21
|
+
kind: string,
|
|
22
|
+
outcome: Outcome,
|
|
23
|
+
suffix: string,
|
|
24
|
+
detail: Record<string, unknown> = {},
|
|
25
|
+
): DebugFrame =>
|
|
26
|
+
ev(`${name}.${suffix}`, { capability: "invoke", phase: "end", outcome, ref: { kind, name }, ...detail });
|
|
27
|
+
|
|
15
28
|
describe("deriveGraph", () => {
|
|
16
29
|
it("creates a gray (created) node on Created", () => {
|
|
17
30
|
const { nodes } = deriveGraph([created("Http.Server", "api")]);
|
|
@@ -52,7 +65,7 @@ describe("deriveGraph", () => {
|
|
|
52
65
|
it("records an ok invocation with inputs and outputs", () => {
|
|
53
66
|
const { nodes } = deriveGraph([
|
|
54
67
|
created("Js.Script", "echo"),
|
|
55
|
-
|
|
68
|
+
disp("echo", "Js.Script", "ok", "Invoked", { inputs: { msg: "hi" }, outputs: { echoed: "hi" } }),
|
|
56
69
|
]);
|
|
57
70
|
expect(nodes[0].invokeCount).toBe(1);
|
|
58
71
|
expect(nodes[0].lastInvoke).toMatchObject({
|
|
@@ -63,18 +76,27 @@ describe("deriveGraph", () => {
|
|
|
63
76
|
});
|
|
64
77
|
});
|
|
65
78
|
|
|
66
|
-
it("
|
|
79
|
+
it("skips a phase:start event (no outcome) — only terminal events count", () => {
|
|
80
|
+
const { nodes } = deriveGraph([
|
|
81
|
+
created("Js.Script", "echo"),
|
|
82
|
+
ev("echo.Invoking", { capability: "invoke", phase: "start", ref: { kind: "Js.Script", name: "echo" }, inputs: {} }),
|
|
83
|
+
]);
|
|
84
|
+
expect(nodes[0].invokeCount).toBe(0);
|
|
85
|
+
expect(nodes[0].lastInvoke).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("records failure, rejection and cancellation outcomes from the payload", () => {
|
|
67
89
|
const base = created("Js.Script", "x");
|
|
68
90
|
expect(
|
|
69
|
-
deriveGraph([base,
|
|
91
|
+
deriveGraph([base, disp("x", "Js.Script", "failed", "InvokeFailed", { inputs: {}, name: "Error", message: "boom" })])
|
|
70
92
|
.nodes[0].lastInvoke?.outcome,
|
|
71
93
|
).toBe("failed");
|
|
72
94
|
expect(
|
|
73
|
-
deriveGraph([base,
|
|
95
|
+
deriveGraph([base, disp("x", "Js.Script", "rejected", "InvokeRejected", { inputs: {}, code: "E", message: "no" })])
|
|
74
96
|
.nodes[0].lastInvoke?.outcome,
|
|
75
97
|
).toBe("rejected");
|
|
76
98
|
expect(
|
|
77
|
-
deriveGraph([base,
|
|
99
|
+
deriveGraph([base, disp("x", "Js.Script", "cancelled", "InvokeCancelled", { inputs: {}, reason: "stop" })])
|
|
78
100
|
.nodes[0].lastInvoke?.outcome,
|
|
79
101
|
).toBe("cancelled");
|
|
80
102
|
});
|
|
@@ -82,29 +104,27 @@ describe("deriveGraph", () => {
|
|
|
82
104
|
it("treats InvokeRejected.Undeclared as a rejection on the right node", () => {
|
|
83
105
|
const { nodes } = deriveGraph([
|
|
84
106
|
created("Js.Script", "x"),
|
|
85
|
-
|
|
107
|
+
disp("x", "Js.Script", "rejected", "InvokeRejected.Undeclared", { inputs: {}, code: "E", message: "no" }),
|
|
86
108
|
]);
|
|
87
|
-
expect(nodes[0].lastInvoke).toMatchObject({ outcome: "rejected", suffix: "
|
|
109
|
+
expect(nodes[0].lastInvoke).toMatchObject({ outcome: "rejected", suffix: "Undeclared" });
|
|
88
110
|
});
|
|
89
111
|
|
|
90
112
|
it("counts repeated invocations and advances the activity index", () => {
|
|
91
113
|
const { nodes } = deriveGraph([
|
|
92
114
|
created("Js.Script", "x"),
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
disp("x", "Js.Script", "ok", "Invoked", { inputs: {}, outputs: 1 }),
|
|
116
|
+
disp("x", "Js.Script", "ok", "Invoked", { inputs: {}, outputs: 2 }),
|
|
95
117
|
]);
|
|
96
118
|
expect(nodes[0].invokeCount).toBe(2);
|
|
97
119
|
expect(nodes[0].lastInvoke?.outputs).toBe(2);
|
|
98
120
|
expect(nodes[0].lastActivitySeq).toBeGreaterThan(0);
|
|
99
121
|
});
|
|
100
122
|
|
|
101
|
-
it("attributes an invocation to the
|
|
102
|
-
// `Http.Server.api` and `Http.Server.api.sub` share a prefix; the event must
|
|
103
|
-
// land on the deeper node, not the shorter one.
|
|
123
|
+
it("attributes an invocation to the resource named in the payload ref", () => {
|
|
104
124
|
const { nodes } = deriveGraph([
|
|
105
125
|
created("Http.Server", "api"),
|
|
106
126
|
created("Http.Server.api", "sub"),
|
|
107
|
-
|
|
127
|
+
disp("sub", "Http.Server.api", "ok", "Invoked", { inputs: {}, outputs: "ok" }),
|
|
108
128
|
]);
|
|
109
129
|
expect(nodes.find((n) => n.name === "sub")?.invokeCount).toBe(1);
|
|
110
130
|
expect(nodes.find((n) => n.name === "api")?.invokeCount).toBe(0);
|
|
@@ -121,46 +141,54 @@ describe("deriveGraph", () => {
|
|
|
121
141
|
});
|
|
122
142
|
});
|
|
123
143
|
|
|
144
|
+
/** A terminal dispatch event carrying trace ids — `spanId` / `parentSpanId`. */
|
|
124
145
|
const inv = (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
name: string,
|
|
147
|
+
kind: string,
|
|
148
|
+
spanId: number,
|
|
149
|
+
parentSpanId: number | undefined,
|
|
150
|
+
outcome: Outcome,
|
|
151
|
+
suffix: string,
|
|
152
|
+
detail: Record<string, unknown> = {},
|
|
153
|
+
): DebugFrame =>
|
|
154
|
+
ev(`${name}.${suffix}`, {
|
|
155
|
+
spanId,
|
|
156
|
+
parentSpanId,
|
|
157
|
+
capability: "invoke",
|
|
158
|
+
phase: "end",
|
|
159
|
+
outcome,
|
|
160
|
+
ref: { kind, name },
|
|
161
|
+
...detail,
|
|
162
|
+
});
|
|
135
163
|
|
|
136
164
|
describe("deriveInvocations", () => {
|
|
137
|
-
it("is empty when events carry no
|
|
138
|
-
const t = deriveInvocations([
|
|
165
|
+
it("is empty when events carry no span ids (tracing off)", () => {
|
|
166
|
+
const t = deriveInvocations([disp("x", "Js.Script", "ok", "Invoked", { outputs: 1 })]);
|
|
139
167
|
expect(t.roots).toHaveLength(0);
|
|
140
168
|
expect(t.byId.size).toBe(0);
|
|
141
169
|
});
|
|
142
170
|
|
|
143
171
|
it("collects a root invocation with its resource, inputs and outputs", () => {
|
|
144
|
-
const t = deriveInvocations([inv("Http.Server
|
|
172
|
+
const t = deriveInvocations([inv("api", "Http.Server", 1, undefined, "ok", "Invoked", { inputs: { a: 1 }, outputs: "ok" })]);
|
|
145
173
|
expect(t.roots).toHaveLength(1);
|
|
146
174
|
expect(t.roots[0]).toMatchObject({ id: 1, kind: "Http.Server", name: "api", outcome: "ok", inputs: { a: 1 }, outputs: "ok" });
|
|
147
175
|
});
|
|
148
176
|
|
|
149
177
|
it("links children to parents and keeps only roots at the top", () => {
|
|
150
178
|
const t = deriveInvocations([
|
|
151
|
-
inv("
|
|
152
|
-
inv("
|
|
153
|
-
inv("
|
|
179
|
+
inv("root", "A", 1, undefined, "ok", "Invoked", { outputs: 1 }),
|
|
180
|
+
inv("child", "B", 2, 1, "ok", "Invoked", { outputs: 2 }),
|
|
181
|
+
inv("grand", "C", 3, 2, "ok", "Invoked", { outputs: 3 }),
|
|
154
182
|
]);
|
|
155
183
|
expect(t.roots.map((r) => r.id)).toEqual([1]);
|
|
156
184
|
expect(t.childrenOf.get(1)).toEqual([2]);
|
|
157
185
|
expect(t.childrenOf.get(2)).toEqual([3]);
|
|
158
186
|
});
|
|
159
187
|
|
|
160
|
-
it("dedupes the InvokeRejected.Undeclared echo by id", () => {
|
|
188
|
+
it("dedupes the InvokeRejected.Undeclared echo by id, keeping the primary", () => {
|
|
161
189
|
const t = deriveInvocations([
|
|
162
|
-
inv("
|
|
163
|
-
inv("
|
|
190
|
+
inv("x", "A", 1, undefined, "rejected", "InvokeRejected", { code: "E" }),
|
|
191
|
+
inv("x", "A", 1, undefined, "rejected", "InvokeRejected.Undeclared", { code: "E" }),
|
|
164
192
|
]);
|
|
165
193
|
expect(t.byId.size).toBe(1);
|
|
166
194
|
expect(t.byId.get(1)).toMatchObject({ outcome: "rejected", suffix: "InvokeRejected" });
|
|
@@ -170,9 +198,9 @@ describe("deriveInvocations", () => {
|
|
|
170
198
|
describe("traceSubgraph", () => {
|
|
171
199
|
it("returns only the participating resources wired by call edges", () => {
|
|
172
200
|
const t = deriveInvocations([
|
|
173
|
-
inv("
|
|
174
|
-
inv("
|
|
175
|
-
inv("
|
|
201
|
+
inv("root", "A", 1, undefined, "ok", "Invoked", { outputs: 1 }),
|
|
202
|
+
inv("child", "B", 2, 1, "ok", "Invoked", { outputs: 2 }),
|
|
203
|
+
inv("other", "C", 9, undefined, "ok", "Invoked", { outputs: 9 }), // a different trace
|
|
176
204
|
]);
|
|
177
205
|
const g = traceSubgraph(t, 1);
|
|
178
206
|
expect(g.nodes.map((n) => n.id).sort()).toEqual(["A.root", "B.child"]);
|
|
@@ -182,9 +210,9 @@ describe("traceSubgraph", () => {
|
|
|
182
210
|
|
|
183
211
|
it("collapses repeated calls to one node holding every invocation", () => {
|
|
184
212
|
const t = deriveInvocations([
|
|
185
|
-
inv("
|
|
186
|
-
inv("
|
|
187
|
-
inv("
|
|
213
|
+
inv("root", "A", 1, undefined, "ok", "Invoked", { outputs: 1 }),
|
|
214
|
+
inv("svc", "B", 2, 1, "ok", "Invoked", { outputs: 2 }),
|
|
215
|
+
inv("svc", "B", 3, 1, "ok", "Invoked", { outputs: 3 }),
|
|
188
216
|
]);
|
|
189
217
|
const g = traceSubgraph(t, 1);
|
|
190
218
|
const svc = g.nodes.find((n) => n.id === "B.svc");
|
package/src/graph.ts
CHANGED
|
@@ -7,18 +7,19 @@ import { type DebugEvent, type DebugFrame, eventSuffix, isLogFrame } from "./wir
|
|
|
7
7
|
* tracks each node's lifecycle status and most-recent invocation. The component
|
|
8
8
|
* layer adds positions (dagre) and rendering.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Two event families drive it:
|
|
11
|
+
* - Lifecycle events `<Kind>.<Name>.{Created,Initialized,Teardown}` — a node
|
|
12
|
+
* appears (its payload carries the resolved `{kind,name}` and `dependencies`),
|
|
13
|
+
* then flips to `initialized` / `torndown`.
|
|
14
|
+
* - Dispatch (trace) events — every capability call emits a structured payload
|
|
15
|
+
* `{ spanId, parentSpanId, capability, phase, outcome, ref:{kind,name}, … }`.
|
|
16
|
+
* The consumer reads the *payload*, never the dotted name: `ref` locates the
|
|
17
|
+
* node, `outcome` records the result. Terminal events carry an `outcome`;
|
|
18
|
+
* `phase:"start"` events carry none and are skipped here.
|
|
18
19
|
*
|
|
19
|
-
* When the producer has tracing on,
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
20
|
+
* When the producer has tracing on, dispatch events also carry `spanId` /
|
|
21
|
+
* `parentSpanId`; {@link deriveInvocations} folds those into the call tree behind
|
|
22
|
+
* the invocation list + scoped trace graph.
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
export type NodeStatus = "created" | "initialized" | "torndown";
|
|
@@ -28,7 +29,7 @@ export type InvokeOutcome = "ok" | "failed" | "rejected" | "cancelled";
|
|
|
28
29
|
/** The most recent invocation observed for a node. */
|
|
29
30
|
export interface InvokeRecord {
|
|
30
31
|
outcome: InvokeOutcome;
|
|
31
|
-
/** The
|
|
32
|
+
/** The terminal event's suffix (`Invoked`, `InvokeFailed`, …) — for display only. */
|
|
32
33
|
suffix: string;
|
|
33
34
|
timestamp: string;
|
|
34
35
|
/** The dispatched inputs (present on every invocation event the kernel emits). */
|
|
@@ -85,14 +86,34 @@ function asResource(payload: unknown): WireResourceRef | undefined {
|
|
|
85
86
|
return undefined;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
89
|
+
/** The resolved `{kind,name}` a dispatch event's payload carries under `ref`. */
|
|
90
|
+
function readRef(payload: Record<string, unknown> | undefined): { kind: string; name: string } | undefined {
|
|
91
|
+
const ref = payload?.ref;
|
|
92
|
+
if (ref && typeof ref === "object") {
|
|
93
|
+
const { kind, name } = ref as Record<string, unknown>;
|
|
94
|
+
if (typeof kind === "string" && typeof name === "string") return { kind, name };
|
|
95
|
+
}
|
|
93
96
|
return undefined;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/** Narrow a payload `outcome` to a known {@link InvokeOutcome}. Absent on
|
|
100
|
+
* `phase:"start"` events, so they are naturally skipped by callers. */
|
|
101
|
+
function asOutcome(value: unknown): InvokeOutcome | undefined {
|
|
102
|
+
return value === "ok" || value === "failed" || value === "rejected" || value === "cancelled"
|
|
103
|
+
? value
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function num(value: unknown): number | undefined {
|
|
108
|
+
return typeof value === "number" ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function asCapability(value: unknown): SpanCapability | undefined {
|
|
112
|
+
return value === "invoke" || value === "run" || value === "provide" || value === "request"
|
|
113
|
+
? value
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
/**
|
|
97
118
|
* Fold a frame buffer into the resource graph. Logs are ignored; events are
|
|
98
119
|
* applied in arrival order so the returned status / `lastInvoke` reflect the
|
|
@@ -145,19 +166,19 @@ export function deriveGraph(frames: readonly DebugFrame[]): GraphState {
|
|
|
145
166
|
return;
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
//
|
|
149
|
-
// are name-keyed
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const outcome =
|
|
154
|
-
|
|
169
|
+
// Dispatch (trace) events: the payload's `ref.name` resolves the target node
|
|
170
|
+
// directly (nodes are name-keyed). `phase:"start"` events carry no `outcome`
|
|
171
|
+
// and are skipped — only terminal events record a result.
|
|
172
|
+
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
173
|
+
const ref = readRef(p);
|
|
174
|
+
const outcome = asOutcome(p?.outcome);
|
|
175
|
+
const node = ref ? nodes.get(ref.name) : undefined;
|
|
176
|
+
if (!node || !outcome) return;
|
|
155
177
|
node.invokeCount += 1;
|
|
156
178
|
node.lastActivitySeq = seq;
|
|
157
|
-
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
158
179
|
node.lastInvoke = {
|
|
159
180
|
outcome,
|
|
160
|
-
suffix:
|
|
181
|
+
suffix: eventSuffix(event.event),
|
|
161
182
|
timestamp: event.timestamp,
|
|
162
183
|
inputs: p && "inputs" in p ? p.inputs : undefined,
|
|
163
184
|
outputs: p && "outputs" in p ? p.outputs : undefined,
|
|
@@ -170,15 +191,28 @@ export function deriveGraph(frames: readonly DebugFrame[]): GraphState {
|
|
|
170
191
|
|
|
171
192
|
// ── Invocation traces ──────────────────────────────────────────────────────
|
|
172
193
|
|
|
173
|
-
/**
|
|
194
|
+
/** Which capability the span represents. `"request"` is an inbound-boundary span. */
|
|
195
|
+
export type SpanCapability = "invoke" | "run" | "provide" | "request";
|
|
196
|
+
|
|
197
|
+
/** One invocation — a single dispatched call, identified by its `spanId`. */
|
|
174
198
|
export interface Invocation {
|
|
175
199
|
id: number;
|
|
176
200
|
parentId?: number;
|
|
201
|
+
/** Trace this span belongs to — present while tracing; groups the call tree. */
|
|
202
|
+
traceId?: string;
|
|
177
203
|
kind: string;
|
|
178
204
|
name: string;
|
|
205
|
+
capability?: SpanCapability;
|
|
179
206
|
outcome: InvokeOutcome;
|
|
180
|
-
/** The event suffix (`Invoked`, `InvokeFailed`, …). */
|
|
207
|
+
/** The terminal event's suffix (`Invoked`, `InvokeFailed`, …) — for display only. */
|
|
181
208
|
suffix: string;
|
|
209
|
+
/** Human label (e.g. a route `"POST /feedback"`), for `request` spans. */
|
|
210
|
+
label?: string;
|
|
211
|
+
/** Structured span attributes (e.g. `{ method, path }`). */
|
|
212
|
+
attributes?: Record<string, unknown>;
|
|
213
|
+
/** On a trace root: the redacted CEL root scope (variables / secrets / resources
|
|
214
|
+
* / ports) the trace could reference. */
|
|
215
|
+
context?: Record<string, unknown>;
|
|
182
216
|
timestamp: string;
|
|
183
217
|
inputs?: unknown;
|
|
184
218
|
outputs?: unknown;
|
|
@@ -195,39 +229,12 @@ export interface TraceState {
|
|
|
195
229
|
roots: Invocation[];
|
|
196
230
|
}
|
|
197
231
|
|
|
198
|
-
/** Known invocation suffixes, longest first so `InvokeRejected.Undeclared` wins
|
|
199
|
-
* over `InvokeRejected`. */
|
|
200
|
-
const INVOKE_SUFFIXES = [
|
|
201
|
-
"InvokeRejected.Undeclared",
|
|
202
|
-
"InvokeCancelled",
|
|
203
|
-
"InvokeRejected",
|
|
204
|
-
"InvokeFailed",
|
|
205
|
-
"Invoked",
|
|
206
|
-
"RunCancelled",
|
|
207
|
-
] as const;
|
|
208
|
-
|
|
209
|
-
/** Split a dotted invocation event into `{ kind, name, suffix }` by stripping a
|
|
210
|
-
* known trailing suffix — self-contained, no node registry needed. */
|
|
211
|
-
function parseInvocationEvent(event: string): { kind: string; name: string; suffix: string } | undefined {
|
|
212
|
-
for (const suffix of INVOKE_SUFFIXES) {
|
|
213
|
-
if (event.endsWith(`.${suffix}`)) {
|
|
214
|
-
const head = event.slice(0, event.length - suffix.length - 1);
|
|
215
|
-
const dot = head.lastIndexOf(".");
|
|
216
|
-
if (dot <= 0) return undefined;
|
|
217
|
-
return { kind: head.slice(0, dot), name: head.slice(dot + 1), suffix };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function metaId(value: unknown): number | undefined {
|
|
224
|
-
return typeof value === "number" ? value : undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
232
|
/**
|
|
228
|
-
* Fold the
|
|
229
|
-
*
|
|
230
|
-
*
|
|
233
|
+
* Fold the dispatch events that carry a `spanId` into a call tree. Each
|
|
234
|
+
* invocation appears once — keyed by `spanId`, the first terminal event wins, so
|
|
235
|
+
* the `InvokeRejected.Undeclared` echo (same id, emitted after `InvokeRejected`)
|
|
236
|
+
* is deduped. `phase:"start"` events carry no `outcome` and are skipped. Empty
|
|
237
|
+
* when the producer isn't tracing (no `spanId`).
|
|
231
238
|
*/
|
|
232
239
|
export function deriveInvocations(frames: readonly DebugFrame[]): TraceState {
|
|
233
240
|
const byId = new Map<number, Invocation>();
|
|
@@ -237,22 +244,33 @@ export function deriveInvocations(frames: readonly DebugFrame[]): TraceState {
|
|
|
237
244
|
for (const frame of frames) {
|
|
238
245
|
if (isLogFrame(frame)) continue;
|
|
239
246
|
const event = frame as DebugEvent;
|
|
240
|
-
const
|
|
247
|
+
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
248
|
+
const id = num(p?.spanId);
|
|
241
249
|
if (id === undefined || byId.has(id)) continue;
|
|
242
|
-
const
|
|
243
|
-
if (!parsed) continue;
|
|
244
|
-
const outcome = classify(parsed.suffix);
|
|
250
|
+
const outcome = asOutcome(p?.outcome);
|
|
245
251
|
if (!outcome) continue;
|
|
252
|
+
const ref = readRef(p);
|
|
253
|
+
if (!ref) continue;
|
|
246
254
|
|
|
247
|
-
const parentId =
|
|
248
|
-
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
255
|
+
const parentId = num(p?.parentSpanId);
|
|
249
256
|
const invocation: Invocation = {
|
|
250
257
|
id,
|
|
251
258
|
parentId,
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
traceId: typeof p?.traceId === "string" ? p.traceId : undefined,
|
|
260
|
+
kind: ref.kind,
|
|
261
|
+
name: ref.name,
|
|
262
|
+
capability: asCapability(p?.capability),
|
|
254
263
|
outcome,
|
|
255
|
-
suffix:
|
|
264
|
+
suffix: eventSuffix(event.event),
|
|
265
|
+
label: typeof p?.label === "string" ? p.label : undefined,
|
|
266
|
+
attributes:
|
|
267
|
+
p?.attributes && typeof p.attributes === "object"
|
|
268
|
+
? (p.attributes as Record<string, unknown>)
|
|
269
|
+
: undefined,
|
|
270
|
+
context:
|
|
271
|
+
p?.context && typeof p.context === "object"
|
|
272
|
+
? (p.context as Record<string, unknown>)
|
|
273
|
+
: undefined,
|
|
256
274
|
timestamp: event.timestamp,
|
|
257
275
|
inputs: p && "inputs" in p ? p.inputs : undefined,
|
|
258
276
|
outputs: p && "outputs" in p ? p.outputs : undefined,
|
|
@@ -278,6 +296,8 @@ export interface TraceNode {
|
|
|
278
296
|
id: string;
|
|
279
297
|
kind: string;
|
|
280
298
|
name: string;
|
|
299
|
+
/** Human label from the span (e.g. a route `"POST /feedback"`), if any. */
|
|
300
|
+
label?: string;
|
|
281
301
|
isRoot: boolean;
|
|
282
302
|
invocations: Invocation[];
|
|
283
303
|
}
|
|
@@ -313,7 +333,15 @@ export function traceSubgraph(trace: TraceState, rootId: number): TraceSubgraph
|
|
|
313
333
|
const key = resourceKey(inv);
|
|
314
334
|
const node = nodes.get(key);
|
|
315
335
|
if (node) node.invocations.push(inv);
|
|
316
|
-
else
|
|
336
|
+
else
|
|
337
|
+
nodes.set(key, {
|
|
338
|
+
id: key,
|
|
339
|
+
kind: inv.kind,
|
|
340
|
+
name: inv.name,
|
|
341
|
+
label: inv.label,
|
|
342
|
+
isRoot: id === rootId,
|
|
343
|
+
invocations: [inv],
|
|
344
|
+
});
|
|
317
345
|
|
|
318
346
|
for (const childId of trace.childrenOf.get(id) ?? []) {
|
|
319
347
|
const child = trace.byId.get(childId);
|