autotel-cloudflare 2.19.0 → 3.1.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.
@@ -2,159 +2,53 @@
2
2
  * Type definitions for Cloudflare Agents SDK observability integration
3
3
  */
4
4
 
5
- import type { ConfigurationOption } from 'autotel-edge';
6
5
  import type { Attributes } from '@opentelemetry/api';
6
+ import type { ConfigurationOption } from 'autotel-edge';
7
+ import type { AgentObservabilityEvent } from './agent';
8
+ import type { BaseEvent } from './base';
9
+ import type { MCPObservabilityEvent } from './mcp';
10
+ import type {
11
+ ChannelEventMap,
12
+ Observability,
13
+ ObservabilityEvent,
14
+ } from './observability';
7
15
 
8
- /**
9
- * Base event structure from Agents SDK (mirrors agents/src/observability/base.ts)
10
- */
11
- export interface BaseAgentEvent<
12
- T extends string,
13
- Payload extends Record<string, unknown> = Record<string, unknown>
14
- > {
15
- type: T;
16
- /** Unique identifier for the event */
17
- id: string;
18
- /** Human-readable message for logging */
19
- displayMessage: string;
20
- /** Event payload with type-specific data */
21
- payload: Payload & Record<string, unknown>;
22
- /** Timestamp in milliseconds since epoch */
23
- timestamp: number;
24
- }
25
-
26
- /**
27
- * Agent-specific observability events
28
- */
29
- export type AgentObservabilityEvent =
30
- | BaseAgentEvent<'state:update', Record<string, unknown>>
31
- | BaseAgentEvent<
32
- 'rpc',
33
- {
34
- method: string;
35
- streaming?: boolean;
36
- }
37
- >
38
- | BaseAgentEvent<'message:request' | 'message:response', Record<string, unknown>>
39
- | BaseAgentEvent<'message:clear'>
40
- | BaseAgentEvent<
41
- 'schedule:create' | 'schedule:execute' | 'schedule:cancel',
42
- {
43
- callback: string;
44
- id: string;
45
- }
46
- >
47
- | BaseAgentEvent<'destroy'>
48
- | BaseAgentEvent<
49
- 'connect',
50
- {
51
- connectionId: string;
52
- }
53
- >;
54
-
55
- /**
56
- * MCP-specific observability events
57
- */
58
- export type MCPObservabilityEvent =
59
- | BaseAgentEvent<'mcp:client:preconnect', { serverId: string }>
60
- | BaseAgentEvent<
61
- 'mcp:client:connect',
62
- { url: string; transport: string; state: string; error?: string }
63
- >
64
- | BaseAgentEvent<
65
- 'mcp:client:authorize',
66
- {
67
- serverId: string;
68
- authUrl: string;
69
- clientId?: string;
70
- }
71
- >
72
- | BaseAgentEvent<'mcp:client:discover', Record<string, unknown>>;
16
+ export type { BaseEvent, AgentObservabilityEvent, MCPObservabilityEvent };
17
+ export type { Observability, ObservabilityEvent, ChannelEventMap };
73
18
 
74
- /**
75
- * Union of all observability event types
76
- */
77
- export type ObservabilityEvent = AgentObservabilityEvent | MCPObservabilityEvent;
78
-
79
- /**
80
- * Observability interface from Agents SDK
81
- */
82
- export interface Observability {
83
- /**
84
- * Emit an event for the Agent's observability implementation to handle.
85
- * @param event - The event to emit
86
- * @param ctx - The execution context of the invocation (optional)
87
- */
88
- emit(event: ObservabilityEvent, ctx?: DurableObjectState): void;
89
- }
19
+ export type ObservabilityExecutionContext = DurableObjectState | ExecutionContext;
90
20
 
