From 0d152915255e19c5b677efced3594fb1ad1cdcd7 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 15 Oct 2026 09:27:07 +0000 Subject: [PATCH 1/2] Stop dropping transaction root status --- lib/sentry/opentelemetry/span_processor.ex | 8 +++ .../opentelemetry/span_processor_test.exs | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/lib/sentry/opentelemetry/span_processor.ex b/lib/sentry/opentelemetry/span_processor.ex index e8940591..975190c0 100644 --- a/lib/sentry/opentelemetry/span_processor.ex +++ b/lib/sentry/opentelemetry/span_processor.ex @@ -289,6 +289,11 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do data: data } + # The root span is the transaction, so it is not copied into `spans`. + # Its HTTP status still has to land on contexts.trace; child spans keep + # their own status from build_span/1. Skip the key when there is none. + context = put_trace_status(context, span_status(span_record)) + # Add links if present (for root spans, links go in trace context) if span_record.links != [] do Map.put(context, :links, format_links(span_record.links)) @@ -297,6 +302,9 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do end end + defp put_trace_status(context, nil), do: context + defp put_trace_status(context, status), do: Map.put(context, :status, status) + defp get_op_description( %{ attributes: %{ diff --git a/test/sentry/opentelemetry/span_processor_test.exs b/test/sentry/opentelemetry/span_processor_test.exs index efe891b8..fe026361 100644 --- a/test/sentry/opentelemetry/span_processor_test.exs +++ b/test/sentry/opentelemetry/span_processor_test.exs @@ -730,6 +730,61 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do end end + describe "HTTP transaction status" do + @tag span_storage: true + test "copies the root span HTTP status onto contexts.trace", %{bypass: bypass} do + put_test_config( + environment_name: "test", + traces_sample_rate: 1.0, + traces_ignore_http_status_codes: [] + ) + + # 302 stays "unknown_error": to_status/1 is unchanged. This only checks + # that the status already computed for the root span reaches the transaction. + for {status_code, expected_status} <- [ + {200, "ok"}, + {302, "unknown_error"}, + {404, "not_found"}, + {500, "internal_error"} + ] do + ref = setup_bypass_envelope_collector(bypass) + + Tracer.with_span "GET /api/items", %{ + kind: :server, + attributes: %{ + HTTPAttributes.http_request_method() => :GET, + URLAttributes.url_path() => "/api/items", + HTTPAttributes.http_response_status_code() => status_code + } + } do + Tracer.with_span "GET /downstream", %{ + kind: :client, + attributes: %{ + HTTPAttributes.http_request_method() => :GET, + URLAttributes.url_path() => "/downstream", + HTTPAttributes.http_response_status_code() => 201 + } + } do + Process.sleep(1) + end + end + + [tx] = collect_sentry_transactions(ref, 1) + + assert tx["contexts"]["trace"]["op"] == "http.server" + assert tx["contexts"]["trace"]["span_id"] + assert tx["contexts"]["trace"]["trace_id"] + + assert tx["contexts"]["trace"]["status"] == expected_status, + "HTTP #{status_code}: expected transaction status #{inspect(expected_status)}, got #{inspect(tx["contexts"]["trace"]["status"])}" + + assert [child] = tx["spans"] + assert child["status"] == "ok" + assert child["data"]["http.response.status_code"] == 201 + end + end + end + describe "race condition: parent finishes before child" do @tag span_storage: true test "in-progress child span is preserved and becomes transaction root when parent finishes first", From 5cebaa2e86de303e287fa747c79f031f01adbca5 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Wed, 21 Oct 2026 08:11:41 +0000 Subject: [PATCH 2/2] test(opentelemetry): Collect only transaction envelopes The Phoenix distributed-trace test treated the first envelope as the transaction. Runtime metrics, logs, and client reports share that Bypass and can arrive first, so the assertion saw an empty list. Restrict the collector to transaction envelopes. Co-Authored-By: Grok 4.7 --- .../test/phoenix_app/distributed_traces_test.exs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test_integrations/phoenix_app/test/phoenix_app/distributed_traces_test.exs b/test_integrations/phoenix_app/test/phoenix_app/distributed_traces_test.exs index 278e7402..1d4b79bd 100644 --- a/test_integrations/phoenix_app/test/phoenix_app/distributed_traces_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app/distributed_traces_test.exs @@ -9,7 +9,13 @@ defmodule PhoenixApp.DistributedTracesTest do @remote_span_id String.duplicate("b", 16) setup do - Sentry.Test.setup_sentry(collect_envelopes: true, traces_sample_rate: 1.0) + # Runtime metrics, logs, and client reports share this Bypass. Collecting + # the first envelope of any type drops the transaction when one of those + # arrives first. + Sentry.Test.setup_sentry( + collect_envelopes: [type: "transaction"], + traces_sample_rate: 1.0 + ) end test "work continuing an upstream trace is reported with its instrumented spans", %{ref: ref} do @@ -22,7 +28,7 @@ defmodule PhoenixApp.DistributedTracesTest do end assert [tx] = collect_sentry_transactions(ref, 1, timeout: 500), - "nothing was reported for work continuing an upstream trace" + "nothing was reported for work continuing an upstream trace" assert tx["transaction"] == "etl.sync"