From eb5615aa585392621313000fc9815ece9699b6c8 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 29 Apr 2026 10:03:54 +0000 Subject: [PATCH 1/4] fix(tests): accept `nil` as the DSN value again --- lib/sentry.ex | 22 ++------ lib/sentry/client.ex | 61 +++++++++++++++-------- test/sentry/test_backward_compat_test.exs | 43 ++++++++++++++++ 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/lib/sentry.ex b/lib/sentry.ex index f6d1ed11..133c5b82 100644 --- a/lib/sentry.ex +++ b/lib/sentry.ex @@ -380,12 +380,6 @@ defmodule Sentry do ClientReport.Sender.record_discarded_events(:event_processor, [event]) :ignored - !Config.dsn() -> - # We still validate options even if we're not sending the event. This aims at catching - # configuration issues during development instead of only when deploying to production. - _options = NimbleOptions.validate!(options, Options.send_event_schema()) - :ignored - included_envs == :all or to_string(Config.environment_name()) in included_envs -> Client.send_event(event, options) @@ -399,12 +393,6 @@ defmodule Sentry do included_envs = Config.included_environments() cond do - !Config.dsn() -> - # We still validate options even if we're not sending the event. This aims at catching - # configuration issues during development instead of only when deploying to production. - _options = NimbleOptions.validate!(options, Options.send_event_schema()) - :ignored - included_envs == :all or to_string(Config.environment_name()) in included_envs -> Client.send_transaction(transaction, options) @@ -458,13 +446,9 @@ defmodule Sentry do @spec capture_check_in(keyword()) :: {:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()} def capture_check_in(options) when is_list(options) do - if Config.dsn() do - options - |> CheckIn.new() - |> Client.send_check_in(options) - else - :ignored - end + options + |> CheckIn.new() + |> Client.send_check_in(options) end @doc ~S""" diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index e9ebd266..1171915d 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -27,30 +27,35 @@ defmodule Sentry.Client do @max_message_length 8_192 @spec send_check_in(CheckIn.t(), keyword()) :: - {:ok, check_in_id :: String.t()} | {:error, ClientError.t()} + {:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()} def send_check_in(%CheckIn{} = check_in, opts) when is_list(opts) do - if Config.telemetry_processor_category?(:check_in) do - case TelemetryProcessor.add(check_in) do - {:ok, {:rate_limited, data_category}} -> - ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category) + cond do + is_nil(Config.dsn()) -> + :ignored - :ok -> - :ok - end + Config.telemetry_processor_category?(:check_in) -> + case TelemetryProcessor.add(check_in) do + {:ok, {:rate_limited, data_category}} -> + ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category) - {:ok, check_in.check_in_id} - else - client = Keyword.get_lazy(opts, :client, &Config.client/0) + :ok -> + :ok + end - # This is a "private" option, only really used in testing. - request_retries = - Keyword.get_lazy(opts, :request_retries, fn -> - Application.get_env(:sentry, :request_retries, Transport.default_retries()) - end) + {:ok, check_in.check_in_id} + + true -> + client = Keyword.get_lazy(opts, :client, &Config.client/0) - check_in - |> Envelope.from_check_in() - |> Transport.encode_and_post_envelope(client, request_retries) + # This is a "private" option, only really used in testing. + request_retries = + Keyword.get_lazy(opts, :request_retries, fn -> + Application.get_env(:sentry, :request_retries, Transport.default_retries()) + end) + + check_in + |> Envelope.from_check_in() + |> Transport.encode_and_post_envelope(client, request_retries) end end @@ -59,6 +64,7 @@ defmodule Sentry.Client do @spec send_event(Event.t(), keyword()) :: {:ok, event_id :: String.t()} | {:error, ClientError.t()} + | :ignored | :unsampled | :excluded def send_event(%Event{} = event, opts) when is_list(opts) do @@ -79,7 +85,8 @@ defmodule Sentry.Client do result = with {:ok, %Event{} = event} <- maybe_call_before_send(event, before_send), :ok <- sample_event(sample_rate), - :ok <- maybe_dedupe(event) do + :ok <- maybe_dedupe(event), + :ok <- ensure_dsn_configured() do send_result = encode_and_send(event, result_type, client, request_retries) _ignored = maybe_call_after_send(event, send_result, after_send_event) send_result @@ -99,6 +106,9 @@ defmodule Sentry.Client do :excluded -> :excluded + :ignored -> + :ignored + {:error, %ClientError{} = error} -> {:error, error} end @@ -120,6 +130,7 @@ defmodule Sentry.Client do @spec send_transaction(Transaction.t(), keyword()) :: {:ok, transaction_id :: String.t()} | {:error, ClientError.t()} + | :ignored | :excluded def send_transaction(%Transaction{} = transaction, opts \\ []) do opts = NimbleOptions.validate!(opts, Options.send_transaction_schema()) @@ -134,16 +145,24 @@ defmodule Sentry.Client do Application.get_env(:sentry, :request_retries, Transport.default_retries()) end) - with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do + with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send), + :ok <- ensure_dsn_configured() do send_result = encode_and_send(transaction, result_type, client, request_retries) _ignored = maybe_call_after_send(transaction, send_result, after_send_event) send_result else :excluded -> :excluded + + :ignored -> + :ignored end end + defp ensure_dsn_configured do + if Config.dsn(), do: :ok, else: :ignored + end + defp sample_event(sample_rate) do cond do sample_rate == 1 -> :ok diff --git a/test/sentry/test_backward_compat_test.exs b/test/sentry/test_backward_compat_test.exs index b8d1d9ed..1611065f 100644 --- a/test/sentry/test_backward_compat_test.exs +++ b/test/sentry/test_backward_compat_test.exs @@ -74,4 +74,47 @@ defmodule Sentry.TestBackwardCompatTest do assert [] == SentryTest.pop_sentry_reports() end end + + describe "with dsn: nil" do + setup do + assert :ok = SentryTest.start_collecting_sentry_reports() + Sentry.Test.Config.put(dsn: nil) + :ok + end + + test "capture_exception/2 returns :ignored and routes the event to the collector" do + assert :ignored = Sentry.capture_exception(%RuntimeError{message: "boom"}, result: :sync) + + assert [%Sentry.Event{} = event] = SentryTest.pop_sentry_reports() + assert event.original_exception == %RuntimeError{message: "boom"} + end + + test "capture_message/2 returns :ignored and routes the event to the collector" do + assert :ignored = Sentry.capture_message("hello world", result: :sync) + + assert [%Sentry.Event{} = event] = SentryTest.pop_sentry_reports() + assert event.message.formatted == "hello world" + end + + test "send_transaction/2 returns :ignored and routes the transaction to the collector" do + transaction = + Sentry.Transaction.new(%{ + span_id: "nil-dsn-span", + start_timestamp: "2025-01-01T00:00:00Z", + timestamp: "2025-01-02T00:00:00Z", + contexts: %{trace: %{trace_id: "nil-dsn-trace", span_id: "nil-dsn-span"}}, + spans: [] + }) + + assert :ignored = Sentry.send_transaction(transaction, result: :sync) + + assert [%Sentry.Transaction{} = collected] = SentryTest.pop_sentry_transactions() + assert collected.span_id == "nil-dsn-span" + end + + test "capture_check_in/1 returns :ignored without raising" do + assert :ignored = + Sentry.capture_check_in(status: :ok, monitor_slug: "nil-dsn-job") + end + end end From 30b418ff57977f778bb548421b7acb06de36b76d Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 29 Apr 2026 10:42:02 +0000 Subject: [PATCH 2/4] fixup: ensure dedupe works fine when ignoring --- lib/sentry/client.ex | 4 ++-- test/sentry/test_backward_compat_test.exs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index 1171915d..4414c46b 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -84,9 +84,9 @@ defmodule Sentry.Client do result = with {:ok, %Event{} = event} <- maybe_call_before_send(event, before_send), + :ok <- ensure_dsn_configured(), :ok <- sample_event(sample_rate), - :ok <- maybe_dedupe(event), - :ok <- ensure_dsn_configured() do + :ok <- maybe_dedupe(event) do send_result = encode_and_send(event, result_type, client, request_retries) _ignored = maybe_call_after_send(event, send_result, after_send_event) send_result diff --git a/test/sentry/test_backward_compat_test.exs b/test/sentry/test_backward_compat_test.exs index 1611065f..7cff66ce 100644 --- a/test/sentry/test_backward_compat_test.exs +++ b/test/sentry/test_backward_compat_test.exs @@ -116,5 +116,16 @@ defmodule Sentry.TestBackwardCompatTest do assert :ignored = Sentry.capture_check_in(status: :ok, monitor_slug: "nil-dsn-job") end + + test "capturing the same event twice does not pollute the dedupe table" do + Sentry.Test.Config.put(dedup_events: true) + + exception = %RuntimeError{ + message: "dedupe-regression-#{System.unique_integer([:positive])}" + } + + assert :ignored = Sentry.capture_exception(exception, result: :sync) + assert :ignored = Sentry.capture_exception(exception, result: :sync) + end end end From e6c72023c3de7dd5eeab12c4172952cce734e1b2 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 29 Apr 2026 11:36:19 +0000 Subject: [PATCH 3/4] feat(tests): ensure user callbacks are skipped when dsn is nil in prod/dev --- lib/sentry/config.ex | 44 +++++++++++++++++++-- lib/sentry/test.ex | 82 ++++++++------------------------------- lib/sentry/test/config.ex | 13 +++++++ 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 1edbf328..bf598c8e 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -929,7 +929,7 @@ defmodule Sentry.Config do def hackney_opts, do: fetch!(:hackney_opts) @spec before_send() :: (Sentry.Event.t() -> Sentry.Event.t()) | {module(), atom()} | nil - def before_send, do: get(:before_send) + def before_send, do: compose_send_callback(:before_send) @spec after_send_event() :: (Sentry.Event.t(), term() -> Sentry.Event.t()) | {module(), atom()} | nil @@ -1028,11 +1028,49 @@ defmodule Sentry.Config do @spec before_send_log() :: (Sentry.LogEvent.t() -> Sentry.LogEvent.t() | nil | false) | {module(), atom()} | nil - def before_send_log, do: get(:before_send_log) + def before_send_log, do: compose_send_callback(:before_send_log) @spec before_send_metric() :: (Sentry.Metric.t() -> Sentry.Metric.t() | nil | false) | {module(), atom()} | nil - def before_send_metric, do: get(:before_send_metric) + def before_send_metric, do: compose_send_callback(:before_send_metric) + + # Composes the user-provided callback (under `key`) with an internal callback + # (under `internal_`). In production/development the user-provided + # callback is dropped when there is no DSN, so callbacks never run for events + # that won't be sent. In test mode the user-provided callback is always + # honored — tests routinely use `dsn: nil` to assert callback behavior in + # isolation, and dropping there would break those contracts. + defp compose_send_callback(key) do + user_callback = if user_callbacks_enabled?(), do: get(key), else: nil + internal_callback = get(internal_callback_key(key)) + + case {user_callback, internal_callback} do + {nil, nil} -> nil + {user, nil} -> user + {nil, internal} -> internal + {user, internal} -> chain_send_callbacks(user, internal) + end + end + + defp user_callbacks_enabled? do + not is_nil(dsn()) or test_mode?() + end + + defp internal_callback_key(:before_send), do: :_internal_before_send + defp internal_callback_key(:before_send_log), do: :_internal_before_send_log + defp internal_callback_key(:before_send_metric), do: :_internal_before_send_metric + + defp chain_send_callbacks(first, second) do + fn struct -> + case apply_send_callback(first, struct) do + result when result == nil or result == false -> result + result -> apply_send_callback(second, result) + end + end + end + + defp apply_send_callback(fun, struct) when is_function(fun, 1), do: fun.(struct) + defp apply_send_callback({mod, fun}, struct), do: apply(mod, fun, [struct]) @spec put_config(atom(), term()) :: :ok def put_config(key, value) when is_atom(key) do diff --git a/lib/sentry/test.ex b/lib/sentry/test.ex index de28eb6a..702fd00e 100644 --- a/lib/sentry/test.ex +++ b/lib/sentry/test.ex @@ -650,30 +650,6 @@ defmodule Sentry.Test do # Store in process dict for pop_* lookups Process.put(:sentry_test_collector, collector_table) - # Extract user-provided callbacks from extra_config (if any), falling back to current config - {user_before_send, extra_config} = Keyword.pop(extra_config, :before_send) - {user_before_send_event, extra_config} = Keyword.pop(extra_config, :before_send_event) - {user_before_send_log, extra_config} = Keyword.pop(extra_config, :before_send_log) - {user_before_send_metric, extra_config} = Keyword.pop(extra_config, :before_send_metric) - - # Use the caller-only registry lookup instead of `Sentry.Config.before_send/0` - # so the captured "original" callback is only this test's override (or the - # global default), never another concurrent test's wrapping callback. - original_before_send = - user_before_send || user_before_send_event || - original_config_value(:before_send) - - original_before_send_log = - user_before_send_log || original_config_value(:before_send_log) - - original_before_send_metric = - user_before_send_metric || original_config_value(:before_send_metric) - - # Build collecting callbacks that wrap the originals - new_before_send = build_collecting_callback(original_before_send) - new_before_send_log = build_collecting_callback(original_before_send_log) - new_before_send_metric = build_collecting_callback(original_before_send_metric) - # Always set a per-test DSN override. When no DSN is provided, use the # default Bypass DSN. extra_config = @@ -686,17 +662,20 @@ defmodule Sentry.Test do end end - config = - [finch_request_opts: [receive_timeout: 2000]] - |> Keyword.merge(extra_config) - |> Keyword.merge( - before_send: new_before_send, - before_send_log: new_before_send_log, - before_send_metric: new_before_send_metric - ) + config = Keyword.merge([finch_request_opts: [receive_timeout: 2000]], extra_config) put_test_config(config) + # Install standalone collecting callbacks under internal slots. They're + # composed with the user-provided :before_send / :before_send_log / + # :before_send_metric in `Sentry.Config` based on DSN value: when DSN is + # `nil`, only these collecting callbacks run; when DSN is set, the user's + # callback runs first and its result is collected. + collector = build_collecting_callback() + Sentry.Test.Config.put_override(:_internal_before_send, collector) + Sentry.Test.Config.put_override(:_internal_before_send_log, collector) + Sentry.Test.Config.put_override(:_internal_before_send_metric, collector) + scheduler_pid = get_scheduler_pid() if scheduler_pid do @@ -752,18 +731,11 @@ defmodule Sentry.Test do end) end - # Reads `key` from this test's per-process scope (or any caller's scope on - # `[self() | $callers]`), falling back to the global config value. Skips the - # full namespace resolver so the captured "original" callback is never - # another concurrent test's wrapping callback. - defp original_config_value(key) do - case Sentry.Test.Scope.Registry.lookup_caller_override(key) do - {:ok, value} -> value - :default -> :persistent_term.get({:sentry_config, key}, nil) - end - end - - defp build_collecting_callback(nil) do + # Standalone collecting callback. Records the struct in this test's + # collector ETS table when one is registered for the calling process (or any + # of its callers), then returns the struct unchanged so it flows through any + # remaining pipeline stages. + defp build_collecting_callback do fn struct -> case find_collector() do nil -> :ok @@ -774,28 +746,6 @@ defmodule Sentry.Test do end end - defp build_collecting_callback({mod, fun}) do - build_collecting_callback(Function.capture(mod, fun, 1)) - end - - defp build_collecting_callback(original) when is_function(original, 1) do - fn struct -> - # Guard on find_collector/0 so that a test-specific callback stored in - # :persistent_term is never invoked from an unrelated async test's process. - # When a collector IS found, call the original first so user-defined - # filtering/modification is applied before we collect the result. - case find_collector() do - nil -> - struct - - table -> - result = original.(struct) - unless is_nil(result), do: collect_struct(table, result) - result - end - end - end - defp collect_struct(table, struct) do :ets.insert(table, {System.unique_integer([:monotonic]), struct}) end diff --git a/lib/sentry/test/config.ex b/lib/sentry/test/config.ex index 88b10dca..7ab2370e 100644 --- a/lib/sentry/test/config.ex +++ b/lib/sentry/test/config.ex @@ -146,6 +146,19 @@ defmodule Sentry.Test.Config do Registry.strict_allow!(owner_pid, allowed_pid) end + @doc false + @spec put_override(atom(), term()) :: :ok + def put_override(key, value) when is_atom(key) do + _ = + Registry.update(self(), fn scope -> + Scope.put_override(scope, key, value) + end) + + auto_allow_globals() + + :ok + end + ## Private helpers defp validate_and_rename({key, value}) do From c6c198ed24154cfecf4ae8027aa1562c5f9f1b35 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Wed, 29 Apr 2026 12:17:22 +0000 Subject: [PATCH 4/4] tests: add basic tests for prod mode --- mix.exs | 26 ++++++--- test_integrations/prod_mode/config/config.exs | 17 ++++++ .../prod_mode/lib/prod_mode/application.ex | 11 ++++ .../prod_mode/lib/prod_mode/callback.ex | 50 +++++++++++++++++ test_integrations/prod_mode/mix.exs | 28 ++++++++++ test_integrations/prod_mode/mix.lock | 10 ++++ .../prod_mode/test/prod_mode_test.exs | 54 +++++++++++++++++++ .../prod_mode/test/test_helper.exs | 1 + 8 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 test_integrations/prod_mode/config/config.exs create mode 100644 test_integrations/prod_mode/lib/prod_mode/application.ex create mode 100644 test_integrations/prod_mode/lib/prod_mode/callback.ex create mode 100644 test_integrations/prod_mode/mix.exs create mode 100644 test_integrations/prod_mode/mix.lock create mode 100644 test_integrations/prod_mode/test/prod_mode_test.exs create mode 100644 test_integrations/prod_mode/test/test_helper.exs diff --git a/mix.exs b/mix.exs index 985f7e32..9d21831f 100644 --- a/mix.exs +++ b/mix.exs @@ -151,6 +151,8 @@ defmodule Sentry.Mixfile do end defp run_integration_tests_if_supported(args) do + run_integration_tests("prod_mode", args, env: [{"MIX_ENV", "prod"}]) + if Version.match?(System.version(), ">= 1.16.0") do run_integration_tests("umbrella", args) run_integration_tests("phoenix_app", args) @@ -160,7 +162,7 @@ defmodule Sentry.Mixfile do end end - defp run_integration_tests(integration, args) do + defp run_integration_tests(integration, args, opts \\ []) do IO.puts( IO.ANSI.format([ "\n", @@ -168,11 +170,11 @@ defmodule Sentry.Mixfile do ]) ) - case setup_integration(integration) do + case setup_integration(integration, opts) do {_, 0} -> color_arg = if IO.ANSI.enabled?(), do: "--color", else: "--no-color" - {_, status} = run_in_integration(integration, ["test", color_arg | args]) + {_, status} = run_in_integration(integration, ["test", color_arg | args], opts) if status > 0 do IO.puts( @@ -194,14 +196,22 @@ defmodule Sentry.Mixfile do end end - defp setup_integration(integration) do - run_in_integration(integration, ["deps.get"]) + defp setup_integration(integration, opts) do + run_in_integration(integration, ["deps.get"], opts) end - defp run_in_integration(integration, args) do - System.cmd("mix", args, + defp run_in_integration(integration, args, opts) do + cmd_opts = [ into: IO.binstream(:stdio, :line), cd: Path.join("test_integrations", integration) - ) + ] + + cmd_opts = + case Keyword.get(opts, :env) do + nil -> cmd_opts + env -> Keyword.put(cmd_opts, :env, env) + end + + System.cmd("mix", args, cmd_opts) end end diff --git a/test_integrations/prod_mode/config/config.exs b/test_integrations/prod_mode/config/config.exs new file mode 100644 index 00000000..9b3defc8 --- /dev/null +++ b/test_integrations/prod_mode/config/config.exs @@ -0,0 +1,17 @@ +import Config + +# This integration project runs the Sentry SDK with `test_mode: false` (the +# default), exercising the production code path that the main test suite +# cannot — the main suite forces `test_mode: true`, which enables per-test +# config isolation and the test collector. With test_mode disabled, +# user-provided callbacks must be dropped when DSN is `nil`, matching the +# no-op semantics that pre-13.0.0 had at the Sentry.send_event/2 layer. +# +# The Mix project itself is run under `MIX_ENV=prod` (see the parent +# `mix.exs` aliases) so that `Mix.env()`-gated configuration also reflects +# production. +config :sentry, + dsn: nil, + before_send: {ProdMode.Callback, :on_event}, + before_send_log: {ProdMode.Callback, :on_log}, + before_send_metric: {ProdMode.Callback, :on_metric} diff --git a/test_integrations/prod_mode/lib/prod_mode/application.ex b/test_integrations/prod_mode/lib/prod_mode/application.ex new file mode 100644 index 00000000..d99a0463 --- /dev/null +++ b/test_integrations/prod_mode/lib/prod_mode/application.ex @@ -0,0 +1,11 @@ +defmodule ProdMode.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + ProdMode.Callback.init_table() + Supervisor.start_link([], strategy: :one_for_one, name: ProdMode.Supervisor) + end +end diff --git a/test_integrations/prod_mode/lib/prod_mode/callback.ex b/test_integrations/prod_mode/lib/prod_mode/callback.ex new file mode 100644 index 00000000..1e7542e2 --- /dev/null +++ b/test_integrations/prod_mode/lib/prod_mode/callback.ex @@ -0,0 +1,50 @@ +defmodule ProdMode.Callback do + @moduledoc false + + # Test scaffolding for the prod_mode integration project. Every entry point + # the Sentry SDK exposes for user-provided callbacks (`:before_send`, + # `:before_send_log`, `:before_send_metric`) is wired to one of the + # functions in this module. Each invocation appends to a named ETS table + # which the test suite reads back to assert that callbacks were (or were + # not) called. + + @table :prod_mode_callback_log + + @spec init_table() :: :ok + def init_table do + if :ets.whereis(@table) == :undefined do + :ets.new(@table, [:named_table, :public, :duplicate_bag]) + end + + :ok + end + + @spec calls() :: [{atom(), term()}] + def calls do + :ets.tab2list(@table) + end + + @spec reset() :: :ok + def reset do + :ets.delete_all_objects(@table) + :ok + end + + @spec on_event(term()) :: term() + def on_event(event) do + :ets.insert(@table, {:event, event}) + event + end + + @spec on_log(term()) :: term() + def on_log(log) do + :ets.insert(@table, {:log, log}) + log + end + + @spec on_metric(term()) :: term() + def on_metric(metric) do + :ets.insert(@table, {:metric, metric}) + metric + end +end diff --git a/test_integrations/prod_mode/mix.exs b/test_integrations/prod_mode/mix.exs new file mode 100644 index 00000000..9a405516 --- /dev/null +++ b/test_integrations/prod_mode/mix.exs @@ -0,0 +1,28 @@ +defmodule ProdMode.MixProject do + use Mix.Project + + def project do + [ + app: :prod_mode, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger], + mod: {ProdMode.Application, []} + ] + end + + defp deps do + [ + {:sentry, path: "../.."}, + {:finch, "~> 0.17"}, + {:jason, "~> 1.1"} + ] + end +end diff --git a/test_integrations/prod_mode/mix.lock b/test_integrations/prod_mode/mix.lock new file mode 100644 index 00000000..8392e72e --- /dev/null +++ b/test_integrations/prod_mode/mix.lock @@ -0,0 +1,10 @@ +%{ + "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "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"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, +} diff --git a/test_integrations/prod_mode/test/prod_mode_test.exs b/test_integrations/prod_mode/test/prod_mode_test.exs new file mode 100644 index 00000000..5165fd79 --- /dev/null +++ b/test_integrations/prod_mode/test/prod_mode_test.exs @@ -0,0 +1,54 @@ +defmodule ProdModeTest do + use ExUnit.Case, async: false + + alias ProdMode.Callback + + setup do + Callback.reset() + :ok + end + + describe "running with test_mode: false and dsn: nil" do + test "Mix env is :prod and test_mode is disabled" do + assert Mix.env() == :prod + refute Sentry.Config.test_mode?() + assert is_nil(Sentry.Config.dsn()) + end + + test "capture_exception/2 is a no-op and never invokes the user before_send" do + assert :ignored = Sentry.capture_exception(%RuntimeError{message: "boom"}, result: :sync) + assert [] == Callback.calls() + end + + test "capture_message/2 is a no-op and never invokes the user before_send" do + assert :ignored = Sentry.capture_message("hello", result: :sync) + assert [] == Callback.calls() + end + + test "send_transaction/2 is a no-op and never invokes the user before_send" do + transaction = + Sentry.Transaction.new(%{ + span_id: "prod-mode-span", + start_timestamp: "2025-01-01T00:00:00Z", + timestamp: "2025-01-02T00:00:00Z", + contexts: %{trace: %{trace_id: "prod-mode-trace", span_id: "prod-mode-span"}}, + spans: [] + }) + + assert :ignored = Sentry.send_transaction(transaction, result: :sync) + assert [] == Callback.calls() + end + + test "capture_check_in/1 is a no-op without raising" do + assert :ignored = Sentry.capture_check_in(status: :ok, monitor_slug: "prod-mode-job") + assert [] == Callback.calls() + end + + test "Sentry.Config.before_send/0 returns nil" do + # The composed callback should resolve to nil when neither a DSN is set + # nor test_mode is enabled, even though the user has configured a + # before_send callback. + assert is_nil(Sentry.Config.before_send()) + end + end +end diff --git a/test_integrations/prod_mode/test/test_helper.exs b/test_integrations/prod_mode/test/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/test_integrations/prod_mode/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()