91
- /**
92
- * Agent-specific instrumentation options
93
- */
94
21
  export interface AgentInstrumentationOptions {
95
- /**
96
- * Whether to create spans for RPC calls
97
- * @default true
98
- */
99
22
  traceRpc?: boolean;
100
-
101
- /**
102
- * Whether to create spans for schedule operations
103
- * @default true
104
- */
105
23
  traceSchedule?: boolean;
106
-
107
- /**
108
- * Whether to create spans for MCP operations
109
- * @default true
110
- */
24
+ traceQueue?: boolean;
25
+ traceSubmissions?: boolean;
111
26
  traceMcp?: boolean;
112
-
113
- /**
114
- * Whether to create spans for state updates
115
- * @default false (can be noisy)
116
- */
117
27
  traceStateUpdates?: boolean;
118
-
119
- /**
120
- * Whether to create spans for message events
121
- * @default true
122
- */
123
28
  traceMessages?: boolean;
124
-
125
- /**
126
- * Whether to create spans for connect/destroy lifecycle events
127
- * @default true
128
- */
129
29
  traceLifecycle?: boolean;
130
-
131
- /**
132
- * Custom attribute extractor for events
133
- */
30
+ traceChat?: boolean;
31
+ traceTranscripts?: boolean;
32
+ traceFibers?: boolean;
33
+ traceToolRecovery?: boolean;
34
+ traceWorkflow?: boolean;
35
+ traceEmail?: boolean;
134
36
  attributeExtractor?: (event: ObservabilityEvent) => Attributes;
135
-
136
- /**
137
- * Custom span name formatter
138
- */
139
37
  spanNameFormatter?: (event: ObservabilityEvent) => string;
140
38
  }
141
39
 
142
- /**
143
- * Configuration for OtelObservability
144
- */
145
40
  export type OtelObservabilityConfig = ConfigurationOption & {
146
- /**
147
- * Agent-specific instrumentation options
148
- */
149
41
  agents?: AgentInstrumentationOptions;
150
42
  };
151
43
 
