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", 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"