Skip to content

Commit 00019eb

Browse files
Merge pull request #259 from getsentry/phoenix-endpoint-errors
Capture Phoenix.Endpoint errors
2 parents 9c3f8c1 + ba39032 commit 00019eb

File tree

13 files changed

+141
-24
lines changed

13 files changed

+141
-24
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,22 @@ For optional settings check the [docs](https://hexdocs.pm/sentry/readme.html).
3838

3939
### Setup with Plug or Phoenix
4040

41-
In your router add the following lines:
41+
In your Plug.Router or Phoenix.Router, add the following lines:
4242

4343
```elixir
4444
use Plug.ErrorHandler
4545
use Sentry.Plug
4646
```
4747

48+
If you are using Phoenix, you can also include [Sentry.Phoenix.Endpoint](https://hexdocs.pm/sentry/Sentry.Phoenix.Endpoint.html) in your Endpoint. This module captures errors occurring in the Phoenix pipeline before the request reaches the Router:
49+
50+
```elixir
51+
use Phoenix.Endpoint, otp_app: :my_app
52+
use Sentry.Phoenix.Endpoint
53+
```
54+
55+
More information on why this may be necessary can be found here: https://github.com/getsentry/sentry-elixir/issues/229 and https://github.com/phoenixframework/phoenix/issues/2791
56+
4857
### Capture All Exceptions
4958

5059
This library comes with an extension to capture all error messages that the Plug handler might not. This is based on the Erlang [error_logger](http://erlang.org/doc/man/error_logger.html).

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Filtering Events
115115
If you would like to prevent certain exceptions, the ``:filter`` configuration option
116116
allows you to implement the ``Sentry.EventFilter`` behaviour. The first argument is the
117117
exception to be sent, and the second is the source of the event. ``Sentry.Plug``
118-
will have a source of ``:plug``, and ``Sentry.Logger`` will have a source of ``:logger``.
118+
will have a source of ``:plug``, ``Sentry.Logger`` will have a source of ``:logger``, and ``Sentry.Phoenix.Endpoint`` will have a source of ``:endpoint``.
119119
If an exception does not come from either of those sources, the source will be nil
120120
unless the ``:event_source`` option is passed to ``Sentry.capture_exception/2``
121121

lib/sentry.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule Sentry do
5454
If you would like to prevent certain exceptions, the `:filter` configuration option
5555
allows you to implement the `Sentry.EventFilter` behaviour. The first argument is the
5656
exception to be sent, and the second is the source of the event. `Sentry.Plug`
57-
will have a source of `:plug`, and `Sentry.Logger` will have a source of `:logger`.
57+
will have a source of `:plug`, `Sentry.Logger` will have a source of `:logger`, and `Sentry.Phoenix.Endpoint` will have a source of `:endpoint`.
5858
If an exception does not come from either of those sources, the source will be nil
5959
unless the `:event_source` option is passed to `Sentry.capture_exception/2`
6060

lib/sentry/event_filter.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
defmodule Sentry.EventFilter do
2-
@moduledoc """
3-
4-
"""
2+
@moduledoc false
53

64
@callback exclude_exception?(Exception.t(), atom) :: any
75
end

lib/sentry/phoenix_endpoint.ex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
defmodule Sentry.Phoenix.Endpoint do
2+
@moduledoc """
3+
Provides basic functionality to handle errors in a Phoenix Endpoint. Errors occurring within a Phoenix request before it reaches the Router will not be captured by `Sentry.Plug` due to the internal functionality of Phoenix.
4+
5+
It is recommended to include `Sentry.Phoenix.Endpoint` in your Phoenix app if you would like to receive errors occurring in the previously mentioned circumstances.
6+
7+
For more information, see https://github.com/getsentry/sentry-elixir/issues/229 and https://github.com/phoenixframework/phoenix/issues/2791.
8+
9+
10+
#### Usage
11+
12+
Add the following to your endpoint.ex, below `use Phoenix.Endpoint, otp_app: :my_app`
13+
14+
use Sentry.Phoenix.Endpoint
15+
16+
"""
17+
defmacro __using__(_opts) do
18+
quote do
19+
@before_compile Sentry.Phoenix.Endpoint
20+
end
21+
end
22+
23+
defmacro __before_compile__(_) do
24+
quote do
25+
defoverridable call: 2
26+
27+
def call(conn, opts) do
28+
try do
29+
super(conn, opts)
30+
catch
31+
kind, reason ->
32+
stacktrace = System.stacktrace()
33+
request = Sentry.Plug.build_request_interface_data(conn, [])
34+
exception = Exception.normalize(kind, reason, stacktrace)
35+
36+
Sentry.capture_exception(
37+
exception,
38+
stacktrace: stacktrace,
39+
request: request,
40+
event_source: :endpoint,
41+
error_type: kind
42+
)
43+
44+
:erlang.raise(kind, reason, stacktrace)
45+
end
46+
end
47+
end
48+
end
49+
end

lib/sentry/plug.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,14 @@ if Code.ensure_loaded?(Plug) do
148148

149149
@spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map()
150150
def build_request_interface_data(%Plug.Conn{} = conn, opts) do
151-
body_scrubber = Keyword.get(opts, :body_scrubber)
152-
header_scrubber = Keyword.get(opts, :header_scrubber)
153-
cookie_scrubber = Keyword.get(opts, :cookie_scrubber)
151+
body_scrubber = Keyword.get(opts, :body_scrubber, {__MODULE__, :default_body_scrubber})
152+
153+
header_scrubber =
154+
Keyword.get(opts, :header_scrubber, {__MODULE__, :default_header_scrubber})
155+
156+
cookie_scrubber =
157+
Keyword.get(opts, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber})
158+
154159
request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header
155160

156161
conn =

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule Sentry.Mixfile do
2828
{:uuid, "~> 1.0"},
2929
{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0"},
3030
{:plug, "~> 1.0", optional: true},
31+
{:phoenix, "~> 1.3", optional: true},
3132
{:dialyxir, "> 0.0.0", only: [:dev], runtime: false},
3233
{:ex_doc, "~> 0.18.0", only: :dev},
3334
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},

mix.lock

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
%{
22
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
33
"bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
4-
"certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"},
4+
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
55
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
66
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
7-
"credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
7+
"credo": {:hex, :credo, "0.9.0", "5d1b494e4f2dc672b8318e027bd833dda69be71eaac6eedd994678be74ef7cb4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
88
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"},
99
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"},
1010
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
11-
"hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
12-
"idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
11+
"hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
12+
"idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
1313
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
1414
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
1515
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
16-
"plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
16+
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
17+
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
18+
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
19+
"plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
1720
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
1821
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
1922
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
2023
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []},
21-
"unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], [], "hexpm"},
24+
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
2225
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"},
2326
}

test/event_test.exs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Sentry.EventTest do
2-
use ExUnit.Case, async: true
2+
use ExUnit.Case
33
alias Sentry.Event
44
import Sentry.TestEnvironmentHelper
55

@@ -448,22 +448,25 @@ defmodule Sentry.EventTest do
448448
event = Sentry.Event.transform_exception(exception, [])
449449

450450
assert event.modules == %{
451+
phoenix: "1.3.2",
452+
phoenix_pubsub: "1.0.2",
451453
bunt: "0.2.0",
452454
bypass: "0.8.1",
453-
certifi: "1.2.1",
455+
certifi: "2.3.1",
454456
cowboy: "1.1.2",
455457
cowlib: "1.0.2",
456-
credo: "0.8.10",
457-
hackney: "1.8.6",
458-
idna: "5.0.2",
458+
credo: "0.9.0",
459+
hackney: "1.12.1",
460+
idna: "5.1.1",
459461
metrics: "1.0.1",
460462
mime: "1.2.0",
461463
mimerl: "1.0.2",
462-
plug: "1.4.3",
464+
parse_trans: "3.2.0",
465+
plug: "1.5.0",
463466
poison: "3.1.0",
464467
ranch: "1.3.2",
465468
ssl_verify_fun: "1.1.1",
466-
unicode_util_compat: "0.2.0",
469+
unicode_util_compat: "0.3.1",
467470
uuid: "1.1.8"
468471
}
469472
end

test/phoenix_endpoint_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule Sentry.PhoenixEndpointTest do
2+
use ExUnit.Case
3+
use Plug.Test
4+
import Sentry.TestEnvironmentHelper
5+
6+
Application.put_env(
7+
:sentry,
8+
__MODULE__.Endpoint,
9+
render_errors: [view: Sentry.ErrorView, accepts: ~w(html)]
10+
)
11+
12+
defmodule Endpoint do
13+
use Phoenix.Endpoint, otp_app: :sentry
14+
use Sentry.Phoenix.Endpoint
15+
plug(:error)
16+
plug(Sentry.ExampleApp)
17+
18+
def error(_conn, _opts) do
19+
raise "EndpointError"
20+
end
21+
end
22+
23+
test "reports errors occurring in Phoenix Endpoint" do
24+
bypass = Bypass.open()
25+
26+
Bypass.expect(bypass, fn conn ->
27+
{:ok, body, conn} = Plug.Conn.read_body(conn)
28+
json = Poison.decode!(body)
29+
assert json["culprit"] == "Sentry.PhoenixEndpointTest.Endpoint.error/2"
30+
assert json["message"] == "(RuntimeError) EndpointError"
31+
Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>)
32+
end)
33+
34+
modify_env(:sentry, dsn: "http://public:secret@localhost:#{bypass.port}/1")
35+
modify_env(:phoenix, format_encoders: [])
36+
{:ok, _} = Endpoint.start_link()
37+
38+
assert_raise RuntimeError, "EndpointError", fn ->
39+
conn(:get, "/")
40+
|> Endpoint.call([])
41+
end
42+
end
43+
end

0 commit comments

Comments
 (0)