diff --git a/lib/sentry.ex b/lib/sentry.ex index 7f3040e6..f2fd5aca 100644 --- a/lib/sentry.ex +++ b/lib/sentry.ex @@ -467,7 +467,7 @@ defmodule Sentry do @spec send_event(Event.t(), keyword()) :: send_result def send_event(event, opts \\ []) - def send_event(%Event{message: nil, exception: nil}, _opts) do + def send_event(%Event{message: nil, exception: []}, _opts) do Logger.log(Config.log_level(), "Sentry: unable to parse exception") :ignored diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index 281a2db8..d1946f8e 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -89,7 +89,7 @@ defmodule Sentry.Client do send_result = Transport.post_envelope(envelope, request_retries) if match?({:ok, _}, send_result) do - Sentry.put_last_event_id_and_source(event.event_id, event.__source__) + Sentry.put_last_event_id_and_source(event.event_id, event.source) end _ = maybe_log_send_result(send_result, event) @@ -98,7 +98,7 @@ defmodule Sentry.Client do defp encode_and_send(%Event{} = event, _result_type = :none, _request_retries) do :ok = @sender_module.send_async(event) - Sentry.put_last_event_id_and_source(event.event_id, event.__source__) + Sentry.put_last_event_id_and_source(event.event_id, event.source) {:ok, ""} end @@ -107,15 +107,14 @@ defmodule Sentry.Client do json_library = Config.json_library() event - |> Map.from_struct() + |> Event.remove_non_payload_keys() |> update_if_present(:message, &String.slice(&1, 0, @max_message_length)) |> update_if_present(:breadcrumbs, fn bcs -> Enum.map(bcs, &Map.from_struct/1) end) |> update_if_present(:sdk, &Map.from_struct/1) |> update_if_present(:extra, &sanitize_non_jsonable_values(&1, json_library)) |> update_if_present(:user, &sanitize_non_jsonable_values(&1, json_library)) |> update_if_present(:tags, &sanitize_non_jsonable_values(&1, json_library)) - |> update_if_present(:exception, &[render_exception(&1)]) - |> Map.drop([:__source__, :__original_exception__]) + |> update_if_present(:exception, fn list -> Enum.map(list, &render_exception/1) end) end defp render_exception(%Interfaces.Exception{} = exception) do @@ -178,7 +177,7 @@ defmodule Sentry.Client do end end - defp maybe_log_send_result(_send_result, %Event{__source__: :logger}) do + defp maybe_log_send_result(_send_result, %Event{source: :logger}) do :ok end diff --git a/lib/sentry/envelope.ex b/lib/sentry/envelope.ex index db914f67..2818ae48 100644 --- a/lib/sentry/envelope.ex +++ b/lib/sentry/envelope.ex @@ -131,14 +131,14 @@ defmodule Sentry.Envelope do culprit: fields["culprit"], environment: fields["environment"], event_id: fields["event_id"], - __source__: fields["event_source"], - exception: fields["exception"], + source: fields["event_source"], + exception: List.wrap(fields["exception"]), extra: fields["extra"], fingerprint: fields["fingerprint"], level: fields["level"], message: fields["message"], modules: fields["modules"], - __original_exception__: fields["original_exception"], + original_exception: fields["original_exception"], platform: fields["platform"], release: fields["release"], request: fields["request"], diff --git a/lib/sentry/event.ex b/lib/sentry/event.ex index ed9e64a8..131b3803 100644 --- a/lib/sentry/event.ex +++ b/lib/sentry/event.ex @@ -29,7 +29,22 @@ defmodule Sentry.Event do @typedoc """ The type for the event struct. - See [`%Sentry.Event{}`](`__struct__/0`) for more information. + All of the fields in this struct map directly to the fields described in the + [Sentry documentation](https://develop.sentry.dev/sdk/event-payloads). These fields + are the exceptions, and are specific to the Elixir Sentry SDK: + + * `:source` - the source of the event. `Sentry.LoggerBackend` and `Sentry.LoggerHandler` + set this to `:logger`, while `Sentry.PlugCapture` and `Sentry.PlugContext` set it to + `:plug`. You can set it to any atom. See the `:event_source` option in `create_event/1` + and `transform_exception/2`. + + * `:original_exception` - the original exception that is being reported, if there's one. + The Elixir Sentry SDK manipulates reported exceptions to make them fit the payload + required by the Sentry API, and these end up in the `:exception` field. The + `:original_exception` field, instead, contains the original exception as the raw Elixir + term (such as `%RuntimeError{...}`). + + See also [`%Sentry.Event{}`](`__struct__/0`). """ @type t() :: %__MODULE__{ # Required @@ -53,113 +68,144 @@ defmodule Sentry.Event do # Interfaces. breadcrumbs: [Interfaces.Breadcrumb.t()], contexts: Interfaces.context(), - exception: Interfaces.Exception.t() | nil, + exception: [Interfaces.Exception.t()], message: String.t() | nil, request: Interfaces.request(), sdk: Interfaces.SDK.t() | nil, user: Interfaces.user() | nil, # Non-payload fields. - __source__: term(), - __original_exception__: Exception.t() | nil + source: atom(), + original_exception: Exception.t() | nil } @doc """ The struct representing the event. - In general, you're not advised to manipulate this struct's fields directly. Instead, - try to use functions such as `create_event/1` or `transform_exception/2` for creating + You're not advised to manipulate this struct's fields directly. Instead, + use functions such as `create_event/1` or `transform_exception/2` for creating events. + + See the `t:t/0` type for information on the fields and their types. """ @enforce_keys [:event_id, :timestamp] defstruct [ # Required. Hexadecimal string representing a uuid4 value. The length is exactly 32 # characters. Dashes are not allowed. Has to be lowercase. - :event_id, + event_id: nil, # Required. Indicates when the event was created in the Sentry SDK. The format is either a # string as defined in RFC 3339 or a numeric (integer or float) value representing the number # of seconds that have elapsed since the Unix epoch. - :timestamp, - - # Optional fields without defaults. - :level, - :logger, - :transaction, - :server_name, - :release, - :dist, - - # Interfaces. - :breadcrumbs, - :contexts, - :exception, - :message, - :request, - :sdk, - :user, + timestamp: nil, + + # Optional fields. + breadcrumbs: [], + contexts: nil, + dist: nil, + environment: "production", + exception: [], + extra: %{}, + fingerprint: [], + level: nil, + logger: nil, + message: nil, + modules: %{}, + platform: :elixir, + release: nil, + request: %{}, + sdk: nil, + server_name: nil, + tags: %{}, + transaction: nil, + user: %{}, # "Culprit" is not documented anymore and we should move to transactions at some point. # https://forum.sentry.io/t/culprit-deprecated-in-favor-of-what/4871/9 - :culprit, + culprit: nil, # Non-payload "private" fields. - :__source__, - :__original_exception__, - - # Required. Has to be "elixir". - platform: :elixir, - - # Optional fields with defaults. - tags: %{}, - modules: %{}, - extra: %{}, - fingerprint: [], - environment: "production" + source: nil, + original_exception: nil ] + # Removes all the non-payload keys from the event so that the client can render + @doc false + @spec remove_non_payload_keys(t()) :: map() + def remove_non_payload_keys(%__MODULE__{} = event) do + event + |> Map.from_struct() + |> Map.drop([:original_exception, :source]) + end + @doc """ Creates an event struct out of collected context and options. + > #### Merging Options with Context and Config {: .info} + > + > Some of the options documented below are **merged** with the Sentry context, or + > with the Sentry context *and* the configuration. The option you pass here always + > has higher precedence, followed by the context and finally by the configuration. + > + > See also `Sentry.Context` for information on the Sentry context and `Sentry` for + > information on configuration. + ## Options - * `:exception` - an `t:Exception.t/0` + * `:exception` - an `t:Exception.t/0`. This is the exception that gets reported in the + `:exception` field of `t:t/0`. The term passed here also ends up unchanged in the + `:original_exception` field of `t:t/0`. This option is **required** unless the + `:message` option is present. This is not present by default. - * `:stacktrace` - a stacktrace, as in `t:Exception.stacktrace/0` + * `:stacktrace` - a stacktrace, as in `t:Exception.stacktrace/0`. This is not present + by default. - * `:message` - a message (`t:String.t/0`) + * `:message` - a message (`t:String.t/0`). This is not present by default. * `:extra` - map of extra context, which gets merged with the current context - (see `Sentry.Context`) + (see `Sentry.Context.set_extra_context/1`). If fields collide, the ones + in the map passed through this option have precedence over the ones in + the context. Defaults to `%{}`. * `:user` - map of user context, which gets merged with the current context - (see `Sentry.Context`) + (see `Sentry.Context.set_user_context/1`). If fields collide, the ones + in the map passed through this option have precedence over the ones in + the context. Defaults to `%{}`. - * `:tags` - map of tags context, which gets merged with the current context - (see `Sentry.Context`) + * `:tags` - map of tags context, which gets merged with the current context (see + `Sentry.Context.set_tags_context/1`) and with the `:tags` option in the global + Sentry configuration. If fields collide, the ones in the map passed through + this option have precedence over the ones in the context, which have precedence + over the ones in the configuration. Defaults to `%{}`. * `:request` - map of request context, which gets merged with the current context - (see `Sentry.Context`) + (see `Sentry.Context.set_request_context/1`). If fields collide, the ones + in the map passed through this option have precedence over the ones in + the context. Defaults to `%{}`. - * `:breadcrumbs` - list of breadcrumbs + * `:breadcrumbs` - list of breadcrumbs. This list gets **prepended** to the list + in the context (see `Sentry.Context.add_breadcrumb/1`). Defaults to `[]`. - * `:level` - error level (see `t:t/0`) + * `:level` - error level (see `t:t/0`). Defaults to `:error`. - * `:fingerprint` - list of the fingerprint for grouping this event (a list of `t:String.t/0`) + * `:fingerprint` - list of the fingerprint for grouping this event (a list + of `t:String.t/0`). Defaults to `["{{ default }}"]`. - * `:event_source` - the source of the event. This fills in the `:__source__` field of the - returned struct. + * `:event_source` - the source of the event. This fills in the `:source` field of the + returned struct. This is not present by default. ## Examples iex> event = create_event(exception: %RuntimeError{message: "oops"}, level: :warning) iex> event.level :warning - iex> event.exception.type + iex> hd(event.exception).type "RuntimeError" + iex> event.original_exception + %RuntimeError{message: "oops"} - iex> event = create_event(event_source: :plug) - iex> event.__source__ + iex> event = create_event(message: "Unknown route", event_source: :plug) + iex> event.source :plug """ @@ -173,7 +219,7 @@ defmodule Sentry.Event do | {:level, level()} | {:fingerprint, [String.t()]} | {:message, String.t()} - | {:event_source, term()} + | {:event_source, atom()} | {:exception, Exception.t()} | {:stacktrace, Exception.stacktrace()} def create_event(opts) when is_list(opts) do @@ -191,25 +237,18 @@ defmodule Sentry.Event do request: request_context } = Sentry.Context.get_all() + level = Keyword.get(opts, :level, :error) fingerprint = Keyword.get(opts, :fingerprint, ["{{ default }}"]) - extra = - extra_context - |> Map.merge(Keyword.get(opts, :extra, %{})) - - user = - user_context - |> Map.merge(Keyword.get(opts, :user, %{})) + extra = Map.merge(extra_context, Keyword.get(opts, :extra, %{})) + user = Map.merge(user_context, Keyword.get(opts, :user, %{})) + request = Map.merge(request_context, Keyword.get(opts, :request, %{})) tags = Config.tags() |> Map.merge(tags_context) |> Map.merge(Keyword.get(opts, :tags, %{})) - request = - request_context - |> Map.merge(Keyword.get(opts, :request, %{})) - breadcrumbs = Keyword.get(opts, :breadcrumbs, []) |> Kernel.++(breadcrumbs_context) @@ -218,31 +257,30 @@ defmodule Sentry.Event do message = Keyword.get(opts, :message) exception = Keyword.get(opts, :exception) + stacktrace = Keyword.get(opts, :stacktrace) + source = Keyword.get(opts, :event_source) %__MODULE__{ - event_id: UUID.uuid4_hex(), - timestamp: timestamp, - level: Keyword.get(opts, :level, :error), - server_name: Config.server_name() || to_string(:net_adm.localhost()), - release: Config.release(), - sdk: @sdk, - tags: tags, - modules: - Enum.reduce(@deps, %{}, fn app, acc -> - Map.put(acc, app, to_string(Application.spec(app, :vsn))) - end), - culprit: culprit_from_stacktrace(Keyword.get(opts, :stacktrace, [])), - extra: extra, breadcrumbs: breadcrumbs, contexts: generate_contexts(), - exception: coerce_exception(exception, Keyword.get(opts, :stacktrace), message), - message: message, - fingerprint: fingerprint, + culprit: culprit_from_stacktrace(Keyword.get(opts, :stacktrace, [])), environment: Config.environment_name(), - user: user, + event_id: UUID.uuid4_hex(), + exception: List.wrap(coerce_exception(exception, stacktrace, message)), + extra: extra, + fingerprint: fingerprint, + level: level, + message: message, + modules: Map.new(@deps, &{&1, to_string(Application.spec(&1, :vsn))}), + original_exception: exception, + release: Config.release(), request: request, - __source__: Keyword.get(opts, :event_source), - __original_exception__: exception + sdk: @sdk, + server_name: Config.server_name() || to_string(:net_adm.localhost()), + source: source, + tags: tags, + timestamp: timestamp, + user: user } end diff --git a/lib/sentry/transport/sender.ex b/lib/sentry/transport/sender.ex index 627bedf0..24da5a1f 100644 --- a/lib/sentry/transport/sender.ex +++ b/lib/sentry/transport/sender.ex @@ -88,7 +88,7 @@ defmodule Sentry.Transport.Sender do end defp maybe_log_send_result(send_result, events) do - if Enum.any?(events, &(&1.__source__ == :logger)) do + if Enum.any?(events, &(&1.source == :logger)) do :ok else message = diff --git a/test/envelope_test.exs b/test/envelope_test.exs index ac30d943..03049ec7 100644 --- a/test/envelope_test.exs +++ b/test/envelope_test.exs @@ -27,8 +27,8 @@ defmodule Sentry.EnvelopeTest do breadcrumbs: [], environment: :test, event_id: "1d208b37d9904203918a9c2125ea91fa", - __source__: nil, - exception: nil, + source: nil, + exception: [], extra: %{}, fingerprint: ["{{ default }}"], level: "error", @@ -59,7 +59,7 @@ defmodule Sentry.EnvelopeTest do telemetry: "0.4.2", unicode_util_compat: "0.7.0" }, - __original_exception__: nil, + original_exception: nil, platform: :elixir, release: nil, request: %{}, @@ -83,7 +83,7 @@ defmodule Sentry.EnvelopeTest do breadcrumbs: [], environment: "test", event_id: "1d208b37d9904203918a9c2125ea91fa", - exception: nil, + exception: [], extra: %{}, fingerprint: ["{{ default }}"], level: "error", @@ -141,7 +141,7 @@ defmodule Sentry.EnvelopeTest do assert decoded_event["event_id"] == event.event_id assert decoded_event["breadcrumbs"] == [] assert decoded_event["environment"] == "test" - assert decoded_event["exception"] == nil + assert decoded_event["exception"] == [] assert decoded_event["extra"] == %{} assert decoded_event["user"] == %{} assert decoded_event["request"] == %{} diff --git a/test/event_test.exs b/test/event_test.exs index e1294b5b..0de3b3a7 100644 --- a/test/event_test.exs +++ b/test/event_test.exs @@ -22,11 +22,14 @@ defmodule Sentry.EventTest do assert event.platform == :elixir assert event.extra == %{} - assert %Interfaces.Exception{ - type: "UndefinedFunctionError", - value: "function Sentry.Event.not_a_function/3 is undefined or private", - module: nil - } = event.exception + assert [ + %Interfaces.Exception{ + type: "UndefinedFunctionError", + value: "function Sentry.Event.not_a_function/3 is undefined or private", + module: nil, + stacktrace: stacktrace + } + ] = event.exception assert event.level == :error assert event.message == nil @@ -100,7 +103,7 @@ defmodule Sentry.EventTest do pre_context: [], vars: %{"arg0" => "1", "arg1" => "2", "arg2" => "3"} } - ] = event.exception.stacktrace.frames + ] = stacktrace.frames assert event.tags == %{} assert event.timestamp =~ ~r/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ @@ -128,7 +131,7 @@ defmodule Sentry.EventTest do assert %{} = event.request assert %{} = event.contexts assert event.release == nil - assert event.exception == nil + assert event.exception == [] assert event.message == nil assert map_size(event.modules) > 0 end @@ -198,11 +201,13 @@ defmodule Sentry.EventTest do stacktrace: stacktrace ) - assert %Interfaces.Exception{ - type: "RuntimeError", - value: "foo", - stacktrace: %Interfaces.Stacktrace{frames: [stacktrace_frame | _rest]} - } = event.exception + assert [ + %Interfaces.Exception{ + type: "RuntimeError", + value: "foo", + stacktrace: %Interfaces.Stacktrace{frames: [stacktrace_frame | _rest]} + } + ] = event.exception assert %Interfaces.Stacktrace.Frame{} = stacktrace_frame assert is_binary(stacktrace_frame.filename) @@ -214,10 +219,12 @@ defmodule Sentry.EventTest do assert %Event{} = event = Event.create_event(exception: %RuntimeError{message: "foo"}) - assert event.exception == %Interfaces.Exception{ - type: "RuntimeError", - value: "foo" - } + assert event.exception == [ + %Interfaces.Exception{ + type: "RuntimeError", + value: "foo" + } + ] end test "raises an error if passing :stacktrace without :exception" do @@ -230,7 +237,7 @@ defmodule Sentry.EventTest do assert %Event{ breadcrumbs: [], environment: :test, - exception: nil, + exception: [], extra: %{}, level: :error, message: "Test message", @@ -249,8 +256,8 @@ defmodule Sentry.EventTest do assert %Event{} = event = Event.create_event(exception: exception, event_source: :plug) - assert event.__source__ == :plug - assert event.__original_exception__ == exception + assert event.source == :plug + assert event.original_exception == exception end end @@ -348,7 +355,7 @@ defmodule Sentry.EventTest do pre_context: [], vars: %{} } - ] == event.exception.stacktrace.frames + ] == hd(event.exception).stacktrace.frames end test "transforms mix deps to map of modules" do diff --git a/test/logger_backend_test.exs b/test/logger_backend_test.exs index a5401f23..9aabe134 100644 --- a/test/logger_backend_test.exs +++ b/test/logger_backend_test.exs @@ -41,8 +41,9 @@ defmodule Sentry.LoggerBackendTest do end) assert_receive {^ref, event} - assert event.exception.type == "RuntimeError" - assert event.exception.value == "Unique Error" + assert [exception] = event.exception + assert exception.type == "RuntimeError" + assert exception.value == "Unique Error" end test "a GenServer throw is reported" do @@ -51,11 +52,12 @@ defmodule Sentry.LoggerBackendTest do pid = start_supervised!(TestGenServer) Sentry.TestGenServer.throw(pid) assert_receive {^ref, event} - assert event.exception.value =~ "GenServer #{inspect(pid)} terminating\n" - assert event.exception.value =~ "** (stop) bad return value: \"I am throwing\"\n" - assert event.exception.value =~ "Last message: {:\"$gen_cast\", :throw}\n" - assert event.exception.value =~ "State: []" - assert event.exception.stacktrace.frames == [] + assert [exception] = event.exception + assert exception.value =~ "GenServer #{inspect(pid)} terminating\n" + assert exception.value =~ "** (stop) bad return value: \"I am throwing\"\n" + assert exception.value =~ "Last message: {:\"$gen_cast\", :throw}\n" + assert exception.value =~ "State: []" + assert exception.stacktrace.frames == [] end test "abnormal GenServer exit is reported" do @@ -64,11 +66,12 @@ defmodule Sentry.LoggerBackendTest do pid = start_supervised!(TestGenServer) Sentry.TestGenServer.exit(pid) assert_receive {^ref, event} - assert event.exception.type == "message" - assert event.exception.value =~ "GenServer #{inspect(pid)} terminating\n" - assert event.exception.value =~ "** (stop) :bad_exit\n" - assert event.exception.value =~ "Last message: {:\"$gen_cast\", :exit}\n" - assert event.exception.value =~ "State: []" + assert [exception] = event.exception + assert exception.type == "message" + assert exception.value =~ "GenServer #{inspect(pid)} terminating\n" + assert exception.value =~ "** (stop) :bad_exit\n" + assert exception.value =~ "Last message: {:\"$gen_cast\", :exit}\n" + assert exception.value =~ "State: []" end test "bad function call causing GenServer crash is reported" do @@ -80,7 +83,7 @@ defmodule Sentry.LoggerBackendTest do Sentry.TestGenServer.invalid_function(pid) assert_receive {^ref, event} - assert event.exception.type == "FunctionClauseError" + assert hd(event.exception).type == "FunctionClauseError" assert [%{message: "test"}] = event.breadcrumbs assert %{ @@ -89,7 +92,7 @@ defmodule Sentry.LoggerBackendTest do context_line: nil, pre_context: [], post_context: [] - } = List.last(event.exception.stacktrace.frames) + } = List.last(hd(event.exception).stacktrace.frames) end test "GenServer timeout is reported" do @@ -104,14 +107,16 @@ defmodule Sentry.LoggerBackendTest do assert_receive {^ref, event} - assert event.exception.type == "message" + assert [exception] = event.exception - assert event.exception.value =~ + assert exception.type == "message" + + assert exception.value =~ "Task #{inspect(task_pid)} started from #{inspect(self())} terminating\n" - assert event.exception.value =~ "** (stop) exited in: GenServer.call(" - assert event.exception.value =~ "** (EXIT) time out" - assert length(event.exception.stacktrace.frames) > 0 + assert exception.value =~ "** (stop) exited in: GenServer.call(" + assert exception.value =~ "** (EXIT) time out" + assert length(exception.stacktrace.frames) > 0 end test "captures errors from spawn/0 in Plug app" do @@ -123,7 +128,7 @@ defmodule Sentry.LoggerBackendTest do assert_receive {^ref, event} - assert [stacktrace_frame] = event.exception.stacktrace.frames + assert [stacktrace_frame] = hd(event.exception).stacktrace.frames assert stacktrace_frame.filename == "test/support/example_plug_application.ex" end @@ -275,8 +280,10 @@ defmodule Sentry.LoggerBackendTest do assert event.user.user_id == 3 assert event.extra.day_of_week == "Friday" - assert event.exception.type == "RuntimeError" - assert event.exception.value == "oops" + + assert [exception] = event.exception + assert exception.type == "RuntimeError" + assert exception.value == "oops" end test "handles malformed :callers metadata" do diff --git a/test/sentry/client_test.exs b/test/sentry/client_test.exs index f5fddeed..3c64af91 100644 --- a/test/sentry/client_test.exs +++ b/test/sentry/client_test.exs @@ -144,7 +144,7 @@ defmodule Sentry.ClientTest do test "if :before_send_event callback returns falsey, the event is not sent" do defmodule CallbackModuleArithmeticError do def before_send_event(event) do - case event.__original_exception__ do + case event.original_exception do %ArithmeticError{} -> false _ -> event end diff --git a/test/sentry/logger_handler_test.exs b/test/sentry/logger_handler_test.exs index 19bd3639..1149b469 100644 --- a/test/sentry/logger_handler_test.exs +++ b/test/sentry/logger_handler_test.exs @@ -42,8 +42,9 @@ defmodule Sentry.LoggerHandlerTest do end) assert_receive {^ref, event} - assert event.exception.type == "RuntimeError" - assert event.exception.value == "Unique Error" + assert [exception] = event.exception + assert exception.type == "RuntimeError" + assert exception.value == "Unique Error" end test "a GenServer throw is reported", %{sender_ref: ref} do @@ -65,7 +66,7 @@ defmodule Sentry.LoggerHandlerTest do assert event.message =~ "** (stop) :bad_exit" if System.otp_release() >= "26" do - assert event.exception.type == "message" + assert hd(event.exception).type == "message" end end @@ -80,11 +81,13 @@ defmodule Sentry.LoggerHandlerTest do assert [%{message: "test"}] = event.breadcrumbs + assert [exception] = event.exception + if System.otp_release() >= "26" do - assert event.exception.type == "FunctionClauseError" + assert exception.type == "FunctionClauseError" else assert event.message =~ "** (stop) :function_clause" - assert event.exception.type == "message" + assert exception.type == "message" end assert %{ @@ -93,7 +96,7 @@ defmodule Sentry.LoggerHandlerTest do context_line: nil, pre_context: [], post_context: [] - } = List.last(event.exception.stacktrace.frames) + } = List.last(exception.stacktrace.frames) end test "GenServer timeout is reported", %{sender_ref: ref} do @@ -107,11 +110,13 @@ defmodule Sentry.LoggerHandlerTest do assert_receive {^ref, event} - assert event.exception.type == "message" + assert [exception] = event.exception + + assert exception.type == "message" - assert event.exception.value =~ "** (stop) exited in: GenServer.call(" - assert event.exception.value =~ "** (EXIT) time out" - assert length(event.exception.stacktrace.frames) > 0 + assert exception.value =~ "** (stop) exited in: GenServer.call(" + assert exception.value =~ "** (EXIT) time out" + assert length(exception.stacktrace.frames) > 0 end if System.otp_release() >= "26.0" do @@ -125,7 +130,7 @@ defmodule Sentry.LoggerHandlerTest do assert_receive {^ref, event} if System.otp_release() >= "26" do - assert [stacktrace_frame] = event.exception.stacktrace.frames + assert [stacktrace_frame] = hd(event.exception).stacktrace.frames assert stacktrace_frame.filename == "test/support/example_plug_application.ex" else assert event.message =~ "Error in process" @@ -259,8 +264,9 @@ defmodule Sentry.LoggerHandlerTest do assert event.user.user_id == 3 assert event.extra.day_of_week == "Friday" - assert event.exception.type == "RuntimeError" - assert event.exception.value == "oops" + assert [exception] = event.exception + assert exception.type == "RuntimeError" + assert exception.value == "oops" end test "handles malformed :callers metadata", %{sender_ref: ref} do