From f29b9d3b847602ea33634f82b72b224595fb41aa Mon Sep 17 00:00:00 2001 From: Andrew Selder Date: Mon, 14 Dec 2020 08:57:11 -0800 Subject: [PATCH 1/2] Send Sentry reports on uncaught throws/exits Fix #446 Previously PlugCapture would only catch exceptions. If there was an uncaught exit or throw, this would be ignored and Plug/Phoenix would end up send a 500 Internal Server Error to the clients with no notification to Sentry. --- lib/sentry/plug_capture.ex | 6 ++ test/plug_capture_test.exs | 88 ++++++++++++++++++++++++ test/support/example_plug_application.ex | 10 +++ 3 files changed, 104 insertions(+) diff --git a/lib/sentry/plug_capture.ex b/lib/sentry/plug_capture.ex index ec3b00a9..d5fa5729 100644 --- a/lib/sentry/plug_capture.ex +++ b/lib/sentry/plug_capture.ex @@ -45,6 +45,12 @@ defmodule Sentry.PlugCapture do e -> _ = Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug) :erlang.raise(:error, e, __STACKTRACE__) + catch + kind, reason -> + message = "Uncaught #{kind} - #{inspect(reason)}" + stack = __STACKTRACE__ + Sentry.capture_message(message, stacktrace: stack, event_source: :plug) + :erlang.raise(kind, reason, stack) end end end diff --git a/test/plug_capture_test.exs b/test/plug_capture_test.exs index 742ba64c..d50f96bf 100644 --- a/test/plug_capture_test.exs +++ b/test/plug_capture_test.exs @@ -7,6 +7,8 @@ defmodule Sentry.PlugCaptureTest do defmodule PhoenixController do use Phoenix.Controller def error(_conn, _params), do: raise("PhoenixError") + def exit(_conn, _params), do: exit(:test) + def throw(_conn, _params), do: throw(:test) def assigns(conn, _params) do _test = conn.assigns2.test @@ -17,6 +19,8 @@ defmodule Sentry.PlugCaptureTest do use Phoenix.Router get "/error_route", PhoenixController, :error + get "/exit_route", PhoenixController, :exit + get "/throw_route", PhoenixController, :throw get "/assigns_route", PhoenixController, :assigns end @@ -56,6 +60,40 @@ defmodule Sentry.PlugCaptureTest do end) end + test "sends throws to Sentry" do + bypass = Bypass.open() + + Bypass.expect(bypass, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + _json = Jason.decode!(body) + Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>) + end) + + modify_env(:sentry, dsn: "http://public:secret@localhost:#{bypass.port}/1") + + catch_throw( + conn(:get, "/throw_route") + |> Sentry.ExamplePlugApplication.call([]) + ) + end + + test "sends exits to Sentry" do + bypass = Bypass.open() + + Bypass.expect(bypass, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + _json = Jason.decode!(body) + Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>) + end) + + modify_env(:sentry, dsn: "http://public:secret@localhost:#{bypass.port}/1") + + catch_exit( + conn(:get, "/exit_route") + |> Sentry.ExamplePlugApplication.call([]) + ) + end + test "works with Sentry.PlugContext" do bypass = Bypass.open() @@ -131,6 +169,56 @@ defmodule Sentry.PlugCaptureTest do end) end + test "reports exits occurring in Phoenix Endpoint" do + bypass = Bypass.open() + + Bypass.expect(bypass, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + json = Jason.decode!(body) + assert json["culprit"] == "Sentry.PlugCaptureTest.PhoenixController.exit/2" + assert json["message"] == "Uncaught exit - :test" + Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>) + end) + + modify_env(:sentry, + dsn: "http://public:secret@localhost:#{bypass.port}/1", + "#{__MODULE__.PhoenixEndpoint}": [ + render_errors: [view: Sentry.ErrorView, accepts: ~w(html)] + ] + ) + + {:ok, _} = PhoenixEndpoint.start_link() + + capture_log(fn -> + catch_exit(conn(:get, "/exit_route") |> PhoenixEndpoint.call([])) + end) + end + + test "reports throws occurring in Phoenix Endpoint" do + bypass = Bypass.open() + + Bypass.expect(bypass, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + json = Jason.decode!(body) + assert json["culprit"] == "Sentry.PlugCaptureTest.PhoenixController.throw/2" + assert json["message"] == "Uncaught throw - :test" + Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>) + end) + + modify_env(:sentry, + dsn: "http://public:secret@localhost:#{bypass.port}/1", + "#{__MODULE__.PhoenixEndpoint}": [ + render_errors: [view: Sentry.ErrorView, accepts: ~w(html)] + ] + ) + + {:ok, _} = PhoenixEndpoint.start_link() + + capture_log(fn -> + catch_throw(conn(:get, "/throw_route") |> PhoenixEndpoint.call([])) + end) + end + test "can render feedback form in Phoenix ErrorView" do bypass = Bypass.open() diff --git a/test/support/example_plug_application.ex b/test/support/example_plug_application.ex index 09338715..bb6bb7f2 100644 --- a/test/support/example_plug_application.ex +++ b/test/support/example_plug_application.ex @@ -13,6 +13,16 @@ defmodule Sentry.ExamplePlugApplication do raise RuntimeError, "Error" end + get "/exit_route" do + _ = conn + exit(:test) + end + + get "/throw_route" do + _ = conn + throw(:test) + end + post "/error_route" do _ = conn raise RuntimeError, "Error" From 72d4c9a854b2a757ead03cbc4401049f004b8c4c Mon Sep 17 00:00:00 2001 From: Andrew Selder Date: Mon, 14 Dec 2020 09:36:29 -0800 Subject: [PATCH 2/2] Fix a doc test. --- test/sources_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sources_test.exs b/test/sources_test.exs index 59b96e27..eee72ccd 100644 --- a/test/sources_test.exs +++ b/test/sources_test.exs @@ -55,7 +55,7 @@ defmodule Sentry.SourcesTest do test "exception makes call to Sentry API" do correct_context = %{ "context_line" => " raise RuntimeError, \"Error\"", - "post_context" => [" end", "", " post \"/error_route\" do"], + "post_context" => [" end", "", " get \"/exit_route\" do"], "pre_context" => ["", " get \"/error_route\" do", " _ = conn"] }