152
- /**
153
- * Semantic attributes for Agent spans
154
- */
155
44
  export interface AgentSpanAttributes {
156
45
  'agent.event.type': string;
157
- 'agent.event.id': string;
46
+ 'agent.event.id'?: string;
47
+ 'agent.class'?: string;
48
+ 'agent.instance.name'?: string;
49
+ 'agent.display_message'?: string;
50
+ 'agent.framework'?: string;
51
+ 'gen_ai.agent.name'?: string;
158
52
  'agent.rpc.method'?: string;
159
53
  'agent.rpc.streaming'?: boolean;
160
54
  'agent.schedule.callback'?: string;
package/src/agents.ts CHANGED
@@ -47,12 +47,20 @@
47
47
  * const observability = createOtelObservability({
48
48
  * service: { name: 'my-agent' },
49
49
  * agents: {
50
- * traceRpc: true, // Trace RPC calls (default: true)
51
- * traceSchedule: true, // Trace scheduled tasks (default: true)
52
- * traceMcp: true, // Trace MCP operations (default: true)
53
- * traceStateUpdates: false, // Skip state updates (default: false, can be noisy)
54
- * traceMessages: true, // Trace message events (default: true)
55
- * traceLifecycle: true, // Trace connect/destroy (default: true)
50
+ * traceRpc: true, // RPC and rpc:error
51
+ * traceSchedule: true, // schedule:* events
52
+ * traceQueue: true, // queue:* events
53
+ * traceSubmissions: true, // submission:* events
54
+ * traceMcp: true, // mcp:* events
55
+ * traceStateUpdates: false, // state:update (off by default; noisy)
56
+ * traceMessages: true, // message:* and tool:* events
57
+ * traceChat: true, // chat:* recovery/stream/context events
58
+ * traceTranscripts: true, // chat:transcript:* events
59
+ * traceFibers: true, // fiber:* events
60
+ * traceToolRecovery: true, // agent_tool:* recovery events
61
+ * traceWorkflow: true, // workflow:* events
62
+ * traceEmail: true, // email:* events
63
+ * traceLifecycle: true, // connect/disconnect/destroy
56
64
  * }
57
65
  * })
58
66
  * ```
@@ -65,12 +73,15 @@ export {
65
73
  createOtelObservabilityFromEnv,
66
74
  OtelObservability,
67
75
  } from './agents/otel-observability';
76
+ export { channels, genericObservability, subscribe } from './agents/observability';
68
77
  export type {
78
+ BaseEvent,
69
79
  OtelObservabilityConfig,
70
80
  AgentObservabilityEvent,
71
81
  MCPObservabilityEvent,
72
82
  ObservabilityEvent,
73
83
  Observability,
84
+ ChannelEventMap,
74
85
  AgentInstrumentationOptions,
75
86
  AgentSpanAttributes,
76
87
  } from './agents/types';
package/src/index.ts CHANGED
@@ -81,3 +81,11 @@ export {
81
81
 
82
82
  // Global instrumentations
83
83
  export { instrumentGlobalFetch, instrumentGlobalCache } from './global';
84
+
85
+ // Cloudflare native tracing helpers (auto-wired by the handler wrappers;
86
+ // exported for manual detection). `enterSpan` is re-exported from autotel-edge
87
+ // above via `export * from 'autotel-edge'`.
88
+ export {
89
+ isNativeTracingAvailable,
90
+ getNativeTracerFromCtx,
91
+ } from './native/native-tracing';
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ isNativeTracingAvailable,
4
+ getNativeTracerFromCtx,
5
+ } from './native-tracing';
6
+
7
+ function ctxWithTracing() {
8
+ const enterSpan = vi.fn((_name: string, cb: (s: any) => unknown) =>
9
+ cb({ isTraced: true, setAttribute: vi.fn() }),
10
+ );
11
+ return { tracing: { enterSpan }, waitUntil() {}, passThroughOnException() {} };
12
+ }
13
+
14
+ describe('isNativeTracingAvailable', () => {
15
+ it('is true when ctx.tracing.enterSpan is a function', () => {
16
+ expect(isNativeTracingAvailable(ctxWithTracing())).toBe(true);
17
+ });
18
+
19
+ it('is false for a plain ExecutionContext (tracing disabled / old runtime)', () => {
20
+ expect(isNativeTracingAvailable({ waitUntil() {} })).toBe(false);
21
+ });
22
+
23
+ it('is false for null/undefined or malformed tracing', () => {
24
+ expect(isNativeTracingAvailable(undefined)).toBe(false);
25
+ expect(isNativeTracingAvailable(null)).toBe(false);
26
+ expect(isNativeTracingAvailable({ tracing: {} })).toBe(false);
27
+ expect(isNativeTracingAvailable({ tracing: { enterSpan: 123 } })).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('getNativeTracerFromCtx', () => {
32
+ it('returns null when native tracing is unavailable', () => {
33
+ expect(getNativeTracerFromCtx({ waitUntil() {} })).toBeNull();
34
+ });
35
+
36
+ it('wraps ctx.tracing.enterSpan as a NativeTracer', () => {
37
+ const ctx = ctxWithTracing();
38
+ const tracer = getNativeTracerFromCtx(ctx);
39
+ expect(tracer).not.toBeNull();
40
+
41
+ const result = tracer!.enterSpan('work', (span) => {
42
+ span.setAttribute('k', 'v');
43
+ return 7;
44
+ });
45
+
46
+ expect(result).toBe(7);
47
+ expect(ctx.tracing.enterSpan).toHaveBeenCalledWith('work', expect.any(Function));
48
+ });
49
+
50
+ it('attaches the supplied correlation id to the tracer', () => {
51
+ const tracer = getNativeTracerFromCtx(ctxWithTracing(), 'ray-xyz');
52
+ expect(tracer?.correlationId).toBe('ray-xyz');
53
+ });
54
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cloudflare native tracing adapter
3
+ *
4
+ * Bridges Cloudflare Workers' built-in custom-span API
5
+ * (`ctx.tracing.enterSpan()` / `import { tracing } from "cloudflare:workers"`)
6
+ * to autotel-edge's runtime-agnostic {@link NativeTracer} seam.
7
+ *
8
+ * When a Worker has tracing enabled (`observability.traces.enabled = true` in
9
+ * Wrangler) the runtime exposes `ctx.tracing`. The handler wrappers detect it,
10
+ * wrap it as a {@link NativeTracer}, and install it into the active context with
11
+ * `withNativeTracer()`. From then on every autotel `span()` / `trace()` /
12
+ * `enterSpan()` call — including deep inside utility functions and libraries —
13
+ * automatically routes to Cloudflare's native tracer and nests inside the
14
+ * platform's trace waterfall (fetch / KV / R2 / D1 / handler spans), exported
15
+ * by Cloudflare to whichever destination is configured in Wrangler.
16
+ *
17
+ * No `cloudflare:workers` import is required here: the native tracer travels
18
+ * through autotel's AsyncLocalStorage context, so code without access to `ctx`
19
+ * still picks it up via `getActiveNativeTracer()`.
20
+ */
21
+
22
+ import type { NativeTracer, NativeSpanHandle } from 'autotel-edge';
23
+
24
+ /**
25
+ * Cloudflare's native custom-span surface. Declared locally because it is not
26
+ * yet present in `@cloudflare/workers-types`. Structurally compatible with
27
+ * autotel-edge's {@link NativeSpanHandle}.
28
+ */
29
+ interface CloudflareSpan {
30
+ readonly isTraced: boolean;
31
+ setAttribute(key: string, value: string | number | boolean | undefined): void;
32
+ }
33
+
34
+ /**
35
+ * Cloudflare's `tracing` object, available as `ctx.tracing` on the
36
+ * ExecutionContext and as the `tracing` export of `cloudflare:workers`.
37
+ */
38
+ interface CloudflareTracing {
39
+ enterSpan<T, A extends unknown[]>(
40
+ name: string,
41
+ callback: (span: CloudflareSpan, ...args: A) => T,
42
+ ...args: A
43
+ ): T;
44
+ }
45
+
46
+ type MaybeTracingCarrier = { tracing?: CloudflareTracing } | null | undefined;
47
+
48
+ function readTracing(carrier: unknown): CloudflareTracing | undefined {
49
+ const tracing = (carrier as MaybeTracingCarrier)?.tracing;
50
+ return typeof tracing?.enterSpan === 'function' ? tracing : undefined;
51
+ }
52
+
53
+ /**
54
+ * Returns `true` when Cloudflare native custom-span tracing is available on the
55
+ * given ExecutionContext (i.e. tracing is enabled for this Worker).
56
+ */
57
+ export function isNativeTracingAvailable(ctx: unknown): boolean {
58
+ return readTracing(ctx) !== undefined;
59
+ }
60
+
61
+ /**
62
+ * Wrap Cloudflare's `ctx.tracing` as an autotel {@link NativeTracer}, or return
63
+ * `null` when native tracing is unavailable on this context.
64
+ *
65
+ * @param correlationId Optional per-request id (e.g. the `cf-ray` header)
66
+ * surfaced as `ctx.correlationId` and a `correlation.id` span attribute, so
67
+ * logs, custom spans, and the Cloudflare dashboard share one queryable key
68
+ * today — before Cloudflare exposes in-code trace/span ids.
69
+ */
70
+ export function getNativeTracerFromCtx(
71
+ ctx: unknown,
72
+ correlationId?: string,
73
+ ): NativeTracer | null {
74
+ const tracing = readTracing(ctx);
75
+ if (!tracing) {
76
+ return null;
77
+ }
78
+ return {
79
+ correlationId,
80
+ enterSpan: <T>(name: string, callback: (span: NativeSpanHandle) => T): T =>
81
+ tracing.enterSpan(name, callback as (span: CloudflareSpan) => T),
82
+ };
83
+ }
package/src/native.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * autotel-cloudflare/native
3
+ *
4
+ * Cloudflare native tracing helpers. Most users never need this entry point —
5
+ * the handler wrappers (`instrument`, `wrapModule`, `defineWorkerFetch`,
6
+ * `wrapDurableObject`) auto-detect and wire up native tracing for you. Import
7
+ * from here only when you want to detect native tracing yourself.
8
+ */
9
+ export {
10
+ isNativeTracingAvailable,
11
+ getNativeTracerFromCtx,
12
+ } from './native/native-tracing';
@@ -41,10 +41,14 @@ import {
41
41
  getActiveConfig,
42
42
  getServiceForPath,
43
43
  shouldInstrumentPath,
44
+ withNativeTracer,
45
+ ensureGlobalContextManager,
44
46
  type Initialiser,
47
+ type NativeTracer,
45
48
  WorkerTracerProvider,
46
49
  WorkerTracer,
47
50
  } from 'autotel-edge';
51
+ import { getNativeTracerFromCtx } from '../native/native-tracing';
48
52
  import { proxyExecutionContext, unwrap, wrap, type PromiseTracker } from '../bindings/common';
49
53
  import { instrumentGlobalFetch } from '../global/fetch';
50
54
  import { instrumentGlobalCache } from '../global/cache';
@@ -528,6 +532,68 @@ function createHandlerFlow<T extends Trigger, E, R>(
528
532
  };
529
533
  }
530
534
 
535
+ let warnedMissingNative = false;
536
+
537
+ /**
538
+ * Resolve whether this invocation should use Cloudflare's native tracer.
539
+ *
540
+ * - `nativeTracing: 'off'` → never use native (always autotel's OTLP pipeline).
541
+ * - `nativeTracing: 'on'` → require native; warn once and fall back if absent.
542
+ * - `nativeTracing: 'auto'` (default) → use native when `ctx.tracing` exists.
543
+ */
544
+ function resolveNativeTracer(
545
+ config: ResolvedEdgeConfig,
546
+ ctx: ExecutionContext,
547
+ trigger: Trigger,
548
+ ): NativeTracer | null {
549
+ const mode = config.nativeTracing ?? 'auto';
550
+ if (mode === 'off') {
551
+ return null;
552
+ }
553
+ // Correlation id: prefer Cloudflare's ray id (also stamped on the native root
554
+ // span + carried by the workers logger), so logs/spans/dashboard share a key.
555
+ // Fall back to a per-invocation uuid for non-fetch triggers.
556
+ const rayId = trigger instanceof Request ? trigger.headers.get('cf-ray') : null;
557
+ const correlationId = rayId ?? crypto.randomUUID();
558
+ const nativeTracer = getNativeTracerFromCtx(ctx, correlationId);
559
+ if (!nativeTracer && mode === 'on' && !warnedMissingNative) {
560
+ warnedMissingNative = true;
561
+ console.warn(
562
+ "[autotel-cloudflare] nativeTracing is 'on' but Cloudflare native tracing was not detected. " +
563
+ 'Enable observability.traces in your Wrangler config (and use a recent compatibility_date). ' +
564
+ 'Falling back to the autotel OTLP exporter.',
565
+ );
566
+ }
567
+ return nativeTracer;
568
+ }
569
+
570
+ /**
571
+ * Run a handler in native-tracing mode.
572
+ *
573
+ * Cloudflare already creates the root handler span and instruments platform
574
+ * operations (fetch / KV / R2 / D1 / DO), so autotel defers entirely: no
575
+ * binding proxies (avoids duplicate spans), no provider/exporter, no manual
576
+ * flush. We install the native tracer into the active context so that the
577
+ * user's `span()` / `trace()` / `enterSpan()` calls nest inside Cloudflare's
578
+ * native waterfall and are exported by the platform.
579
+ */
580
+ function runWithNativeTracing<T extends Trigger, E, R>(
581
+ handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
582
+ trigger: T,
583
+ env: E,
584
+ ctx: ExecutionContext,
585
+ config: ResolvedEdgeConfig,
586
+ nativeTracer: NativeTracer,
587
+ ): R | Promise<R> {
588
+ // Register the context manager (we skip initProvider in native mode) so the
589
+ // native tracer propagates through OTel context into span()/trace() — without
590
+ // it, getActiveNativeTracer() returns null and custom spans never route to
591
+ // enterSpan. Bindings are NOT proxied: Cloudflare instruments them natively.
592
+ ensureGlobalContextManager();
593
+ const nativeContext = withNativeTracer(nativeTracer, setConfig(config));
594
+ return api_context.with(nativeContext, () => handlerFn(trigger, env, ctx));
595
+ }
596
+
531
597
  /**
532
598
  * Create handler proxy
533
599
  */
@@ -539,16 +605,22 @@ function createHandlerProxy<T extends Trigger, E, R>(
539
605
  ): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
540
606
  return (trigger: T, env: E, ctx: ExecutionContext) => {
541
607
  const config = initialiser(env, trigger);
542
-
608
+
543
609
  // Check if instrumentation is disabled (useful for local dev)
544
610
  if (config.instrumentation.disabled) {
545
611
  // Return handler as-is without instrumentation
546
612
  return handlerFn(trigger, env, ctx);
547
613
  }
548
-
614
+
615
+ // Prefer Cloudflare native tracing when available — defer to the platform.
616
+ const nativeTracer = resolveNativeTracer(config, ctx, trigger);
617
+ if (nativeTracer) {
618
+ return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer);
619
+ }
620
+
549
621
  // Auto-instrument Cloudflare bindings in the environment
550
622
  const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
551
-
623
+
552
624
  const configContext = setConfig(config);
553
625
 
554
626
  // Initialize provider on first call
@@ -593,10 +665,16 @@ function createHandlerProxyWithConfig<T extends Trigger, E, R>(
593
665
  return handlerFn(trigger, env, ctx) as ReturnType<typeof handlerFn>;
594
666
  }
595
667
  }
596
-
668
+
669
+ // Prefer Cloudflare native tracing when available — defer to the platform.
670
+ const nativeTracer = resolveNativeTracer(config, ctx, trigger);
671
+ if (nativeTracer) {
672
+ return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer) as ReturnType<typeof handlerFn>;
673
+ }
674
+
597
675
  // Auto-instrument Cloudflare bindings in the environment
598
676
  const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
599
-
677
+
600
678
  const configContext = setConfig(config);
601
679
 
602
680
  // Initialize provider on first call
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { instrument } from './instrument';
3
+ import { span } from 'autotel-edge';
4
+ import { isWrapped } from '../bindings/common';
5
+
6
+ /**
7
+ * A KV-shaped binding so instrumentBindings() would proxy it in the OTLP path.
8
+ * In native mode we must NOT proxy it (Cloudflare traces KV natively) — these
9
+ * tests assert that the handler receives the original, unwrapped binding and
10
+ * that user spans route to ctx.tracing.enterSpan instead of the OTel pipeline.
11
+ */
12
+ function fakeKv() {
13
+ return {
14
+ get: vi.fn(async () => 'value'),
15
+ put: vi.fn(async () => {}),
16
+ delete: vi.fn(async () => {}),
17
+ list: vi.fn(async () => ({ keys: [] })),
18
+ };
19
+ }
20
+
21
+ function nativeCtx() {
22
+ const enteredSpans: string[] = [];
23
+ const waitUntil = vi.fn();
24
+ const ctx = {
25
+ waitUntil,
26
+ passThroughOnException() {},
27
+ tracing: {
28
+ enterSpan: vi.fn((name: string, cb: (s: any) => unknown) => {
29
+ enteredSpans.push(name);
30
+ return cb({ isTraced: true, setAttribute: vi.fn() });
31
+ }),
32
+ },
33
+ };
34
+ return { ctx, enteredSpans, waitUntil };
35
+ }
36
+
37
+ const baseConfig = {
38
+ service: { name: 'native-test' },
39
+ exporter: { url: 'http://localhost:4318/v1/traces' },
40
+ } as const;
41
+
42
+ describe('instrument() with Cloudflare native tracing (auto)', () => {
43
+ it('defers binding instrumentation to the platform (no proxy wrapping)', async () => {
44
+ const { ctx } = nativeCtx();
45
+ const kv = fakeKv();
46
+ let seenBinding: unknown;
47
+
48
+ const handler = instrument(
49
+ {
50
+ async fetch(_req, env: { MY_KV: ReturnType<typeof fakeKv> }) {
51
+ seenBinding = env.MY_KV;
52
+ await env.MY_KV.get('k');
53
+ return new Response('ok');
54
+ },
55
+ },
56
+ baseConfig,
57
+ );
58
+
59
+ await handler.fetch!(new Request('https://x/'), { MY_KV: kv }, ctx as never);
60
+
61
+ // Handler received the ORIGINAL binding, not an autotel proxy.
62
+ expect(seenBinding).toBe(kv);
63
+ expect(isWrapped(seenBinding)).toBe(false);
64
+ // No "KV ...: get" span was created via the native tracer.
65
+ expect(
66
+ ctx.tracing.enterSpan.mock.calls.some(([name]) =>
67
+ String(name).startsWith('KV '),
68
+ ),
69
+ ).toBe(false);
70
+ });
71
+
72
+ it('surfaces the cf-ray header as a correlation.id span attribute', async () => {
73
+ const attrs: Record<string, unknown> = {};
74
+ const ctx = {
75
+ waitUntil() {},
76
+ passThroughOnException() {},
77
+ tracing: {
78
+ enterSpan: vi.fn((_name: string, cb: (s: any) => unknown) =>
79
+ cb({
80
+ isTraced: true,
81
+ setAttribute(k: string, v: unknown) {
82
+ attrs[k] = v;
83
+ },
84
+ }),
85
+ ),
86
+ },
87
+ };
88
+
89
+ const handler = instrument(
90
+ { async fetch() { return span('work', () => new Response('ok')); } },
91
+ baseConfig,
92
+ );
93
+
94
+ const req = new Request('https://x/', { headers: { 'cf-ray': 'ray-7f' } });
95
+ await handler.fetch!(req, {}, ctx as never);
96
+ expect(attrs['correlation.id']).toBe('ray-7f');
97
+ });
98
+
99
+ it('routes user span() calls to ctx.tracing.enterSpan', async () => {
100
+ const { ctx, enteredSpans } = nativeCtx();
101
+
102
+ const handler = instrument(
103
+ {
104
+ async fetch() {
105
+ return span('user.work', (s) => {
106
+ s.setAttribute('ok', true);
107
+ return new Response('done');
108
+ });
109
+ },
110
+ },
111
+ baseConfig,
112
+ );
113
+
114
+ const res = await handler.fetch!(new Request('https://x/'), {}, ctx as never);
115
+ expect(await res.text()).toBe('done');
116
+ expect(enteredSpans).toContain('user.work');
117
+ });
118
+
119
+ it('does not run the OTLP export flow (no waitUntil) in native mode', async () => {
120
+ const { ctx, waitUntil } = nativeCtx();
121
+ const handler = instrument(
122
+ { async fetch() { return new Response('ok'); } },
123
+ baseConfig,
124
+ );
125
+ await handler.fetch!(new Request('https://x/'), {}, ctx as never);
126
+ expect(waitUntil).not.toHaveBeenCalled();
127
+ });
128
+ });
129
+
130
+ describe('instrument() with nativeTracing: "off"', () => {
131
+ it('ignores ctx.tracing and uses the autotel pipeline (proxies bindings)', async () => {
132
+ const { ctx } = nativeCtx();
133
+ const kv = fakeKv();
134
+ let seenBinding: unknown;
135
+
136
+ const handler = instrument(
137
+ {
138
+ async fetch(_req, env: { MY_KV: ReturnType<typeof fakeKv> }) {
139
+ seenBinding = env.MY_KV;
140
+ return new Response('ok');
141
+ },
142
+ },
143
+ { ...baseConfig, nativeTracing: 'off' },
144
+ );
145
+
146
+ await handler.fetch!(new Request('https://x/'), { MY_KV: kv }, ctx as never);
147
+
148
+ // OTLP path proxies the binding, so the handler sees a wrapped object.
149
+ expect(isWrapped(seenBinding)).toBe(true);
150
+ // Native tracer was never used.
151
+ expect(ctx.tracing.enterSpan).not.toHaveBeenCalled();
152
+ });
153
+ });