Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ This is the official Sentry SDK for Elixir. It captures errors, monitors cron jo
| `lib/sentry/logger_handler.ex` | Erlang logger handler |
| `lib/sentry/plug_capture.ex` | Plug exception capture |
| `lib/sentry/live_view_hook.ex` | Phoenix LiveView hook |
| `lib/sentry/test.ex` | Testing utilities |
| `lib/mix/tasks/` | Mix tasks (install, test event, source packaging) |
| `test/` | Unit and integration tests |
| `test/support/` | Test helpers and shared utilities |
Expand Down Expand Up @@ -97,14 +96,13 @@ Configuration is validated at application start using NimbleOptions, then cached

### Key Patterns

- **HTTP testing:** Use `Bypass` for HTTP-level tests with `send_result: :sync`
- **Buffered path testing:** Use `Sentry.Test.start_collecting_sentry_reports/0` + `assert_receive`/`pop_sentry_*` helpers
- **HTTP testing:** Use `Bypass` for HTTP-level tests with `send_result: :sync`. Use `setup_bypass/1` to open a Bypass instance and configure DSN, `setup_bypass_envelope_collector/1` to forward envelopes to the test process, and `collect_envelopes/3` + `extract_events/1` / `extract_transactions/1` / `extract_log_items/1` to retrieve and filter decoded envelope items.
- **Test isolation:** Each test gets uniquely-named components (rate limiter, processor, span storage) via process dictionary and `:persistent_term`
- **Config isolation:** Use `put_test_config/1` from test helpers for isolated config changes with automatic cleanup

### Test Configuration

Tests run with `send_result: :sync` and `test_mode: true` (set in `config/config.exs`). This bypasses the TelemetryProcessor pipeline for direct, synchronous event sending.
Tests run with `send_result: :sync` (set in `config/config.exs`). This bypasses the TelemetryProcessor pipeline for direct, synchronous event sending. All tests that send events use Bypass to capture HTTP requests — there is no in-memory event collection.

### Integration Tests

Expand Down
4 changes: 2 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ if config_env() == :test do
tags: %{},
enable_source_code_context: true,
root_source_code_paths: [File.cwd!()],
finch_request_opts: [receive_timeout: 50],
dsn: "http://public:secret@localhost:8999/1",
finch_request_opts: [receive_timeout: 2000],
send_result: :sync,
send_max_attempts: 1,
dedup_events: false,
test_mode: true,
traces_sample_rate: 1.0

config :sentry, request_retries: []
Expand Down
7 changes: 0 additions & 7 deletions lib/sentry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,6 @@ defmodule Sentry do
ClientReport.Sender.record_discarded_events(:event_processor, [event])
:ignored

# If we're in test mode, let's send the event down the pipeline anyway.
Config.test_mode?() ->
Client.send_event(event, options)

!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.
Expand All @@ -392,9 +388,6 @@ defmodule Sentry do
included_envs = Config.included_environments()

cond do
Config.test_mode?() ->
Client.send_transaction(transaction, options)

!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.
Expand Down
86 changes: 28 additions & 58 deletions lib/sentry/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -217,40 +217,25 @@ defmodule Sentry.Client do
end

defp encode_and_send(%Event{} = event, _result_type = :sync, client, request_retries) do
case Sentry.Test.maybe_collect(event) do
:collected ->
{:ok, ""}

:not_collecting ->
send_result =
event
|> Envelope.from_event()
|> Transport.encode_and_post_envelope(client, request_retries)

send_result
end
event
|> Envelope.from_event()
|> Transport.encode_and_post_envelope(client, request_retries)
end

defp encode_and_send(%Event{} = event, _result_type = :none, client, _request_retries) do
case Sentry.Test.maybe_collect(event) do
:collected ->
{:ok, ""}

:not_collecting ->
if Config.telemetry_processor_category?(:error) do
case TelemetryProcessor.add(event) do
{:ok, {:rate_limited, data_category}} ->
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)

:ok ->
:ok
end
else
:ok = Transport.Sender.send_async(client, event)
end
if Config.telemetry_processor_category?(:error) do
case TelemetryProcessor.add(event) do
{:ok, {:rate_limited, data_category}} ->
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)

{:ok, ""}
:ok ->
:ok
end
else
:ok = Transport.Sender.send_async(client, event)
end

{:ok, ""}
end

defp encode_and_send(
Expand All @@ -259,18 +244,9 @@ defmodule Sentry.Client do
client,
request_retries
) do
case Sentry.Test.maybe_collect(transaction) do
:collected ->
{:ok, ""}

:not_collecting ->
send_result =
transaction
|> Envelope.from_transaction()
|> Transport.encode_and_post_envelope(client, request_retries)

send_result
end
transaction
|> Envelope.from_transaction()
|> Transport.encode_and_post_envelope(client, request_retries)
end

