@@ -9,11 +9,12 @@ defmodule Sentry.Test do
99 Events are captured via the existing `before_send` and `before_send_log` callbacks
1010 and stored in an isolated ETS table per test, preserving the full struct data.
1111
12- > #### Bypass Required {: .info}
12+ > #### Bypass and NimbleOwnership Required {: .info}
1313 >
14- > This module requires `bypass` as a test dependency :
14+ > This module requires `bypass` and `nimble_ownership` as test dependencies :
1515 >
1616 > {:bypass, "~> 2.0", only: [:test]}
17+ > {:nimble_ownership, "~> 1.0", only: [:test]}
1718
1819 ## Examples
1920
@@ -52,9 +53,10 @@ defmodule Sentry.Test do
5253
5354 @ moduledoc since: "10.2.0"
5455
55- @ compile { :no_warn_undefined , [ Bypass , Plug.Conn ] }
56+ @ compile { :no_warn_undefined , [ Bypass , Plug.Conn , NimbleOwnership ] }
5657
57- @ registry_table :sentry_test_collectors
58+ @ ownership_server Sentry.Test.OwnershipServer
59+ @ collector_key :sentry_test_collector
5860
5961 # Public API
6062
@@ -108,6 +110,7 @@ defmodule Sentry.Test do
108110 @ spec setup_sentry ( keyword ( ) ) :: % { bypass: term ( ) , telemetry_processor: atom ( ) }
109111 def setup_sentry ( extra_config \\ [ ] ) do
110112 ensure_bypass_loaded! ( )
113+ ensure_nimble_ownership_loaded! ( )
111114
112115 # Open a per-test Bypass and stub the envelope endpoint
113116 bypass = Bypass . open ( )
@@ -251,19 +254,95 @@ defmodule Sentry.Test do
251254 end
252255
253256 @ doc """
254- Allows `pid_to_allow` to collect events back to the root process via `owner_pid`.
257+ Allows `pid_to_allow` to collect events back to `owner_pid`'s test scope.
258+
259+ Use this when an unrelated process — one that does not appear in the
260+ current test's `$callers` chain — needs to have its captured events
261+ routed into this test's collector. Typical examples include Broadway
262+ workers, processes started by `phoenix_test_playwright`, or
263+ long-lived `GenServer`s that outlive the calling test process.
264+
265+ `pid_to_allow` may be a pid or a zero-arity function returning a pid;
266+ the function form is resolved on call and is convenient when the pid
267+ is not known until later.
268+
269+ This function is idempotent for the same `owner_pid`. It raises
270+ `ArgumentError` when `owner_pid` has not yet called `setup_sentry/1`
271+ (or `start_collecting_sentry_reports/0`), and raises when a different
272+ live test scope already owns `pid_to_allow`.
273+
274+ Cleanup is automatic: allow entries are removed when the test exits
275+ via the same `on_exit` callback registered by `setup_sentry/1`.
276+
277+ ## Example
278+
279+ setup do
280+ Sentry.Test.setup_sentry()
281+ end
282+
283+ test "events from a Broadway worker are captured" do
284+ {:ok, worker_pid} = MyApp.Worker.start_link()
285+ :ok = Sentry.Test.allow_sentry_reports(self(), worker_pid)
286+
287+ send(worker_pid, :do_work_that_reports)
288+
289+ assert_receive {:done, _}
290+ assert [%Sentry.Event{}] = Sentry.Test.pop_sentry_reports()
291+ end
255292
256- > #### Deprecated {: .warning}
257- >
258- > This function is deprecated and will be removed in v13.0.0.
259- > Child processes are automatically tracked via the `$callers` mechanism.
260- > There is no need to explicitly allow processes.
261293 """
262- @ doc since: "10.2.0"
263- @ doc deprecated: "Child processes are now automatically tracked via $callers"
294+ @ doc since: "13.0.2"
264295 @ spec allow_sentry_reports ( pid ( ) , pid ( ) | ( -> pid ( ) ) ) :: :ok
265- def allow_sentry_reports ( _owner_pid , _pid_to_allow ) do
266- :ok
296+ def allow_sentry_reports ( owner_pid , pid_or_fun ) when is_pid ( owner_pid ) do
297+ ensure_nimble_ownership_loaded! ( )
298+ allowed_pid = resolve_allowed_pid ( pid_or_fun )
299+
300+ case NimbleOwnership . allow ( @ ownership_server , owner_pid , allowed_pid , @ collector_key ) do
301+ :ok ->
302+ # Also route per-test config overrides (DSN, before_send hooks,
303+ # the internal collector callback, etc.) through the owner's
304+ # scope so that Sentry callbacks invoked from `allowed_pid`
305+ # resolve to the same configuration the test set up.
306+ Sentry.Test.Config . allow ( owner_pid , allowed_pid )
307+ :ok
308+
309+ { :error , % { reason: { :already_allowed , ^ owner_pid } } } ->
310+ # Idempotent re-allow under the same owner.
311+ Sentry.Test.Config . allow ( owner_pid , allowed_pid )
312+ :ok
313+
314+ { :error , % { reason: { :already_allowed , existing_owner } } } ->
315+ raise ArgumentError ,
316+ "cannot allow #{ inspect ( allowed_pid ) } for #{ inspect ( owner_pid ) } : " <>
317+ "already allowed by another live test scope " <>
318+ "(owner: #{ inspect ( existing_owner ) } )"
319+
320+ { :error , % { reason: :not_allowed } } ->
321+ raise ArgumentError ,
322+ "owner #{ inspect ( owner_pid ) } is not collecting Sentry reports; " <>
323+ "call Sentry.Test.setup_sentry/1 or " <>
324+ "Sentry.Test.start_collecting_sentry_reports/0 first"
325+
326+ { :error , % { reason: :already_an_owner } } ->
327+ raise ArgumentError ,
328+ "cannot allow #{ inspect ( allowed_pid ) } for #{ inspect ( owner_pid ) } : " <>
329+ "#{ inspect ( allowed_pid ) } is already collecting Sentry reports " <>
330+ "itself (called setup_sentry/1 or start_collecting_sentry_reports/0)"
331+ end
332+ end
333+
334+ defp resolve_allowed_pid ( pid ) when is_pid ( pid ) , do: pid
335+
336+ defp resolve_allowed_pid ( fun ) when is_function ( fun , 0 ) do
337+ case fun . ( ) do
338+ pid when is_pid ( pid ) ->
339+ pid
340+
341+ other ->
342+ raise ArgumentError ,
343+ "expected the function passed to allow_sentry_reports/2 to return a pid, " <>
344+ "got: #{ inspect ( other ) } "
345+ end
267346 end
268347
269348 @ doc """
@@ -636,16 +715,39 @@ defmodule Sentry.Test do
636715 end
637716 end
638717
718+ defp ensure_nimble_ownership_loaded! do
719+ unless Code . ensure_loaded? ( NimbleOwnership ) do
720+ raise """
721+ NimbleOwnership is required for Sentry.Test but is not available.
722+
723+ Add it to your test dependencies in mix.exs:
724+
725+ {:nimble_ownership, "~> 1.0", only: [:test]}
726+ """
727+ end
728+ end
729+
639730 # Sets up collection infrastructure (ETS table, before_send wrapping, config)
640731 # without opening a new Bypass. When no :dsn is provided in extra_config,
641732 # falls back to the default Bypass DSN from Registry.
642733 defp setup_collector ( extra_config ) do
734+ ensure_nimble_ownership_loaded! ( )
735+
643736 uid = System . unique_integer ( [ :positive ] )
644737 collector_table = :"sentry_test_collector_#{ uid } "
645738 :ets . new ( collector_table , [ :ordered_set , :public , :named_table ] )
646739
647- # Register this test's collector
648- :ets . insert ( @ registry_table , { self ( ) , collector_table } )
740+ # Register this test as the NimbleOwnership owner of the collector key,
741+ # with the collector ETS table as its metadata. NimbleOwnership monitors
742+ # the owner pid and auto-cleans the key + every transitive allowance
743+ # when the test process exits.
744+ { :ok , _ } =
745+ NimbleOwnership . get_and_update (
746+ @ ownership_server ,
747+ self ( ) ,
748+ @ collector_key ,
749+ fn _prev -> { :ok , collector_table } end
750+ )
649751
650752 # Store in process dict for pop_* lookups
651753 Process . put ( :sentry_test_collector , collector_table )
@@ -676,30 +778,19 @@ defmodule Sentry.Test do
676778 Sentry.Test.Config . put_override ( :_internal_before_send_log , collector )
677779 Sentry.Test.Config . put_override ( :_internal_before_send_metric , collector )
678780
781+ # The TelemetryProcessor's scheduler is not in `$callers` of this test —
782+ # allow it explicitly so log/metric events routed through the buffered
783+ # pipeline can find this test's collector.
679784 scheduler_pid = get_scheduler_pid ( )
680785
681786 if scheduler_pid do
682- :ets . insert_new ( @ registry_table , { scheduler_pid , collector_table } )
787+ :ok =
788+ NimbleOwnership . allow ( @ ownership_server , self ( ) , scheduler_pid , @ collector_key )
683789 end
684790
685- # Register cleanup
686- test_pid = self ( )
687-
791+ # Register cleanup for the collector ETS table only. NimbleOwnership
792+ # cleans up the key and allowances automatically when this test exits.
688793 ExUnit.Callbacks . on_exit ( fn ->
689- if :ets . whereis ( @ registry_table ) != :undefined do
690- :ets . delete ( @ registry_table , test_pid )
691-
692- if scheduler_pid do
693- case :ets . lookup ( @ registry_table , scheduler_pid ) do
694- [ { ^ scheduler_pid , ^ collector_table } ] ->
695- :ets . delete ( @ registry_table , scheduler_pid )
696-
697- _ ->
698- :ok
699- end
700- end
701- end
702-
703794 if :ets . whereis ( collector_table ) != :undefined do
704795 :ets . delete ( collector_table )
705796 end
@@ -723,12 +814,15 @@ defmodule Sentry.Test do
723814 defp find_collector do
724815 pids = [ self ( ) | Process . get ( :"$callers" , [ ] ) ]
725816
726- Enum . find_value ( pids , fn pid ->
727- case :ets . lookup ( @ registry_table , pid ) do
728- [ { ^ pid , table } ] -> table
729- [ ] -> nil
730- end
731- end )
817+ case NimbleOwnership . fetch_owner ( @ ownership_server , pids , @ collector_key ) do
818+ { :ok , owner_pid } ->
819+ @ ownership_server
820+ |> NimbleOwnership . get_owned ( owner_pid , % { } )
821+ |> Map . get ( @ collector_key )
822+
823+ :error ->
824+ nil
825+ end
732826 end
733827
734828 # Standalone collecting callback. Records the struct in this test's
0 commit comments