Skip to content

Commit 4d8b059

Browse files
solnicclaude
andcommitted
feat(tests): add :allowance option to Sentry.Test.setup_sentry/1
Introduces the mechanism that future commits will use to install per-test telemetry handlers for popular libraries (Oban, Broadway). This commit ships only the infrastructure: option parsing, a unique per-test handler ID, automatic detach on test exit, and a generic __handle_allowance_event__/4 handler that calls allow_sentry_reports/2 in response to the configured event. The internal allowance_handlers/1 dispatch is empty — any atom passed under :allowance currently raises a clear ArgumentError naming the unsupported entry. Oban and Broadway clauses land in follow-up commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 01f8f19 commit 4d8b059

2 files changed

Lines changed: 127 additions & 3 deletions

File tree

lib/sentry/test.ex

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ defmodule Sentry.Test do
5252

5353
@moduledoc since: "10.2.0"
5454

55-
@compile {:no_warn_undefined, [Bypass, Plug.Conn]}
55+
@compile {:no_warn_undefined, [Bypass, Plug.Conn, :telemetry]}
5656

5757
@ownership_server Sentry.Test.OwnershipServer
5858

@@ -75,8 +75,13 @@ defmodule Sentry.Test do
7575
7676
## Options
7777
78-
Any extra Sentry config options (e.g., `dedup_events: false`, `traces_sample_rate: 1.0`)
79-
will be forwarded to the test config.
78+
* `:allowance` - a list of integration module atoms to enable automatic
79+
`Sentry.Test.allow_sentry_reports/2` wiring for. The integrations land
80+
in follow-up commits; see the integration-specific sections below for
81+
supported entries.
82+
83+
Any other key is forwarded to the per-test Sentry config (e.g.,
84+
`dedup_events: false`, `traces_sample_rate: 1.0`).
8085
8186
The reserved `:telemetry_processor` option is *not* forwarded to the test
8287
config. Instead, its value (a keyword list) is passed to the per-test
@@ -135,6 +140,7 @@ defmodule Sentry.Test do
135140

136141
{tp_opts, extra_config} = Keyword.pop(extra_config, :telemetry_processor, [])
137142
{collect_envelopes, extra_config} = Keyword.pop(extra_config, :collect_envelopes, false)
143+
{allowance, extra_config} = Keyword.pop(extra_config, :allowance, [])
138144

139145
# Open a per-test Bypass and stub the envelope endpoint
140146
bypass = Bypass.open()
@@ -151,6 +157,8 @@ defmodule Sentry.Test do
151157
bypass_config = [dsn: "http://public:secret@localhost:#{bypass.port}/1"]
152158
setup_collector(bypass_config ++ extra_config)
153159

160+
attach_allowance_handlers(allowance, self())
161+
154162
case collect_envelopes do
155163
false ->
156164
%{bypass: bypass, telemetry_processor: processor_name}
@@ -835,6 +843,81 @@ defmodule Sentry.Test do
835843
end
836844
end
837845