defp encode_and_send(
Expand All @@ -279,25 +255,19 @@ defmodule Sentry.Client do
client,
_request_retries
) do
case Sentry.Test.maybe_collect(transaction) do
:collected ->
{:ok, ""}

:not_collecting ->
if Config.telemetry_processor_category?(:transaction) do
case TelemetryProcessor.add(transaction) do
{:ok, {:rate_limited, data_category}} ->
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)

:ok ->
:ok
end
else
:ok = Transport.Sender.send_async(client, transaction)
end
if Config.telemetry_processor_category?(:transaction) do
case TelemetryProcessor.add(transaction) do
{:ok, {:rate_limited, data_category}} ->
ClientReport.Sender.record_discarded_events(:ratelimit_backoff, data_category)

{:ok, ""}
:ok ->
:ok
end
else
:ok = Transport.Sender.send_async(client, transaction)
end

{:ok, ""}
end

@spec render_event(Event.t()) :: map()
Expand Down
15 changes: 0 additions & 15 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,6 @@ defmodule Sentry.Config do
doc: """
The DSN for your Sentry project. If this is not set, Sentry will not be enabled.
If the `SENTRY_DSN` environment variable is set, it will be used as the default value.
If `:test_mode` is `true`, the `:dsn` option is sometimes ignored; see `Sentry.Test`
for more information.
"""
],
environment_name: [
Expand Down Expand Up @@ -365,16 +363,6 @@ defmodule Sentry.Config do
stacktrace, and fingerprint. *Available since v10.0.0*.
"""
],
test_mode: [
type: :boolean,
default: false,
doc: """
Whether to enable *test mode*. When test mode is enabled, the SDK will check whether
there is a process **collecting events** and avoid sending those events if that's the
case. This is useful for testing—see `Sentry.Test`. `:test_mode` works in tandem
with `:dsn`; this is described in detail in `Sentry.Test`.
"""
],
integrations: [
type: :keyword_list,
doc: """
Expand Down Expand Up @@ -890,9 +878,6 @@ defmodule Sentry.Config do
@spec send_client_reports?() :: boolean()
def send_client_reports?, do: fetch!(:send_client_reports)

@spec test_mode?() :: boolean()
def test_mode?, do: fetch!(:test_mode)

@spec integrations() :: keyword()
def integrations, do: fetch!(:integrations)

Expand Down
58 changes: 9 additions & 49 deletions lib/sentry/telemetry/scheduler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -291,67 +291,27 @@ defmodule Sentry.Telemetry.Scheduler do
state
end

defp process_and_send_event(%{on_envelope: on_envelope} = state, %Event{} = event, send_fn) do
# Skip test collection when on_envelope is set (used by unit tests)
if is_nil(on_envelope) do
case Sentry.Test.maybe_collect(event) do
:collected ->
state

:not_collecting ->
envelope = Envelope.from_event(event)
send_fn.(state, envelope)
end
else
envelope = Envelope.from_event(event)
send_fn.(state, envelope)
end
defp process_and_send_event(state, %Event{} = event, send_fn) do
envelope = Envelope.from_event(event)
send_fn.(state, envelope)
end

defp process_and_send_check_in(state, %CheckIn{} = check_in, send_fn) do
envelope = Envelope.from_check_in(check_in)
send_fn.(state, envelope)
end

defp process_and_send_transaction(
%{on_envelope: on_envelope} = state,
%Transaction{} = transaction,
send_fn
) do
# Skip test collection when on_envelope is set (used by unit tests)
if is_nil(on_envelope) do
case Sentry.Test.maybe_collect(transaction) do
:collected ->
state

:not_collecting ->
envelope = Envelope.from_transaction(transaction)
send_fn.(state, envelope)
end
else
envelope = Envelope.from_transaction(transaction)
send_fn.(state, envelope)
end
defp process_and_send_transaction(state, %Transaction{} = transaction, send_fn) do
envelope = Envelope.from_transaction(transaction)
send_fn.(state, envelope)
end

defp process_and_send_logs(%{on_envelope: on_envelope} = state, log_events, send_fn) do
defp process_and_send_logs(state, log_events, send_fn) do
processed_logs = apply_before_send_log_callbacks(log_events)

if processed_logs != [] do
# Skip test collection when on_envelope is set (used by unit tests)
if is_nil(on_envelope) do
case Sentry.Test.maybe_collect_logs(processed_logs) do
:collected ->
state

:not_collecting ->
envelope = Envelope.from_log_events(processed_logs)
send_fn.(state, envelope)
end
else
envelope = Envelope.from_log_events(processed_logs)
send_fn.(state, envelope)
end
envelope = Envelope.from_log_events(processed_logs)
send_fn.(state, envelope)
else
state
end
Expand Down
Loading
Loading