Skip to content

Commit 365fb15

Browse files
committed
wip(tests): restore manual pid allowance via NimbleOwnership
1 parent 363b5f2 commit 365fb15

7 files changed

Lines changed: 386 additions & 48 deletions

File tree

lib/sentry/application.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ defmodule Sentry.Application do
77

88
alias Sentry.Config
99

10+
@compile {:no_warn_undefined, [NimbleOwnership]}
11+
1012
@impl true
1113
def start(_type, _opts) do
1214
config = Config.validate!()
@@ -32,7 +34,14 @@ defmodule Sentry.Application do
3234

3335
maybe_test_registry =
3436
if Config.test_mode?() do
35-
[Sentry.Test.Registry]
37+
if Code.ensure_loaded?(NimbleOwnership) do
38+
[
39+
Sentry.Test.Registry,
40+
{NimbleOwnership, name: Sentry.Test.OwnershipServer}
41+
]
42+
else
43+
[Sentry.Test.Registry]
44+
end
3645
else
3746
[]
3847
end

lib/sentry/test.ex

Lines changed: 134 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/sentry/test/registry.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ defmodule Sentry.Test.Registry do
88
# Bypass and Plug.Conn may not be available at compile time (optional deps).
99
@compile {:no_warn_undefined, [Bypass, Bypass.Instance, Bypass.Supervisor, Plug.Conn]}
1010

11-
@table :sentry_test_collectors
1211
@allows_table :sentry_test_scope_allows
1312

1413
@spec start_link(keyword()) :: GenServer.on_start()
@@ -86,7 +85,6 @@ defmodule Sentry.Test.Registry do
8685

8786
@impl true
8887
def init(nil) do
89-
_table = :ets.new(@table, [:named_table, :public, :set])
9088
_allows_table = :ets.new(@allows_table, [:named_table, :public, :set])
9189
maybe_start_default_bypass()
9290
{:ok, :no_state}

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ defmodule Sentry.Mixfile do
111111
{:plug_cowboy, "~> 2.7", only: [:test]},
112112
{:bandit, "~> 1.0", only: [:test]},
113113
{:bypass, "~> 2.0", only: [:test]},
114+
{:nimble_ownership, "~> 1.0", only: [:test]},
114115
{:dialyxir, "~> 1.0", only: [:test, :dev], runtime: false},
115116
{:ex_doc, "~> 0.29", only: :dev},
116117
{:excoveralls, "~> 0.17.1", only: [:test]},

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
4040
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
4141
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
42+
"nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"},
4243
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
4344
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
4445
"oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},

0 commit comments

Comments
 (0)