846+
defp ensure_telemetry_loaded! do
847+
unless Code.ensure_loaded?(:telemetry) do
848+
raise """
849+
`:telemetry` is required for the `:allowance` option of Sentry.Test
850+
but is not available. Add it to your test dependencies:
851+
852+
{:telemetry, "~> 1.0", only: [:test]}
853+
"""
854+
end
855+
end
856+
857+
# ── :allowance plumbing ──
858+
#
859+
# Each integration atom (e.g. Oban, Broadway) is mapped by
860+
# allowance_handlers!/1 to one or more {telemetry_event, {module, fun}}
861+
# pairs. Commit 1 ships only the catch-all clause; commits 2 and 3 add
862+
# the integration-specific clauses.
863+
864+
defp attach_allowance_handlers([], _owner_pid), do: :ok
865+
866+
defp attach_allowance_handlers(modules, owner_pid) when is_list(modules) do
867+
ensure_telemetry_loaded!()
868+
Enum.each(modules, &attach_allowance_handler(&1, owner_pid))
869+
end
870+
871+
defp attach_allowance_handler(module, owner_pid) do
872+
case allowance_handlers(module) do
873+
:unknown ->
874+
raise ArgumentError,
875+
"unknown :allowance entry #{inspect(module)}. Supported integrations: " <>
876+
"(none built-in yet — Oban and Broadway land in follow-up commits)"
877+
878+
pairs when is_list(pairs) ->
879+
Enum.each(pairs, fn {event, handler_fun} ->
880+
__attach_allowance__(event, handler_fun, %{owner_pid: owner_pid})
881+
end)
882+
end
883+
end
884+
885+
@doc false
886+
@spec __attach_allowance__([atom()], {module(), atom()}, map()) :: :ok
887+
def __attach_allowance__(event, {module, function}, config)
888+
when is_list(event) and is_atom(module) and is_atom(function) and is_map(config) do
889+
ref = System.unique_integer([:positive])
890+
handler_id = {:sentry_test_allowance, ref}
891+
892+
:ok =
893+
:telemetry.attach(
894+
handler_id,
895+
event,
896+
Function.capture(module, function, 4),
897+
config
898+
)
899+
900+
ExUnit.Callbacks.on_exit(fn -> :telemetry.detach(handler_id) end)
901+
:ok
902+
end
903+
904+
# Generic "allow whatever fired this event for owner_pid" handler. Used
905+
# by the foundation unit tests and available for ad-hoc telemetry
906+
# routing; the Oban / Broadway dispatches use their own handlers that
907+
# consult metadata rather than blindly allowing the emitting pid.
908+
@doc false
909+
def __handle_allowance_event__(_event, _measurements, _metadata, %{owner_pid: owner_pid}) do
910+
allow_sentry_reports(owner_pid, self())
911+
rescue
912+
ArgumentError -> :ok
913+
end
914+
915+
# Returns the list of `{event_path, {module, function}}` handler pairs for
916+
# a given integration atom, or `:unknown` for unsupported entries (the
917+
# caller turns that into an `ArgumentError`). Commits 2 and 3 prepend
918+
# clauses for `Oban` and `Broadway` respectively.
919+
defp allowance_handlers(_other), do: :unknown
920+
838921
# Sets up collection infrastructure (ETS table, before_send wrapping, config)
839922
# without opening a new Bypass. When no :dsn is provided in extra_config,
840923
# falls back to the default Bypass DSN from Registry.

test/sentry/test_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,47 @@ defmodule Sentry.TestTest do
375375
end
376376
end
377377

378+
describe "setup_sentry/1 with :allowance (foundation)" do
379+
test "empty allowance list is a no-op" do
380+
assert %{bypass: _, telemetry_processor: _} =
381+
SentryTest.setup_sentry(allowance: [])
382+
end
383+
384+
test "raises a clear error for unknown allowance entries" do
385+
assert_raise ArgumentError, ~r/unknown :allowance entry/, fn ->
386+
SentryTest.setup_sentry(allowance: [SomeUnknownThing])
387+
end
388+
end
389+
390+
test "__attach_allowance__/3 routes worker events back to the owner" do
391+
SentryTest.setup_sentry()
392+
test_pid = self()
393+
394+
SentryTest.__attach_allowance__(
395+
[:sentry_test_allowance, :synthetic, :start],
396+
{SentryTest, :__handle_allowance_event__},
397+
%{owner_pid: test_pid}
398+
)
399+
400+
worker_done = make_ref()
401+
402+
{:ok, _worker} =
403+
Task.start(fn ->
404+
:telemetry.execute([:sentry_test_allowance, :synthetic, :start], %{}, %{})
405+
406+
assert {:ok, _} =
407+
Sentry.capture_message("hello from synthetic worker", result: :sync)
408+
409+
send(test_pid, worker_done)
410+
end)
411+
412+
assert_receive ^worker_done, 5_000
413+
414+
assert [%Sentry.Event{message: %{formatted: "hello from synthetic worker"}}] =
415+
SentryTest.pop_sentry_reports()
416+
end
417+
end
418+
378419
describe "before_send wrapping" do
379420
test "wraps existing before_send callback" do
380421
test_pid = self()

0 commit comments

Comments
 (0)