11defmodule Sentry.PlugCapture do
22 @ moduledoc """
3- Provides basic functionality to handle and send errors occurring within
3+ Provides basic functionality to capture and send errors occurring within
44 Plug applications, including Phoenix.
55
6- It is intended for usage with `Sentry.PlugContext`.
6+ It is intended for usage with `Sentry.PlugContext`, which adds relevant request
7+ metadata to the Sentry context before errors are captured.
78
89 ## Usage
910
11+ ### With Phoenix
12+
1013 In a Phoenix application, it is important to use this module **before**
1114 the Phoenix endpoint itself. It should be added to your `endpoint.ex` file:
1215
@@ -17,7 +20,9 @@ defmodule Sentry.PlugCapture do
1720 # ...
1821 end
1922
20- In a Plug application, it can be added *below* your router:
23+ ### With Plug
24+
25+ In a Plug application, you can add this module *below* your router:
2126
2227 defmodule MyApp.PlugRouter do
2328 use Plug.Router
@@ -32,30 +37,76 @@ defmodule Sentry.PlugCapture do
3237 > and adds capturing errors and reporting to Sentry. You can still re-override
3338 > that callback after `use Sentry.PlugCapture` if you need to.
3439
40+ ## Scrubbing Sensitive Data
41+
42+ > #### Since v9.1.0 {: .neutral}
43+ >
44+ > Scrubbing sensitive data in `Sentry.PlugCapture` is available since v9.1.0
45+ > of this library.
46+
47+ Like `Sentry.PlugContext`, this module also supports scrubbing sensitive data
48+ out of errors. However, this module has to do some *guessing* to figure
49+ out if there are `Plug.Conn` structs to scrub. Right now, the strategy we
50+ use follows these steps:
51+
52+ 1. if the error is `Phoenix.ActionClauseError`, we scrub the `Plug.Conn` structs
53+ from the `args` field of that exception
54+
55+ Otherwise, we don't perform any scrubbing. To configure scrubbing, you can use the
56+ `:scrubbing` option (see below).
57+
58+ ## Options
59+
60+ * `:scrubber` (since v9.1.0) - a term of type `{module, function, args}` that
61+ will be invoked to scrub sensitive data from `Plug.Conn` structs. The
62+ `Plug.Conn` struct is prepended to `args` before invoking the function,
63+ so that the final function will be called as `apply(module, function, [conn | args])`.
64+ The function must return a `Plug.Conn` struct. By default, the built-in
65+ scrubber does this:
66+
67+ * scrubs *all* cookies
68+ * scrubs sensitive headers just like `Sentry.PlugContext.default_header_scrubber/1`
69+ * scrubs sensitive body params just like `Sentry.PlugContext.default_body_scrubber/1`
70+
3571 """
3672
37- defmacro __using__ ( _opts ) do
73+ defmacro __using__ ( opts ) do
3874 quote do
75+ opts = unquote ( Macro . escape ( opts ) )
76+ default_scrubber = { unquote ( __MODULE__ ) , :default_scrubber , [ ] }
77+
78+ scrubber =
79+ case Keyword . get ( opts , :scrubber , default_scrubber ) do
80+ { mod , fun , args } = scrubber when is_atom ( mod ) and is_atom ( fun ) and is_list ( args ) ->
81+ scrubber
82+
83+ other ->
84+ raise ArgumentError ,
85+ "expected :scrubber to be a {module, function, args} tuple, got: #{ inspect ( other ) } "
86+ end
87+
88+ @ __sentry_scrubber scrubber
89+
3990 @ before_compile Sentry.PlugCapture
4091 end
4192 end
4293
43- defmacro __before_compile__ ( _ ) do
94+ defmacro __before_compile__ ( _env ) do
4495 quote do
4596 defoverridable call: 2
4697
4798 def call ( conn , opts ) do
4899 try do
49100 super ( conn , opts )
50101 rescue
51- e in Plug.Conn.WrapperError ->
52- exception = Exception . normalize ( :error , e . reason , e . stack )
53- _ = Sentry . capture_exception ( exception , stacktrace: e . stack , event_source: :plug )
54- Plug.Conn.WrapperError . reraise ( e )
55-
56- e ->
57- _ = Sentry . capture_exception ( e , stacktrace: __STACKTRACE__ , event_source: :plug )
58- :erlang . raise ( :error , e , __STACKTRACE__ )
102+ err in Plug.Conn.WrapperError ->
103+ exception = Exception . normalize ( :error , err . reason , err . stack )
104+ Sentry.PlugCapture . __capture_exception__ ( exception , err . stack , @ __sentry_scrubber )
105+ Plug.Conn.WrapperError . reraise ( err )
106+
107+ exc ->
108+ Sentry.PlugCapture . __capture_exception__ ( exc , __STACKTRACE__ , @ __sentry_scrubber )
109+ :erlang . raise ( :error , exc , __STACKTRACE__ )
59110 catch
60111 kind , reason ->
61112 message = "Uncaught #{ kind } - #{ inspect ( reason ) } "
@@ -66,4 +117,38 @@ defmodule Sentry.PlugCapture do
66117 end
67118 end
68119 end
120+
121+ @ doc false
122+ def __capture_exception__ ( exception , stacktrace , scrubber ) do
123+ # We can't pattern match here, because we're not guaranteed to have
124+ # Phoenix available.
125+ exception =
126+ if is_struct ( exception , Phoenix.ActionClauseError ) do
127+ update_in ( exception , [ Access . key! ( :args ) , Access . all ( ) ] , fn
128+ conn when is_struct ( conn , Plug.Conn ) -> apply_scrubber ( conn , scrubber )
129+ other -> other
130+ end )
131+ else
132+ exception
133+ end
134+
135+ Sentry . capture_exception ( exception , stacktrace: stacktrace , event_source: :plug )
136+ end
137+
138+ @ doc false
139+ def default_scrubber ( conn ) do
140+ % {
141+ conn
142+ | cookies: % { } ,
143+ req_headers: Sentry.PlugContext . default_header_scrubber ( conn ) ,
144+ params: Sentry.PlugContext . default_body_scrubber ( conn )
145+ }
146+ end
147+
148+ defp apply_scrubber ( conn , { mod , fun , args } = _scrubber ) do
149+ case apply ( mod , fun , [ conn | args ] ) do
150+ conn when is_struct ( conn , Plug.Conn ) -> conn
151+ other -> raise ":scrubber function must return a Plug.Conn struct, got: #{ inspect ( other ) } "
152+ end
153+ end
69154end
0 commit comments