You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(otel): propagate TracingChannel span context in OTEL compat mode (#1724)
## Problem
When `setupOtelCompat()` is active, `OtelContextManager` did not expose
`[BRAINTRUST_CURRENT_SPAN_STORE]`, so `bindCurrentSpanStoreToStart()`
silently skipped context binding. Auto-instrumented spans (Anthropic,
OpenAI, Google GenAI) had no parent context in OTEL compat mode.
## Solution
Make `ContextManager` a thin abstraction over whichever ALS is in use:
- **`wrapSpanForStore(span)`** added to `ContextManager` base class
(default: identity — returns span directly)
- **`OtelContextManager`** overrides with two-mode behavior:
- **OTEL ALS available** (`AsyncLocalStorageContextManager`): exposes
OTEL's internal `_asyncLocalStorage`, `wrapSpanForStore` returns an OTEL
`Context` wrapping the span
- **OTEL ALS unavailable** (`AsyncHooksContextManager`): falls back to
its own `IsoAsyncLocalStorage<Span>`, `wrapSpanForStore` returns the
span directly — same behavior as `BraintrustContextManager`
- **`bindCurrentSpanStoreToStart`** (and `google-genai-plugin.ts`) call
`contextManager.wrapSpanForStore(span)` in the bindStore transform
- `getCurrentSpan()` checks both
`otelContext.active().getValue(BT_SPAN_KEY)` and the fallback ALS, so
spans propagated by either path are visible
## Additional cleanup
- Extract `buildBtOtelContext()` helper (eliminates 40-line duplication
in `runInContext`)
- Unify `BT_SPAN_KEY` / `BT_PARENT_KEY` as module-level constants
- Add `CurrentSpanStore = IsoAsyncLocalStorage<unknown>` type alias
- Export `CurrentSpanStore` from `braintrust` package
## Tests
8 new tests in `otel-compat.test.ts`:
- OTEL ALS exposed via `BRAINTRUST_CURRENT_SPAN_STORE` ✓
- `wrapSpanForStore` returns Context → `currentSpan()` works ✓
- `store.run()` propagation ✓
- Nested runs maintain span chain ✓
- OTEL active span has matching IDs ✓
- `AsyncHooksContextManager` fallback: ALS always exposed ✓
- `AsyncHooksContextManager` fallback: `wrapSpanForStore` returns span
directly ✓
- `AsyncHooksContextManager` fallback: `getCurrentSpan()` reads from
fallback ALS ✓
All tests pass on both otel-v1 (OTel 1.x) and otel-v2 (OTel 2.x).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
0 commit comments