Skip to content

Error events carry no trace context, so an issue is never linked to the trace it happened in #1174

Description

@dustinfarris

Summary

Error events are the only event type the SDK sends without trace context. Sentry.Event.create_event/1 sets contexts: to generate_contexts(), which returns %{os: ..., runtime: ...} and nothing else, and no later stage adds a :trace key. So an exception captured inside an active span arrives at Sentry with no link to the trace it happened in, even when the SDK itself exported that trace over the same transport moments earlier.

The visible symptom is that the issue's trace_id equals its event_id — Relay fabricates a trace id from the event id when the payload carries none. In the UI the issue's "Trace" tab shows only itself, and none of the spans, logs or other errors from the same request appear.

Why this looks like an oversight rather than a decision

Three of the four event types the SDK sends already carry trace context, and the fourth documents it as the way to reach errors:

  • Transactions carry it: Sentry.Transaction's typespec makes contexts.trace required, and Sentry.OpenTelemetry.SpanProcessor.build_trace_context/1 fills it in.
  • Logs carry it: Sentry.LogEvent.extract_trace_context/1 reads trace_id/span_id out of logger metadata (as written there by opentelemetry_logger_metadata), and falls back to Sentry.UUID.uuid4_hex() when absent.
  • Check-ins carry it, and the option's own docs say why: "right now Sentry supports the trace_id key under the trace context to connect the check-in with related errors" (Sentry.CheckIn, the :contexts schema entry).
  • Errors carry nothing.

So the SDK will happily connect a cron check-in to related errors, but the errors themselves cannot be connected to anything. The docs for check-ins describe a linkage that the error side of does not exist.

There is also no way for a user to supply it: Sentry.Context.context_keys/0 is [:breadcrumbs, :tags, :user, :extra, :request, :attachments], with no trace slot, so Sentry.Context cannot be used to work around this either.

Reproduction

With tracing configured (Sentry.OpenTelemetry.SpanProcessor + Sentry.OpenTelemetry.Sampler, traces_sample_rate: 1.0):

require OpenTelemetry.Tracer, as: Tracer

Tracer.with_span "repro" do
  # The SDK knows the trace here — it is about to export it as a transaction.
  IO.inspect(:otel_span.hex_trace_id(Tracer.current_span_ctx()), label: "active trace")

  event = Sentry.Event.create_event(message: "hello")
  IO.inspect(event.contexts, label: "contexts")
  # => contexts: %{os: %{...}, runtime: %{...}}   <- no :trace

  Sentry.capture_message("hello")
end

In Sentry, the resulting issue's trace_id is its own event_id, not the trace id printed above.

Expected

An event captured while a valid (non-zero, non-noop) span context is in scope should go out with contexts.trace set from that span — at minimum trace_id and span_id, formatted as the transaction path already formats them, so the two agree. With no active span, behaviour should stay exactly as it is today.

This looks cheap to do centrally: :before_send already runs synchronously in the capturing process (Sentry.Client.send_event/2maybe_call_before_send/2function.(event), no task or spawn), and both the plug capture path and the crash-report handler run in the process that owns the span, so the span context is simply in scope at event-creation time.

Workaround

For anyone else hitting this, a :before_send callback covers it in about ten lines:

def call(%Sentry.Event{} = event) do
  ctx = :otel_tracer.current_span_ctx()

  if :otel_span.is_valid(ctx) do
    trace = %{trace_id: :otel_span.hex_trace_id(ctx), span_id: :otel_span.hex_span_id(ctx)}
    %{event | contexts: Map.put(event.contexts || %{}, :trace, trace)}
  else
    event
  end
end

def call(other), do: other

Two notes for anyone copying it: contexts is nil on a hand-built %Sentry.Event{} (the struct default — only create_event/1 fills it in), and the callback also receives transactions, so it has to pass through anything that is not an %Sentry.Event{} or it takes tracing down with it.

Versions

Verified against sentry 12.0.3 from Hex, reading lib/sentry/event.ex, lib/sentry/context.ex, lib/sentry/client.ex, lib/sentry/plug_capture.ex and lib/sentry/opentelemetry/. I also checked master: create_event/1 still assigns contexts: generate_contexts(), generate_contexts/0 still returns os and runtime only, and client.ex still attaches no trace on the send path. I have not run 13.4.2 directly, so if this was addressed somewhere I did not look, apologies for the noise.

I could not find an existing issue covering this — #1011 and #1163 both concern spans going missing from traces, which is a different failure.

Happy to open a PR if you would like it fixed the way described above, or a different way if you would rather it live somewhere other than create_event/1.

Metadata

Metadata

Assignees

Projects

Status
Waiting for: Product Owner

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions