Skip to content

Commit 417b096

Browse files
authored
fix(tests): accept nil as the DSN value again (#1044)
* fix(tests): accept `nil` as the DSN value again * fixup: ensure dedupe works fine when ignoring * feat(tests): ensure user callbacks are skipped when dsn is nil in prod/dev * tests: add basic tests for prod mode
1 parent bb5ce27 commit 417b096

14 files changed

Lines changed: 355 additions & 116 deletions

File tree

lib/sentry.ex

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,6 @@ defmodule Sentry do
380380
ClientReport.Sender.record_discarded_events(:event_processor, [event])
381381
:ignored
382382

383-
!Config.dsn() ->
384-
# We still validate options even if we're not sending the event. This aims at catching
385-
# configuration issues during development instead of only when deploying to production.
386-
_options = NimbleOptions.validate!(options, Options.send_event_schema())
387-
:ignored
388-
389383
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
390384
Client.send_event(event, options)
391385

@@ -399,12 +393,6 @@ defmodule Sentry do
399393
included_envs = Config.included_environments()
400394

401395
cond do
402-
!Config.dsn() ->
403-
# We still validate options even if we're not sending the event. This aims at catching
404-
# configuration issues during development instead of only when deploying to production.
405-
_options = NimbleOptions.validate!(options, Options.send_event_schema())
406-
:ignored
407-
408396
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
409397
Client.send_transaction(transaction, options)
410398

@@ -458,13 +446,9 @@ defmodule Sentry do
458446
@spec capture_check_in(keyword()) ::
459447
{:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()}
460448
def capture_check_in(options) when is_list(options) do
461-
if Config.dsn() do
462-
options
463-
|> CheckIn.new()
464-
|> Client.send_check_in(options)
465-
else
466-
:ignored
467-
end
449+
options
450+
|> CheckIn.new()
451+
|> Client.send_check_in(options)
468452
end
469453

470454
@doc ~S"""

lib/sentry/client.ex

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,35 @@ defmodule Sentry.Client do
2727
@max_message_length 8_192
2828

2929
@spec send_check_in(CheckIn.t(), keyword()) ::
30-
{:ok, check_in_id :: String.t()} | {:error, ClientError.t()}
30+
{:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()}
3131
def send_check_in(%CheckIn{} = check_in, opts) when is_list(opts) do
32-
if Config.telemetry_processor_category?(:check_in) do
33-
case TelemetryProcessor.add(check_in) do
34-
{:ok, {:rate_limited, data_category}} ->
35-
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)
32+
cond do
33+
is_nil(Config.dsn()) ->
34+
:ignored
3635

37-
:ok ->
38-
:ok
39-
end
36+
Config.telemetry_processor_category?(:check_in) ->
37+
case TelemetryProcessor.add(check_in) do
38+
{:ok, {:rate_limited, data_category}} ->
39+
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)
4040

41-
{:ok, check_in.check_in_id}
42-
else
43-
client = Keyword.get_lazy(opts, :client, &Config.client/0)
41+
:ok ->
42+
:ok
43+
end
4444

45-
# This is a "private" option, only really used in testing.
46-
request_retries =
47-
Keyword.get_lazy(opts, :request_retries, fn ->
48-
Application.get_env(:sentry, :request_retries, Transport.default_retries())
49-
end)
45+
{:ok, check_in.check_in_id}
46+
47+
true ->
48+
client = Keyword.get_lazy(opts, :client, &Config.client/0)
5049

51-
check_in
52-
|> Envelope.from_check_in()
53-
|> Transport.encode_and_post_envelope(client, request_retries)
50+
# This is a "private" option, only really used in testing.
51+
request_retries =
52+
Keyword.get_lazy(opts, :request_retries, fn ->
53+
Application.get_env(:sentry, :request_retries, Transport.default_retries())
54+
end)
55+
56+
check_in
57+
|> Envelope.from_check_in()
58+
|> Transport.encode_and_post_envelope(client, request_retries)
5459
end
5560
end
5661

@@ -59,6 +64,7 @@ defmodule Sentry.Client do
5964
@spec send_event(Event.t(), keyword()) ::
6065
{:ok, event_id :: String.t()}
6166
| {:error, ClientError.t()}
67+
| :ignored
6268
| :unsampled
6369
| :excluded
6470
def send_event(%Event{} = event, opts) when is_list(opts) do
@@ -78,6 +84,7 @@ defmodule Sentry.Client do
7884

7985
result =
8086
with {:ok, %Event{} = event} <- maybe_call_before_send(event, before_send),
87+
:ok <- ensure_dsn_configured(),
8188
:ok <- sample_event(sample_rate),
8289
:ok <- maybe_dedupe(event) do
8390
send_result = encode_and_send(event, result_type, client, request_retries)
@@ -99,6 +106,9 @@ defmodule Sentry.Client do
99106
:excluded ->
100107
:excluded
101108

109+
:ignored ->
110+
:ignored
111+
102112
{:error, %ClientError{} = error} ->
103113
{:error, error}
104114
end
@@ -120,6 +130,7 @@ defmodule Sentry.Client do
120130
@spec send_transaction(Transaction.t(), keyword()) ::
121131
{:ok, transaction_id :: String.t()}
122132
| {:error, ClientError.t()}
133+
| :ignored
123134
| :excluded
124135
def send_transaction(%Transaction{} = transaction, opts \\ []) do
125136
opts = NimbleOptions.validate!(opts, Options.send_transaction_schema())
@@ -134,16 +145,24 @@ defmodule Sentry.Client do
134145
Application.get_env(:sentry, :request_retries, Transport.default_retries())
135146
end)
136147

137-
with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
148+
with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send),
149+
:ok <- ensure_dsn_configured() do
138150
send_result = encode_and_send(transaction, result_type, client, request_retries)
139151
_ignored = maybe_call_after_send(transaction, send_result, after_send_event)
140152
send_result
141153
else
142154
:excluded ->
143155
:excluded
156+
157+
:ignored ->
158+
:ignored
144159
end
145160
end
146161

162+
defp ensure_dsn_configured do
163+
if Config.dsn(), do: :ok, else: :ignored
164+
end
165+
147166
defp sample_event(sample_rate) do
148167
cond do
149168
sample_rate == 1 -> :ok

lib/sentry/config.ex

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,7 @@ defmodule Sentry.Config do
929929
def hackney_opts, do: fetch!(:hackney_opts)
930930

931931
@spec before_send() :: (Sentry.Event.t() -> Sentry.Event.t()) | {module(), atom()} | nil
932-
def before_send, do: get(:before_send)
932+
def before_send, do: compose_send_callback(:before_send)
933933

934934
@spec after_send_event() ::
935935
(Sentry.Event.t(), term() -> Sentry.Event.t()) | {module(), atom()} | nil
@@ -1028,11 +1028,49 @@ defmodule Sentry.Config do
10281028

10291029
@spec before_send_log() ::
10301030
(Sentry.LogEvent.t() -> Sentry.LogEvent.t() | nil | false) | {module(), atom()} | nil
1031-
def before_send_log, do: get(:before_send_log)
1031+
def before_send_log, do: compose_send_callback(:before_send_log)
10321032

10331033
@spec before_send_metric() ::
10341034
(Sentry.Metric.t() -> Sentry.Metric.t() | nil | false) | {module(), atom()} | nil
1035-
def before_send_metric, do: get(:before_send_metric)
1035+
def before_send_metric, do: compose_send_callback(:before_send_metric)
1036+
1037+
# Composes the user-provided callback (under `key`) with an internal callback
1038+
# (under `internal_<key>`). In production/development the user-provided
1039+
# callback is dropped when there is no DSN, so callbacks never run for events
1040+
# that won't be sent. In test mode the user-provided callback is always
1041+
# honored — tests routinely use `dsn: nil` to assert callback behavior in
1042+
# isolation, and dropping there would break those contracts.
1043+
defp compose_send_callback(key) do
1044+
user_callback = if user_callbacks_enabled?(), do: get(key), else: nil
1045+
internal_callback = get(internal_callback_key(key))
1046+
1047+
case {user_callback, internal_callback} do
1048+
{nil, nil} -> nil
1049+
{user, nil} -> user
1050+
{nil, internal} -> internal
1051+
{user, internal} -> chain_send_callbacks(user, internal)
1052+
end
1053+
end
1054+
1055+
defp user_callbacks_enabled? do
1056+
not is_nil(dsn()) or test_mode?()
1057+
end
1058+
1059+
defp internal_callback_key(:before_send), do: :_internal_before_send
1060+
defp internal_callback_key(:before_send_log), do: :_internal_before_send_log
1061+
defp internal_callback_key(:before_send_metric), do: :_internal_before_send_metric
1062+
1063+
defp chain_send_callbacks(first, second) do
1064+
fn struct ->
1065+
case apply_send_callback(first, struct) do
1066+
result when result == nil or result == false -> result
1067+
result -> apply_send_callback(second, result)
1068+
end
1069+
end
1070+
end
1071+
1072+
defp apply_send_callback(fun, struct) when is_function(fun, 1), do: fun.(struct)
1073+
defp apply_send_callback({mod, fun}, struct), do: apply(mod, fun, [struct])
10361074

10371075
@spec put_config(atom(), term()) :: :ok
10381076
def put_config(key, value) when is_atom(key) do

lib/sentry/test.ex

Lines changed: 16 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -650,30 +650,6 @@ defmodule Sentry.Test do
650650
# Store in process dict for pop_* lookups
651651
Process.put(:sentry_test_collector, collector_table)
652652

653-
# Extract user-provided callbacks from extra_config (if any), falling back to current config
654-
{user_before_send, extra_config} = Keyword.pop(extra_config, :before_send)
655-
{user_before_send_event, extra_config} = Keyword.pop(extra_config, :before_send_event)
656-
{user_before_send_log, extra_config} = Keyword.pop(extra_config, :before_send_log)
657-
{user_before_send_metric, extra_config} = Keyword.pop(extra_config, :before_send_metric)
658-
659-
# Use the caller-only registry lookup instead of `Sentry.Config.before_send/0`
660-
# so the captured "original" callback is only this test's override (or the
661-
# global default), never another concurrent test's wrapping callback.
662-
original_before_send =
663-
user_before_send || user_before_send_event ||
664-
original_config_value(:before_send)
665-
666-
original_before_send_log =
667-
user_before_send_log || original_config_value(:before_send_log)
668-
669-
original_before_send_metric =
670-
user_before_send_metric || original_config_value(:before_send_metric)
671-
672-
# Build collecting callbacks that wrap the originals
673-
new_before_send = build_collecting_callback(original_before_send)
674-
new_before_send_log = build_collecting_callback(original_before_send_log)
675-
new_before_send_metric = build_collecting_callback(original_before_send_metric)
676-
677653
# Always set a per-test DSN override. When no DSN is provided, use the
678654
# default Bypass DSN.
679655
extra_config =
@@ -686,17 +662,20 @@ defmodule Sentry.Test do
686662
end
687663
end
688664

689-
config =
690-
[finch_request_opts: [receive_timeout: 2000]]
691-
|> Keyword.merge(extra_config)
692-
|> Keyword.merge(
693-
before_send: new_before_send,
694-
before_send_log: new_before_send_log,
695-
before_send_metric: new_before_send_metric
696-
)
665+
config = Keyword.merge([finch_request_opts: [receive_timeout: 2000]], extra_config)
697666

698667
put_test_config(config)
699668

669+
# Install standalone collecting callbacks under internal slots. They're
670+
# composed with the user-provided :before_send / :before_send_log /
671+
# :before_send_metric in `Sentry.Config` based on DSN value: when DSN is
672+
# `nil`, only these collecting callbacks run; when DSN is set, the user's
673+
# callback runs first and its result is collected.
674+
collector = build_collecting_callback()
675+
Sentry.Test.Config.put_override(:_internal_before_send, collector)
676+
Sentry.Test.Config.put_override(:_internal_before_send_log, collector)
677+
Sentry.Test.Config.put_override(:_internal_before_send_metric, collector)
678+
700679
scheduler_pid = get_scheduler_pid()
701680

702681
if scheduler_pid do
@@ -752,18 +731,11 @@ defmodule Sentry.Test do
752731
end)
753732
end
754733

755-
# Reads `key` from this test's per-process scope (or any caller's scope on
756-
# `[self() | $callers]`), falling back to the global config value. Skips the
757-
# full namespace resolver so the captured "original" callback is never
758-
# another concurrent test's wrapping callback.
759-
defp original_config_value(key) do
760-
case Sentry.Test.Scope.Registry.lookup_caller_override(key) do
761-
{:ok, value} -> value
762-
:default -> :persistent_term.get({:sentry_config, key}, nil)
763-
end
764-
end
765-
766-
defp build_collecting_callback(nil) do
734+
# Standalone collecting callback. Records the struct in this test's
735+
# collector ETS table when one is registered for the calling process (or any
736+
# of its callers), then returns the struct unchanged so it flows through any
737+
# remaining pipeline stages.
738+
defp build_collecting_callback do
767739
fn struct ->
768740
case find_collector() do
769741
nil -> :ok
@@ -774,28 +746,6 @@ defmodule Sentry.Test do
774746
end
775747
end
776748

777-
defp build_collecting_callback({mod, fun}) do
778-
build_collecting_callback(Function.capture(mod, fun, 1))
779-
end
780-
781-
defp build_collecting_callback(original) when is_function(original, 1) do
782-
fn struct ->
783-
# Guard on find_collector/0 so that a test-specific callback stored in
784-
# :persistent_term is never invoked from an unrelated async test's process.
785-
# When a collector IS found, call the original first so user-defined
786-
# filtering/modification is applied before we collect the result.
787-
case find_collector() do
788-
nil ->
789-
struct
790-
791-
table ->
792-
result = original.(struct)
793-
unless is_nil(result), do: collect_struct(table, result)
794-
result
795-
end
796-
end
797-
end
798-
799749
defp collect_struct(table, struct) do
800750
:ets.insert(table, {System.unique_integer([:monotonic]), struct})
801751
end

lib/sentry/test/config.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ defmodule Sentry.Test.Config do
146146
Registry.strict_allow!(owner_pid, allowed_pid)
147147
end
148148

149+
@doc false
150+
@spec put_override(atom(), term()) :: :ok
151+
def put_override(key, value) when is_atom(key) do
152+
_ =
153+
Registry.update(self(), fn scope ->
154+
Scope.put_override(scope, key, value)
155+
end)
156+
157+
auto_allow_globals()
158+
159+
:ok
160+
end
161+
149162
## Private helpers
150163

151164
defp validate_and_rename({key, value}) do

0 commit comments

Comments
 (0)