|
1 | 1 | /** |
2 | 2 | * React utilities for OpenTelemetry tracing in components. |
3 | 3 | */ |
4 | | -import { trace, context, SpanStatusCode, Span } from '@opentelemetry/api' |
| 4 | +import { trace, SpanStatusCode, Span } from '@opentelemetry/api' |
5 | 5 |
|
| 6 | +/** |
| 7 | + * Creates a span and executes a function within its context. |
| 8 | + * |
| 9 | + * This ensures that: |
| 10 | + * - The span is automatically set as the active context |
| 11 | + * - Any automatic instrumentation (e.g., fetch, XHR) creates child spans |
| 12 | + * - Nested calls to traceSpan() properly create child spans |
| 13 | + * - The span is properly ended even if errors occur |
| 14 | + * |
| 15 | + * @param spanName - Name of the span to create |
| 16 | + * @param fn - Async function to execute within the span context |
| 17 | + * @param attributes - Optional attributes to set on the span |
| 18 | + * @returns Promise resolving to the function's return value |
| 19 | + */ |
6 | 20 | export async function traceSpan<T>( |
7 | 21 | spanName: string, |
8 | 22 | fn: (span: Span) => Promise<T>, |
9 | 23 | attributes?: Record<string, string | number | boolean> |
10 | 24 | ): Promise<T> { |
11 | 25 | const tracer = trace.getTracer('docs-frontend') |
12 | | - const span = tracer.startSpan(spanName, undefined, context.active()) |
13 | 26 |
|
14 | | - if (attributes) { |
15 | | - span.setAttributes(attributes) |
16 | | - } |
| 27 | + // startActiveSpan automatically: |
| 28 | + // 1. Creates the span |
| 29 | + // 2. Sets it as the active context |
| 30 | + // 3. Executes the callback within that context |
| 31 | + // 4. Ends the span after execution |
| 32 | + return tracer.startActiveSpan(spanName, async (span) => { |
| 33 | + if (attributes) { |
| 34 | + span.setAttributes(attributes) |
| 35 | + } |
17 | 36 |
|
18 | | - try { |
19 | | - const result = await fn(span) |
20 | | - span.setStatus({ code: SpanStatusCode.OK }) |
21 | | - return result |
22 | | - } catch (error) { |
23 | | - // Check if this is an AbortError (user cancelled/typed more) |
24 | | - if (error instanceof Error && error.name === 'AbortError') { |
25 | | - // Cancellation is NOT an error - it's expected behavior |
26 | | - span.setAttribute('cancelled', true) |
| 37 | + try { |
| 38 | + const result = await fn(span) |
27 | 39 | span.setStatus({ code: SpanStatusCode.OK }) |
28 | | - } else { |
29 | | - // Real error - mark as ERROR |
30 | | - span.setStatus({ |
31 | | - code: SpanStatusCode.ERROR, |
32 | | - message: error instanceof Error ? error.message : String(error), |
33 | | - }) |
34 | | - span.recordException(error as Error) |
| 40 | + return result |
| 41 | + } catch (error) { |
| 42 | + // Check if this is an AbortError (user cancelled/typed more) |
| 43 | + if (error instanceof Error && error.name === 'AbortError') { |
| 44 | + // Cancellation is NOT an error - it's expected behavior |
| 45 | + span.setAttribute('cancelled', true) |
| 46 | + span.setStatus({ code: SpanStatusCode.OK }) |
| 47 | + } else { |
| 48 | + // Real error - mark as ERROR |
| 49 | + span.setStatus({ |
| 50 | + code: SpanStatusCode.ERROR, |
| 51 | + message: |
| 52 | + error instanceof Error ? error.message : String(error), |
| 53 | + }) |
| 54 | + span.recordException(error as Error) |
| 55 | + } |
| 56 | + throw error |
| 57 | + } finally { |
| 58 | + span.end() |
35 | 59 | } |
36 | | - throw error |
37 | | - } finally { |
38 | | - span.end() |
39 | | - } |
| 60 | + }) |
40 | 61 | } |
0 commit comments