Skip to content
File

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

javascript116 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 
5// Tests that verify the tracing hierarchy produced by enterSpan is correct. Each test
6// case below runs a specific scenario (nested enterSpan, fetch inside enterSpan, etc.);
7// the tail worker in tracing-hierarchy-instrumentation-test.js receives the streaming
8// tail events and validates the parent/child relationships in a followup validation test.
9 
10import assert from 'node:assert';
11 
12export const nestedEnterSpan = {
13 async test(ctrl, env, ctx) {
14 const { withSpan } = env.tracingTest;
15 // Two nested synchronous enterSpan calls. The inner span's parent must be the outer
16 // span, and the outer span's parent must be the top-level request span (onset).
17 const result = withSpan('hierarchy-outer', (outer) => {
18 outer.setAttribute('case', 'nestedEnterSpan');
19 outer.setAttribute('role', 'outer');
20 return withSpan('hierarchy-inner', (inner) => {
21 inner.setAttribute('case', 'nestedEnterSpan');
22 inner.setAttribute('role', 'inner');
23 return 'ok';
24 });
25 });
26 assert.strictEqual(result, 'ok');
27 },
28};
29 
30export const nestedEnterSpanAsync = {
31 async test(ctrl, env, ctx) {
32 const { withSpan } = env.tracingTest;
33 // Async variant: the inner span is created after the outer span has awaited once,
34 // so the correct parent is only reported if the AsyncContextFrame push propagates
35 // through the microtask boundary.
36 const result = await withSpan('hierarchy-async-outer', async (outer) => {
37 outer.setAttribute('case', 'nestedEnterSpanAsync');
38 outer.setAttribute('role', 'outer');
39 await new Promise((r) => setTimeout(r, 5));
40 return await withSpan('hierarchy-async-inner', async (inner) => {
41 inner.setAttribute('case', 'nestedEnterSpanAsync');
42 inner.setAttribute('role', 'inner');
43 await new Promise((r) => setTimeout(r, 5));
44 return 'ok';
45 });
46 });
47 assert.strictEqual(result, 'ok');
48 },
49};
50 
51export const fetchInsideEnterSpan = {
52 async test(ctrl, env, ctx) {
53 const { withSpan } = env.tracingTest;
54 // fetch() automatically creates a "fetch" user-trace span via IoContext::
55 // makeUserTraceSpan, which reads getCurrentUserTraceSpan(). If our enterSpan
56 // correctly pushed onto the AsyncContextFrame, that fetch span's parent will be
57 // the outer span (not the top-level onset span).
58 await withSpan('hierarchy-fetch-outer', async (outer) => {
59 outer.setAttribute('case', 'fetchInsideEnterSpan');
60 const response = await env.fetchTarget.fetch('http://mock/echo');
61 assert.strictEqual(response.status, 200);
62 await response.text();
63 });
64 },
65};
66 
67export const fetchInsideNestedEnterSpan = {
68 async test(ctrl, env, ctx) {
69 const { withSpan } = env.tracingTest;
70 // Deeper nesting: fetch lives inside an inner enterSpan that is itself inside
71 // an outer enterSpan. The fetch's parent must be the inner span.
72 await withSpan('hierarchy-deep-outer', async (outer) => {
73 outer.setAttribute('case', 'fetchInsideNestedEnterSpan');
74 outer.setAttribute('role', 'outer');
75 await withSpan('hierarchy-deep-inner', async (inner) => {
76 inner.setAttribute('case', 'fetchInsideNestedEnterSpan');
77 inner.setAttribute('role', 'inner');
78 const response = await env.fetchTarget.fetch('http://mock/echo-deep');
79 assert.strictEqual(response.status, 200);
80 await response.text();
81 });
82 });
83 },
84};
85 
86export const siblingEnterSpans = {
87 async test(ctrl, env, ctx) {
88 const { withSpan } = env.tracingTest;
89 // Two enterSpan calls at the same level (sequential, not nested) should each
90 // produce a span whose parent is the top-level onset span, not each other.
91 withSpan('hierarchy-sibling-a', (span) => {
92 span.setAttribute('case', 'siblingEnterSpans');
93 span.setAttribute('role', 'sibling-a');
94 });
95 withSpan('hierarchy-sibling-b', (span) => {
96 span.setAttribute('case', 'siblingEnterSpans');
97 span.setAttribute('role', 'sibling-b');
98 });
99 },
100};
101 
102export const abandonedPromiseSpan = {
103 async test(ctrl, env, ctx) {
104 const { withSpan } = env.tracingTest;
105 // Wrap an async callback whose returned Promise never settles, and discard the
106 // outer promise. The validator asserts that the request's outcome event still
107 // fires in order; without BaseTracer::WeakRef, the abandoned SpanImpl would pin
108 // the tracer via its SpanSubmitter and delay outcome until V8 GC.
109 void withSpan('hierarchy-abandoned', async (span) => {
110 span.setAttribute('case', 'abandonedPromiseSpan');
111 await new Promise(() => {});
112 span.setAttribute('reached', true); // unreachable
113 });
114 },
115};