Skip to content
File

Blob: src/cloudflare/internal/test/tracing/tracing-hierarchy-instrumentation-test.js

javascript149 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import assert from 'node:assert';
6import {
7 createHierarchyAwareCollector,
8 findSpanByName,
9} from 'instrumentation-test-helper';
10 
11const collector = createHierarchyAwareCollector();
12export default { tailStream: collector.tailStream };
13 
14// After all test cases above have run (triggering tailStream invocations per-request),
15// validate the parent/child relationships encoded in the streaming-tail events.
16export const validateHierarchy = {
17 async test() {
18 await collector.waitForCompletion();
19 const { state } = collector;
20 
21 // Assert an edge in the span tree: `child` is a direct child of `parent`.
22 const assertParent = (child, parent, label) => {
23 assert.strictEqual(
24 child.parentSpanId,
25 parent.spanId,
26 `${label}: expected span "${child.name}" (id=${child.spanId}) to have ` +
27 `parent "${parent.name}" (id=${parent.spanId}), but got parentSpanId=${child.parentSpanId}`
28 );
29 assert.strictEqual(
30 child.invocationId,
31 parent.invocationId,
32 `${label}: child and parent should share invocationId`
33 );
34 };
35 
36 // Assert `span` is a direct child of the top-level (onset) span for its invocation.
37 const assertTopLevelParent = (span, label) => {
38 const topLevelSpanId = state.topLevelSpans.get(span.invocationId);
39 assert.ok(
40 topLevelSpanId,
41 `${label}: missing topLevelSpanId for invocation ${span.invocationId}`
42 );
43 assert.strictEqual(
44 span.parentSpanId,
45 topLevelSpanId,
46 `${label}: expected span "${span.name}" to be a direct child of the onset ` +
47 `span (id=${topLevelSpanId}), but parentSpanId=${span.parentSpanId}`
48 );
49 };
50 
51 // ---------- Case 1: nestedEnterSpan (sync) ----------
52 {
53 const outer = findSpanByName(state, 'hierarchy-outer');
54 const inner = findSpanByName(state, 'hierarchy-inner');
55 assert.strictEqual(outer.role, 'outer');
56 assert.strictEqual(inner.role, 'inner');
57 assert.ok(outer.closed, 'outer sync span should be closed');
58 assert.ok(inner.closed, 'inner sync span should be closed');
59 assertParent(inner, outer, 'nestedEnterSpan');
60 assertTopLevelParent(outer, 'nestedEnterSpan');
61 }
62 
63 // ---------- Case 2: nestedEnterSpanAsync ----------
64 // The important assertion here: the inner span is created AFTER an `await`, so its
65 // parent is only correctly reported if the AsyncContextFrame push propagates through
66 // the microtask boundary.
67 {
68 const outer = findSpanByName(state, 'hierarchy-async-outer');
69 const inner = findSpanByName(state, 'hierarchy-async-inner');
70 assert.strictEqual(outer.role, 'outer');
71 assert.strictEqual(inner.role, 'inner');
72 assert.ok(outer.closed);
73 assert.ok(inner.closed);
74 assertParent(inner, outer, 'nestedEnterSpanAsync');
75 assertTopLevelParent(outer, 'nestedEnterSpanAsync');
76 }
77 
78 // ---------- Case 3: fetchInsideEnterSpan ----------
79 // The runtime-created "fetch" span must be nested under our enterSpan. This test
80 // is the key proof that enterSpan's AsyncContextFrame push is observed by lower-level
81 // runtime tracing paths (IoContext::makeUserTraceSpan -> getCurrentUserTraceSpan).
82 {
83 const outer = findSpanByName(state, 'hierarchy-fetch-outer');
84 assert.strictEqual(outer.case, 'fetchInsideEnterSpan');
85 assert.ok(outer.closed);
86 // Find the fetch span that belongs to this invocation (there are other fetch
87 // spans in other test invocations).
88 const fetchSpan = findSpanByName(
89 state,
90 'fetch',
91 (s) => s.invocationId === outer.invocationId
92 );
93 assertParent(fetchSpan, outer, 'fetchInsideEnterSpan');
94 assertTopLevelParent(outer, 'fetchInsideEnterSpan');
95 }
96 
97 // ---------- Case 4: fetchInsideNestedEnterSpan ----------
98 // Three-level chain: onset -> outer -> inner -> fetch.
99 {
100 const outer = findSpanByName(state, 'hierarchy-deep-outer');
101 const inner = findSpanByName(state, 'hierarchy-deep-inner');
102 assert.strictEqual(outer.role, 'outer');
103 assert.strictEqual(inner.role, 'inner');
104 assert.ok(outer.closed);
105 assert.ok(inner.closed);
106 const fetchSpan = findSpanByName(
107 state,
108 'fetch',
109 (s) => s.invocationId === outer.invocationId
110 );
111 assertParent(inner, outer, 'fetchInsideNestedEnterSpan (inner->outer)');
112 assertParent(
113 fetchSpan,
114 inner,
115 'fetchInsideNestedEnterSpan (fetch->inner)'
116 );
117 assertTopLevelParent(outer, 'fetchInsideNestedEnterSpan');
118 }
119 
120 // ---------- Case 5: siblingEnterSpans ----------
121 // Sequential (non-nested) spans should each hang directly off the onset, not off
122 // each other - the first span's AsyncContextFrame push must be fully popped before
123 // the second span opens.
124 {
125 const a = findSpanByName(state, 'hierarchy-sibling-a');
126 const b = findSpanByName(state, 'hierarchy-sibling-b');
127 assert.strictEqual(a.role, 'sibling-a');
128 assert.strictEqual(b.role, 'sibling-b');
129 assert.ok(a.closed);
130 assert.ok(b.closed);
131 assert.strictEqual(
132 a.invocationId,
133 b.invocationId,
134 'siblingEnterSpans: both should share the same invocation'
135 );
136 assertTopLevelParent(a, 'siblingEnterSpans (a)');
137 assertTopLevelParent(b, 'siblingEnterSpans (b)');
138 }
139 
140 // ---------- Case 6: abandonedPromiseSpan ----------
141 // Reaching this point proves the outcome event for the abandoned-promise invocation
142 // was emitted in order: invocationPromises only resolve on "outcome", and
143 // waitForCompletion() awaits them all. BaseTracer::WeakRef prevents the abandoned
144 // SpanImpl from pinning the tracer past end-of-request.
145 
146 console.log('All tracing-hierarchy tests passed!');
147 },
148};