Skip to content
File

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

javascript219 lines
1// Copyright (c) 2025 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 { tracing as publicTracing } from 'cloudflare:workers';
7 
8export const syncFunction = {
9 async test(ctrl, env, ctx) {
10 const { withSpan } = env.tracingTest;
11 
12 // Test: Synchronous function - span should end immediately
13 const result = withSpan('sync-op', (span) => {
14 span.setAttribute('type', 'sync');
15 span.setAttribute('test', 'syncFunction');
16 return 'sync-value';
17 });
18 
19 assert.strictEqual(result, 'sync-value');
20 },
21};
22 
23export const asyncFunction = {
24 async test(ctrl, env, ctx) {
25 const { withSpan } = env.tracingTest;
26 
27 // Test: Async function returning Promise - span should end after promise resolves
28 const result = await withSpan('async-op', async (span) => {
29 span.setAttribute('type', 'async');
30 span.setAttribute('test', 'asyncFunction');
31 await new Promise((resolve) => setTimeout(resolve, 10));
32 return 'async-value';
33 });
34 
35 assert.strictEqual(result, 'async-value');
36 },
37};
38 
39export const syncError = {
40 async test(ctrl, env, ctx) {
41 const { withSpan } = env.tracingTest;
42 
43 // Test: Synchronous error - span should end before throwing
44 let errorCaught = false;
45 try {
46 withSpan('sync-error-op', (span) => {
47 span.setAttribute('type', 'sync-error');
48 span.setAttribute('test', 'syncError');
49 throw new Error('sync error');
50 });
51 } catch (e) {
52 errorCaught = true;
53 assert.strictEqual(e.message, 'sync error');
54 }
55 assert(errorCaught, 'Sync error should have been caught');
56 },
57};
58 
59export const asyncError = {
60 async test(ctrl, env, ctx) {
61 const { withSpan } = env.tracingTest;
62 
63 // Test: Async error (rejected promise) - span should end before rejection
64 let errorCaught = false;
65 try {
66 await withSpan('async-error-op', async (span) => {
67 span.setAttribute('type', 'async-error');
68 span.setAttribute('test', 'asyncError');
69 await new Promise((resolve) => setTimeout(resolve, 10));
70 throw new Error('async error');
71 });
72 } catch (e) {
73 errorCaught = true;
74 assert.strictEqual(e.message, 'async error');
75 }
76 assert(errorCaught, 'Async error should have been caught');
77 },
78};
79 
80// Verify the JS-visible class name is exactly "Span" - no internal-implementation names
81// should leak into JavaScript.
82export const spanClassName = {
83 async test(ctrl, env, ctx) {
84 const { withSpan } = env.tracingTest;
85 
86 withSpan('class-name-op', (span) => {
87 span.setAttribute('test', 'spanClassName');
88 assert.strictEqual(span.constructor.name, 'Span');
89 });
90 },
91};
92 
93// Verify isTraced reflects the state of the span: true while the span is live, false
94// after it has been auto-ended by withSpan.
95export const isTraced = {
96 async test(ctrl, env, ctx) {
97 const { withSpan } = env.tracingTest;
98 
99 let capturedSpan = null;
100 withSpan('is-traced-op', (span) => {
101 capturedSpan = span;
102 // While inside the callback, the span should be traced (we're inside a tailed
103 // request - the streamingTails binding is configured for this worker).
104 assert.strictEqual(
105 span.isTraced,
106 true,
107 'Span should be traced inside withSpan callback'
108 );
109 span.setAttribute('test', 'isTraced');
110 });
111 
112 // After withSpan returns, the span has been auto-ended -> isTraced should be false.
113 assert.strictEqual(
114 capturedSpan.isTraced,
115 false,
116 'Span should no longer be traced after auto-end'
117 );
118 },
119};
120 
121// Verify that setAttribute with undefined is a no-op (the attribute is simply not set),
122// which is the idiomatic pattern for optional attributes.
123export const setAttributeUndefined = {
124 async test(ctrl, env, ctx) {
125 const { withSpan } = env.tracingTest;
126 
127 const result = withSpan('undefined-attr-op', (span) => {
128 span.setAttribute('test', 'setAttributeUndefined');
129 // Passing undefined should not throw and should not record the attribute.
130 span.setAttribute('skipped', undefined);
131 return 'undefined-attr-value';
132 });
133 
134 assert.strictEqual(result, 'undefined-attr-value');
135 },
136};
137 
138// Verify that nested withSpan calls produce correctly nested spans. This exercises the
139// AsyncContextFrame push path in enterSpan: the inner span should be parented on the
140// outer span.
141export const nestedSyncSpans = {
142 async test(ctrl, env, ctx) {
143 const { withSpan } = env.tracingTest;
144 
145 const result = withSpan('nested-outer-op', (outerSpan) => {
146 outerSpan.setAttribute('test', 'nestedSyncSpans');
147 outerSpan.setAttribute('level', 'outer');
148 return withSpan('nested-inner-op', (innerSpan) => {
149 innerSpan.setAttribute('test', 'nestedSyncSpans');
150 innerSpan.setAttribute('level', 'inner');
151 return 'nested-value';
152 });
153 });
154 
155 assert.strictEqual(result, 'nested-value');
156 },
157};
158 
159// Async analog of the nested test: inner span lives inside an await that spans a
160// microtask boundary, so the inner span's parent is preserved only if the
161// AsyncContextFrame push correctly follows the async continuation.
162export const nestedAsyncSpans = {
163 async test(ctrl, env, ctx) {
164 const { withSpan } = env.tracingTest;
165 
166 const result = await withSpan(
167 'nested-async-outer-op',
168 async (outerSpan) => {
169 outerSpan.setAttribute('test', 'nestedAsyncSpans');
170 outerSpan.setAttribute('level', 'outer');
171 // Crossing a microtask boundary before creating the inner span.
172 await new Promise((resolve) => setTimeout(resolve, 5));
173 return await withSpan('nested-async-inner-op', async (innerSpan) => {
174 innerSpan.setAttribute('test', 'nestedAsyncSpans');
175 innerSpan.setAttribute('level', 'inner');
176 await new Promise((resolve) => setTimeout(resolve, 5));
177 return 'nested-async-value';
178 });
179 }
180 );
181 
182 assert.strictEqual(result, 'nested-async-value');
183 },
184};
185 
186// Verify the public import path: `import { tracing } from 'cloudflare:workers'`.
187// Hitting enterSpan via the public import should behave identically to the internal path.
188export const publicImportTracing = {
189 async test(ctrl, env, ctx) {
190 const result = publicTracing.enterSpan('public-import-op', (span) => {
191 span.setAttribute('test', 'publicImportTracing');
192 span.setAttribute('path', 'import-from-cloudflare-workers');
193 assert.strictEqual(span.isTraced, true);
194 return 'public-import-value';
195 });
196 assert.strictEqual(result, 'public-import-value');
197 },
198};
199 
200// Verify ctx.tracing: same Tracing instance should be reachable off the execution context.
201export const ctxTracing = {
202 async test(ctrl, env, ctx) {
203 assert.ok(ctx.tracing, 'ctx.tracing should be defined');
204 assert.strictEqual(
205 typeof ctx.tracing.enterSpan,
206 'function',
207 'ctx.tracing.enterSpan should be a function'
208 );
209 
210 const result = ctx.tracing.enterSpan('ctx-tracing-op', (span) => {
211 span.setAttribute('test', 'ctxTracing');
212 span.setAttribute('path', 'ctx.tracing');
213 assert.strictEqual(span.isTraced, true);
214 return 'ctx-tracing-value';
215 });
216 assert.strictEqual(result, 'ctx-tracing-value');
217 },
218};