From ca069f9df816595b0d78578419fe057210c2a767 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Fri, 23 Oct 2026 08:08:39 +0000 Subject: [PATCH] fix(test): skip unrelated envelopes in typed collectors collect_sentry_transactions/3 counted the first N envelopes, then dropped any that were not transactions. A log, metric, or client report ahead of the transaction made assert_sentry_transaction/2 report a timeout even when the transaction was sent. The same helper pattern applied to events, logs, check-ins, and metrics. Typed collectors now ignore envelopes that do not contain the requested item. Those envelopes do not consume expected_count. The traced-exception integration test no longer asks for two envelopes to look past the exception event. Co-Authored-By: Grok 4.7 --- lib/sentry/test.ex | 63 ++++++++++++++----- lib/sentry/test/assertions.ex | 4 +- test/sentry/test_test.exs | 28 +++++++++ .../controllers/exception_test.exs | 2 +- 4 files changed, 79 insertions(+), 18 deletions(-) diff --git a/lib/sentry/test.ex b/lib/sentry/test.ex index bff6e21f..c8757f9d 100644 --- a/lib/sentry/test.ex +++ b/lib/sentry/test.ex @@ -692,6 +692,39 @@ defmodule Sentry.Test do end end + # Keeps reading until `expected_count` envelopes contain a payload of the + # requested type, or the next envelope does not arrive within `timeout`. + # Other envelopes are discarded: a log or metric posted by a background + # process must not consume a slot the caller reserved for a transaction. + defp collect_typed_envelopes(ref, expected_count, opts, extract_fun) do + timeout = Keyword.get(opts, :timeout, 1000) + do_collect_typed_envelopes(ref, expected_count, timeout, extract_fun, []) + end + + defp do_collect_typed_envelopes(_ref, 0, _timeout, _extract_fun, acc), do: Enum.reverse(acc) + + defp do_collect_typed_envelopes(ref, remaining, timeout, extract_fun, acc) do + receive do + {:bypass_envelope, ^ref, body} -> + case extract_fun.([decode_envelope!(body)]) do + [] -> + do_collect_typed_envelopes(ref, remaining, timeout, extract_fun, acc) + + payloads -> + do_collect_typed_envelopes( + ref, + remaining - 1, + timeout, + extract_fun, + Enum.reverse(payloads, acc) + ) + end + after + timeout -> + Enum.reverse(acc) + end + end + @doc """ Extracts event payloads from decoded envelope item lists. """ @@ -753,8 +786,8 @@ defmodule Sentry.Test do @doc """ Collects events sent through a Bypass envelope collector. - This is a high-level helper combining `collect_envelopes/3` and `extract_events/1`. - Use this instead of `collect_envelopes(ref, count) |> extract_events()`. + Envelopes that do not contain an event are skipped and do not count toward + `expected_count`. `:timeout` is how long to wait for the next envelope. ## Options @@ -770,14 +803,14 @@ defmodule Sentry.Test do @doc since: "13.0.0" @spec collect_sentry_events(reference(), pos_integer(), keyword()) :: [map()] def collect_sentry_events(ref, expected_count, opts \\ []) do - collect_envelopes(ref, expected_count, opts) |> extract_events() + collect_typed_envelopes(ref, expected_count, opts, &extract_events/1) end @doc """ Collects transactions sent through a Bypass envelope collector. - This is a high-level helper combining `collect_envelopes/3` and `extract_transactions/1`. - Use this instead of `collect_envelopes(ref, count) |> extract_transactions()`. + Envelopes that do not contain a transaction are skipped and do not count + toward `expected_count`. `:timeout` is how long to wait for the next envelope. ## Options @@ -793,14 +826,14 @@ defmodule Sentry.Test do @doc since: "13.0.0" @spec collect_sentry_transactions(reference(), pos_integer(), keyword()) :: [map()] def collect_sentry_transactions(ref, expected_count, opts \\ []) do - collect_envelopes(ref, expected_count, opts) |> extract_transactions() + collect_typed_envelopes(ref, expected_count, opts, &extract_transactions/1) end @doc """ Collects log items sent through a Bypass envelope collector. - This is a high-level helper combining `collect_envelopes/3` and `extract_log_items/1`. - Use this instead of `collect_envelopes(ref, count) |> extract_log_items()`. + Envelopes that do not contain a log item are skipped and do not count toward + `expected_count`. `:timeout` is how long to wait for the next envelope. ## Options @@ -816,14 +849,14 @@ defmodule Sentry.Test do @doc since: "13.0.0" @spec collect_sentry_logs(reference(), pos_integer(), keyword()) :: [map()] def collect_sentry_logs(ref, expected_count, opts \\ []) do - collect_envelopes(ref, expected_count, opts) |> extract_log_items() + collect_typed_envelopes(ref, expected_count, opts, &extract_log_items/1) end @doc """ Collects check-in payloads sent through a Bypass envelope collector. - This is a high-level helper combining `collect_envelopes/3` and `extract_check_ins/1`. - Use this instead of manually destructuring `[[{header, body}]]` from `collect_envelopes/3`. + Envelopes that do not contain a check-in are skipped and do not count toward + `expected_count`. `:timeout` is how long to wait for the next envelope. ## Options @@ -840,14 +873,14 @@ defmodule Sentry.Test do @doc since: "13.0.0" @spec collect_sentry_check_ins(reference(), pos_integer(), keyword()) :: [map()] def collect_sentry_check_ins(ref, expected_count, opts \\ []) do - collect_envelopes(ref, expected_count, opts) |> extract_check_ins() + collect_typed_envelopes(ref, expected_count, opts, &extract_check_ins/1) end @doc """ Collects metric batch payloads sent through a Bypass envelope collector. - This is a high-level helper combining `collect_envelopes/3` and `extract_metric_items/1`. - Use this instead of `collect_envelopes(ref, count) |> extract_metric_items()`. + Envelopes that do not contain a metric batch are skipped and do not count + toward `expected_count`. `:timeout` is how long to wait for the next envelope. Each returned map has an `"items"` key containing the individual metric maps. @@ -867,7 +900,7 @@ defmodule Sentry.Test do @doc since: "13.0.0" @spec collect_sentry_metric_items(reference(), pos_integer(), keyword()) :: [map()] def collect_sentry_metric_items(ref, expected_count, opts \\ []) do - collect_envelopes(ref, expected_count, opts) |> extract_metric_items() + collect_typed_envelopes(ref, expected_count, opts, &extract_metric_items/1) end @doc """ diff --git a/lib/sentry/test/assertions.ex b/lib/sentry/test/assertions.ex index a550b3a5..d7671fb3 100644 --- a/lib/sentry/test/assertions.ex +++ b/lib/sentry/test/assertions.ex @@ -290,13 +290,13 @@ defmodule Sentry.Test.Assertions do Collects up to `:count` transaction envelopes via `ref` and returns the first one matching `criteria`. Raises if no transaction matches. - This is shorthand for: + Envelopes that do not contain a transaction are skipped. This is shorthand for: find_sentry_report!(collect_sentry_transactions(ref, count, timeout: timeout), criteria) Reserved options in `criteria`: - * `:count` — max number of envelopes to collect (default: `1`) + * `:count` — max number of transaction envelopes to collect (default: `1`) * `:timeout` — ms to wait for each envelope (default: `#{1000}`) ## Examples diff --git a/test/sentry/test_test.exs b/test/sentry/test_test.exs index 63cfbdc2..bb19f89f 100644 --- a/test/sentry/test_test.exs +++ b/test/sentry/test_test.exs @@ -119,6 +119,26 @@ defmodule Sentry.TestTest do ) end + test "typed collectors skip envelopes that do not contain the requested item" do + %{ref: ref} = SentryTest.setup_sentry(collect_envelopes: true) + + send(self(), {:bypass_envelope, ref, envelope("log", %{"items" => []})}) + send(self(), {:bypass_envelope, ref, envelope("transaction", %{"transaction" => "first"})}) + send(self(), {:bypass_envelope, ref, envelope("trace_metric", %{"items" => []})}) + send(self(), {:bypass_envelope, ref, envelope("transaction", %{"transaction" => "second"})}) + + assert [%{"transaction" => "first"}, %{"transaction" => "second"}] = + SentryTest.collect_sentry_transactions(ref, 2, timeout: 50) + end + + test "typed collectors return what arrived when the next envelope times out" do + %{ref: ref} = SentryTest.setup_sentry(collect_envelopes: true) + + send(self(), {:bypass_envelope, ref, envelope("event", %{"message" => "ignored"})}) + + assert [] = SentryTest.collect_sentry_transactions(ref, 1, timeout: 50) + end + test "collect_envelopes coexists with :telemetry_processor and config options" do %{ref: ref, telemetry_processor: name} = SentryTest.setup_sentry( @@ -617,4 +637,12 @@ defmodule Sentry.TestTest do assert dropped_count == 9 end end + + defp envelope(type, payload) do + json_library = Sentry.Config.json_library() + {:ok, id_line} = Sentry.JSON.encode(%{"event_id" => Sentry.UUID.uuid4_hex()}, json_library) + {:ok, header} = Sentry.JSON.encode(%{"type" => type}, json_library) + {:ok, item} = Sentry.JSON.encode(payload, json_library) + Enum.join([id_line, header, item], "\n") + end end diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs index 58ab5244..a2c8ac87 100644 --- a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs +++ b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs @@ -32,7 +32,7 @@ defmodule Sentry.Integrations.Phoenix.ExceptionTest do original_exception: %RuntimeError{message: "Traced exception"} ) - transaction = find_sentry_transaction!(ref, count: 2, transaction: "process_order") + transaction = find_sentry_transaction!(ref, transaction: "process_order") assert event.contexts.trace.trace_id == transaction["contexts"]["trace"]["trace_id"] assert event.contexts.trace.span_id == transaction["contexts"]["trace"]["span_id"]