Conversation
…l context `create_otel_span` calls `otel_tracer.start_span(name)` for root observations without touching the current context. `Tracer#start_span` implicitly parents the new span to `OpenTelemetry::Context.current`, and that context is process-wide and provider-agnostic -- it is not scoped to the TracerProvider the tracer came from. So when the host application runs its own OpenTelemetry instrumentation (Rack, ActiveJob, Sidekiq...) on a separate provider, a root observation started inside an instrumented request or job silently becomes a child of that ambient span. Langfuse only ingests the child, pointing at a trace_id whose root span was exported somewhere else entirely and never reaches Langfuse -- the UI then shows an empty, unnamed trace wrapping the real observation, and grouping by trace name becomes unusable. This is not fixed by the isolated tracer provider introduced in 0.8.0: that isolates which spans are *exported* to Langfuse, while this is about which span a new span is *parented to*. Resetting to `Context::ROOT` for the duration of the span creation makes root observations genuinely rooted, whatever the caller's context. Nesting is unaffected: once started, the span becomes current again, so child observations keep attaching to it, and the caller's ambient context is restored on the way out. The regression spec covers all three properties: a root observation gets its own trace, children still nest under it, and the ambient context is left untouched.
…butes Resetting to `Context::ROOT` dropped every context value, not just the parent span. `propagate_attributes` stores user_id, session_id, tags and metadata as context values, and `SpanProcessor#on_start` reads them from the parent context -- so a root observation opened inside the documented `propagate_attributes` pattern silently lost its identity. Setting the current span to `Span::INVALID` instead keeps the rest of the context intact while still producing a real root: the SDK only inherits a trace_id when the parent span context is valid (`TracerProvider#internal_start_span`). Added a regression spec asserting user.id / session.id survive on a root observation started inside `propagate_attributes`; it fails with the previous `Context::ROOT` approach. Thanks to Cursor Bugbot for catching this.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 60ac94a. Configure here.
Detaching unconditionally broke legitimate nesting: `observe` makes its observation the current span via `run_in_context`, and a nested `Langfuse.observe` / `start_observation` relies on that ambient context to attach. Every nested observation would have been exported as a disconnected root -- experiment tasks and `observe`-instrumented helpers included. The detach now applies only when the ambient span is not Langfuse's own, which the instrumentation scope identifies: spans opened by `LANGFUSE_TRACER_NAME` are legitimate parents, everything else (host app instrumentation, non-recording or remote spans) is foreign. Added a regression spec asserting a nested observation keeps its parent's trace_id; it fails without the guard.
|
I checked this against I also reproduced two regressions in the current patch:
Both checks pass on Could you add a minimal reproduction of the empty or unnamed trace on current |

Problem
create_otel_spancreates root observations with:Tracer#start_spanimplicitly parents the new span toOpenTelemetry::Context.current. That context is process-wide and provider-agnostic — it is not scoped to theTracerProviderthe tracer came from. So this branch only produces an actual root span when nothing else is currently active.In a host application that runs its own OpenTelemetry instrumentation (Rack, ActiveJob, Sidekiq…) on a separate provider — a very common setup — a root observation started inside an instrumented request or job silently becomes a child of that ambient span.
The consequence is only visible on the Langfuse side: Langfuse ingests the child, whose
trace_idbelongs to a root span that was exported to the application's own backend and never reaches Langfuse. The UI then renders an empty, unnamed trace wrapping the real observation. Grouping or filtering by trace name becomes unusable, since a large share of traces have no name at all.This is distinct from the isolated tracer provider introduced in 0.8.0 (#77): that change isolates which spans are exported to Langfuse (and it works — thank you for it). This issue is about which span a new span is parented to, which context propagation decides independently of the provider.
Fix
Reset to
OpenTelemetry::Context::ROOTfor the duration of the root span creation only:Nesting is deliberately unaffected: once the span is started it becomes current again, so child observations keep attaching to it, and the caller's ambient context is restored on the way out. The child branch is untouched.
Tests
Three specs under
.start_observation, covering the fix and the two properties a naive fix would break:main, passes with the change);bundle exec rspec spec/langfuse_spec.rb→ 100 examples, 0 failures.rubocopclean on both changed files.Happy to adjust naming, comment length or spec placement to your preferences.
Note
Medium Risk
Changes core root-span parenting in mixed OpenTelemetry setups; incorrect detection of “foreign” vs Langfuse ambient spans could mis-nest traces, though the scope check and new specs narrow the blast radius.
Overview
Fixes root Langfuse observations being parented to the host app’s unrelated OpenTelemetry span when Rack/ActiveJob/Sidekiq (or another provider) already has
Context.currentset. Those roots showed up in Langfuse as orphan children under empty, unnamed traces.Root span creation in
create_otel_spannow temporarily clears only the active span (invalid span in context) beforestart_span, so a new trace_id is generated without wipingpropagate_attributescontext values. If the ambient span is Langfuse’s own (ambient_langfuse_span?via tracer instrumentation scope), behavior is unchanged so nestedobserve/start_observationcalls still attach.Adds specs for a foreign ambient span: own trace_id, nested children, restored caller context, nested Langfuse observations, and propagated
user.id/session.id.Reviewed by Cursor Bugbot for commit 259943e. Bugbot is set up for automated code reviews on this repo. Configure here.