|
1 | | -if( |
2 | | - Code.ensure_loaded?(Plug), |
3 | | - do: |
4 | | - defmodule Sentry.Plug do |
5 | | - @default_scrubbed_param_keys ["password", "passwd", "secret"] |
6 | | - @default_scrubbed_header_keys ["authorization", "authentication"] |
7 | | - @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ |
8 | | - @scrubbed_value "*********" |
9 | | - |
10 | | - @moduledoc """ |
11 | | - Provides basic functionality to handle Plug.ErrorHandler |
12 | | -
|
13 | | - #### Usage |
14 | | -
|
15 | | - Add the following to your router.ex: |
16 | | -
|
17 | | - use Plug.ErrorHandler |
18 | | - use Sentry.Plug |
19 | | -
|
20 | | - ### Sending Post Body Params |
21 | | -
|
22 | | - In order to send post body parameters you should first scrub them of sensitive |
23 | | - information. By default, they will be scrubbed with |
24 | | - `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing |
25 | | - the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map |
26 | | - to send. Setting `:body_scrubber` to `nil` will not send any data back. |
27 | | - If you would like to make use of Sentry's default scrubber behavior in a custom |
28 | | - scrubber, it can be called directly. An example configuration may look like |
29 | | - the following: |
30 | | -
|
31 | | - def scrub_params(conn) do |
32 | | - # Makes use of the default body_scrubber to avoid sending password |
33 | | - # and credit card information in plain text. To also prevent sending |
34 | | - # our sensitive "my_secret_field" and "other_sensitive_data" fields, |
35 | | - # we simply drop those keys. |
36 | | - Sentry.Plug.default_body_scrubber(conn) |
37 | | - |> Map.drop(["my_secret_field", "other_sensitive_data"]) |
38 | | - end |
| 1 | +if Code.ensure_loaded?(Plug) do |
| 2 | + defmodule Sentry.Plug do |
| 3 | + @default_scrubbed_param_keys ["password", "passwd", "secret"] |
| 4 | + @default_scrubbed_header_keys ["authorization", "authentication"] |
| 5 | + @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ |
| 6 | + @scrubbed_value "*********" |
| 7 | + |
| 8 | + @moduledoc """ |
| 9 | + Provides basic functionality to handle Plug.ErrorHandler |
| 10 | +
|
| 11 | + #### Usage |
| 12 | +
|
| 13 | + Add the following to your router.ex: |
| 14 | +
|
| 15 | + use Plug.ErrorHandler |
| 16 | + use Sentry.Plug |
| 17 | +
|
| 18 | + ### Sending Post Body Params |
| 19 | +
|
| 20 | + In order to send post body parameters you should first scrub them of sensitive |
| 21 | + information. By default, they will be scrubbed with |
| 22 | + `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing |
| 23 | + the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map |
| 24 | + to send. Setting `:body_scrubber` to `nil` will not send any data back. |
| 25 | + If you would like to make use of Sentry's default scrubber behavior in a custom |
| 26 | + scrubber, it can be called directly. An example configuration may look like |
| 27 | + the following: |
| 28 | +
|
| 29 | + def scrub_params(conn) do |
| 30 | + # Makes use of the default body_scrubber to avoid sending password |
| 31 | + # and credit card information in plain text. To also prevent sending |
| 32 | + # our sensitive "my_secret_field" and "other_sensitive_data" fields, |
| 33 | + # we simply drop those keys. |
| 34 | + Sentry.Plug.default_body_scrubber(conn) |
| 35 | + |> Map.drop(["my_secret_field", "other_sensitive_data"]) |
| 36 | + end |
39 | 37 |
|
40 | | - Then pass it into Sentry.Plug: |
| 38 | + Then pass it into Sentry.Plug: |
41 | 39 |
|
42 | | - use Sentry.Plug, body_scrubber: &scrub_params/1 |
| 40 | + use Sentry.Plug, body_scrubber: &scrub_params/1 |
43 | 41 |
|
44 | | - You can also pass it in as a `{module, fun}` like so: |
| 42 | + You can also pass it in as a `{module, fun}` like so: |
45 | 43 |
|
46 | | - use Sentry.Plug, body_scrubber: {MyModule, :scrub_params} |
| 44 | + use Sentry.Plug, body_scrubber: {MyModule, :scrub_params} |
47 | 45 |
|
48 | | - *Please Note*: If you are sending large files you will want to scrub them out. |
| 46 | + *Please Note*: If you are sending large files you will want to scrub them out. |
49 | 47 |
|
50 | | - ### Headers Scrubber |
| 48 | + ### Headers Scrubber |
51 | 49 |
|
52 | | - By default Sentry will scrub Authorization and Authentication headers from all |
53 | | - requests before sending them. It can be configured similarly to the body params |
54 | | - scrubber, but is configured with the `:header_scrubber` key. |
| 50 | + By default Sentry will scrub Authorization and Authentication headers from all |
| 51 | + requests before sending them. It can be configured similarly to the body params |
| 52 | + scrubber, but is configured with the `:header_scrubber` key. |
55 | 53 |
|
56 | | - def scrub_headers(conn) do |
57 | | - # default is: Sentry.Plug.default_header_scrubber(conn) |
58 | | - # |
59 | | - # We do not want to include Content-Type or User-Agent in reported |
60 | | - # headers, so we drop them. |
61 | | - Enum.into(conn.req_headers, %{}) |
62 | | - |> Map.drop(["content-type", "user-agent"]) |
63 | | - end |
| 54 | + def scrub_headers(conn) do |
| 55 | + # default is: Sentry.Plug.default_header_scrubber(conn) |
| 56 | + # |
| 57 | + # We do not want to include Content-Type or User-Agent in reported |
| 58 | + # headers, so we drop them. |
| 59 | + Enum.into(conn.req_headers, %{}) |
| 60 | + |> Map.drop(["content-type", "user-agent"]) |
| 61 | + end |
64 | 62 |
|
65 | | - Then pass it into Sentry.Plug: |
| 63 | + Then pass it into Sentry.Plug: |
66 | 64 |
|
67 | | - use Sentry.Plug, header_scrubber: &scrub_headers/1 |
| 65 | + use Sentry.Plug, header_scrubber: &scrub_headers/1 |
68 | 66 |
|
69 | | - It can also be passed in as a `{module, fun}` like so: |
| 67 | + It can also be passed in as a `{module, fun}` like so: |
70 | 68 |
|
71 | | - use Sentry.Plug, header_scrubber: {MyModule, :scrub_headers} |
| 69 | + use Sentry.Plug, header_scrubber: {MyModule, :scrub_headers} |
72 | 70 |
|
73 | | - To configure scrubbing body and header data, we can set both configuration keys: |
| 71 | + To configure scrubbing body and header data, we can set both configuration keys: |
74 | 72 |
|
75 | | - use Sentry.Plug, header_scrubber: &scrub_headers/1, body_scrubber: &scrub_params/1 |
| 73 | + use Sentry.Plug, header_scrubber: &scrub_headers/1, body_scrubber: &scrub_params/1 |
76 | 74 |
|
77 | | - ### Including Request Identifiers |
| 75 | + ### Including Request Identifiers |
78 | 76 |
|
79 | | - If you're using Phoenix, Plug.RequestId, or another method to set a request ID |
80 | | - response header, and would like to include that information with errors |
81 | | - reported by Sentry.Plug, the `:request_id_header` option allows you to set |
82 | | - which header key Sentry should check. It will default to "x-request-id", |
83 | | - which Plug.RequestId (and therefore Phoenix) also default to. |
| 77 | + If you're using Phoenix, Plug.RequestId, or another method to set a request ID |
| 78 | + response header, and would like to include that information with errors |
| 79 | + reported by Sentry.Plug, the `:request_id_header` option allows you to set |
| 80 | + which header key Sentry should check. It will default to "x-request-id", |
| 81 | + which Plug.RequestId (and therefore Phoenix) also default to. |
84 | 82 |
|
85 | | - use Sentry.Plug, request_id_header: "application-request-id" |
86 | | - """ |
| 83 | + use Sentry.Plug, request_id_header: "application-request-id" |
| 84 | + """ |
87 | 85 |
|
88 | | - @default_plug_request_id_header "x-request-id" |
| 86 | + @default_plug_request_id_header "x-request-id" |
89 | 87 |
|
90 | | - defmacro __using__(env) do |
91 | | - body_scrubber = Keyword.get(env, :body_scrubber, {__MODULE__, :default_body_scrubber}) |
| 88 | + defmacro __using__(env) do |
| 89 | + body_scrubber = Keyword.get(env, :body_scrubber, {__MODULE__, :default_body_scrubber}) |
92 | 90 |
|
93 | | - header_scrubber = |
94 | | - Keyword.get(env, :header_scrubber, {__MODULE__, :default_header_scrubber}) |
| 91 | + header_scrubber = Keyword.get(env, :header_scrubber, {__MODULE__, :default_header_scrubber}) |
95 | 92 |
|
96 | | - request_id_header = Keyword.get(env, :request_id_header) |
| 93 | + request_id_header = Keyword.get(env, :request_id_header) |
97 | 94 |
|
98 | | - quote do |
99 | | - # Ignore 404s for Plug routes |
100 | | - defp handle_errors(conn, %{reason: %FunctionClauseError{function: :do_match}}) do |
101 | | - nil |
102 | | - end |
| 95 | + quote do |
| 96 | + # Ignore 404s for Plug routes |
| 97 | + defp handle_errors(conn, %{reason: %FunctionClauseError{function: :do_match}}) do |
| 98 | + nil |
| 99 | + end |
103 | 100 |
|
104 | | - if :code.is_loaded(Phoenix) do |
105 | | - # Ignore 404s for Phoenix routes |
106 | | - defp handle_errors(conn, %{reason: %Phoenix.Router.NoRouteError{}}) do |
107 | | - nil |
108 | | - end |
| 101 | + if :code.is_loaded(Phoenix) do |
| 102 | + # Ignore 404s for Phoenix routes |
| 103 | + defp handle_errors(conn, %{reason: %Phoenix.Router.NoRouteError{}}) do |
| 104 | + nil |
109 | 105 | end |
| 106 | + end |
110 | 107 |
|
111 | | - defp handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do |
112 | | - opts = [ |
113 | | - body_scrubber: unquote(body_scrubber), |
114 | | - header_scrubber: unquote(header_scrubber), |
115 | | - request_id_header: unquote(request_id_header) |
116 | | - ] |
117 | | - |
118 | | - request = Sentry.Plug.build_request_interface_data(conn, opts) |
119 | | - exception = Exception.normalize(kind, reason, stack) |
120 | | - |
121 | | - Sentry.capture_exception( |
122 | | - exception, |
123 | | - stacktrace: stack, |
124 | | - request: request, |
125 | | - event_source: :plug, |
126 | | - error_type: kind |
127 | | - ) |
128 | | - end |
| 108 | + defp handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do |
| 109 | + opts = [ |
| 110 | + body_scrubber: unquote(body_scrubber), |
| 111 | + header_scrubber: unquote(header_scrubber), |
| 112 | + request_id_header: unquote(request_id_header) |
| 113 | + ] |
| 114 | + |
| 115 | + request = Sentry.Plug.build_request_interface_data(conn, opts) |
| 116 | + exception = Exception.normalize(kind, reason, stack) |
| 117 | + |
| 118 | + Sentry.capture_exception( |
| 119 | + exception, |
| 120 | + stacktrace: stack, |
| 121 | + request: request, |
| 122 | + event_source: :plug, |
| 123 | + error_type: kind |
| 124 | + ) |
129 | 125 | end |
130 | 126 | end |
| 127 | + end |
131 | 128 |
|
132 | | - @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() |
133 | | - def build_request_interface_data(%Plug.Conn{} = conn, opts) do |
134 | | - body_scrubber = Keyword.get(opts, :body_scrubber) |
135 | | - header_scrubber = Keyword.get(opts, :header_scrubber) |
136 | | - request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header |
137 | | - |
138 | | - conn = |
139 | | - Plug.Conn.fetch_cookies(conn) |
140 | | - |> Plug.Conn.fetch_query_params() |
141 | | - |
142 | | - %{ |
143 | | - url: "#{conn.scheme}://#{conn.host}:#{conn.port}#{conn.request_path}", |
144 | | - method: conn.method, |
145 | | - data: handle_data(conn, body_scrubber), |
146 | | - query_string: conn.query_string, |
147 | | - cookies: conn.req_cookies, |
148 | | - headers: handle_data(conn, header_scrubber), |
149 | | - env: %{ |
150 | | - "REMOTE_ADDR" => remote_address(conn.remote_ip), |
151 | | - "REMOTE_PORT" => remote_port(conn.peer), |
152 | | - "SERVER_NAME" => conn.host, |
153 | | - "SERVER_PORT" => conn.port, |
154 | | - "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() |
155 | | - } |
| 129 | + @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() |
| 130 | + def build_request_interface_data(%Plug.Conn{} = conn, opts) do |
| 131 | + body_scrubber = Keyword.get(opts, :body_scrubber) |
| 132 | + header_scrubber = Keyword.get(opts, :header_scrubber) |
| 133 | + request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header |
| 134 | + |
| 135 | + conn = |
| 136 | + Plug.Conn.fetch_cookies(conn) |
| 137 | + |> Plug.Conn.fetch_query_params() |
| 138 | + |
| 139 | + %{ |
| 140 | + url: "#{conn.scheme}://#{conn.host}:#{conn.port}#{conn.request_path}", |
| 141 | + method: conn.method, |
| 142 | + data: handle_data(conn, body_scrubber), |
| 143 | + query_string: conn.query_string, |
| 144 | + cookies: conn.req_cookies, |
| 145 | + headers: handle_data(conn, header_scrubber), |
| 146 | + env: %{ |
| 147 | + "REMOTE_ADDR" => remote_address(conn.remote_ip), |
| 148 | + "REMOTE_PORT" => remote_port(conn.peer), |
| 149 | + "SERVER_NAME" => conn.host, |
| 150 | + "SERVER_PORT" => conn.port, |
| 151 | + "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() |
156 | 152 | } |
157 | | - end |
| 153 | + } |
| 154 | + end |
158 | 155 |
|
159 | | - defp remote_address(address) do |
160 | | - address |
161 | | - |> :inet.ntoa() |
162 | | - |> to_string() |
163 | | - end |
| 156 | + defp remote_address(address) do |
| 157 | + address |
| 158 | + |> :inet.ntoa() |
| 159 | + |> to_string() |
| 160 | + end |
164 | 161 |
|
165 | | - defp remote_port({_, port}), do: port |
| 162 | + defp remote_port({_, port}), do: port |
166 | 163 |
|
167 | | - defp handle_data(_conn, nil), do: %{} |
| 164 | + defp handle_data(_conn, nil), do: %{} |
168 | 165 |
|
169 | | - defp handle_data(conn, {module, fun}) do |
170 | | - apply(module, fun, [conn]) |
171 | | - end |
| 166 | + defp handle_data(conn, {module, fun}) do |
| 167 | + apply(module, fun, [conn]) |
| 168 | + end |
172 | 169 |
|
173 | | - defp handle_data(conn, fun) when is_function(fun) do |
174 | | - fun.(conn) |
175 | | - end |
| 170 | + defp handle_data(conn, fun) when is_function(fun) do |
| 171 | + fun.(conn) |
| 172 | + end |
176 | 173 |
|
177 | | - @spec default_header_scrubber(Plug.Conn.t()) :: map() |
178 | | - def default_header_scrubber(conn) do |
179 | | - Enum.into(conn.req_headers, %{}) |
180 | | - |> Map.drop(@default_scrubbed_header_keys) |
181 | | - end |
| 174 | + @spec default_header_scrubber(Plug.Conn.t()) :: map() |
| 175 | + def default_header_scrubber(conn) do |
| 176 | + Enum.into(conn.req_headers, %{}) |
| 177 | + |> Map.drop(@default_scrubbed_header_keys) |
| 178 | + end |
182 | 179 |
|
183 | | - @spec default_body_scrubber(Plug.Conn.t()) :: map() |
184 | | - def default_body_scrubber(conn) do |
185 | | - scrub_map(conn.params) |
186 | | - end |
| 180 | + @spec default_body_scrubber(Plug.Conn.t()) :: map() |
| 181 | + def default_body_scrubber(conn) do |
| 182 | + scrub_map(conn.params) |
| 183 | + end |
187 | 184 |
|
188 | | - defp scrub_map(map) do |
189 | | - Enum.map(map, fn {key, value} -> |
190 | | - value = |
191 | | - cond do |
192 | | - Enum.member?(@default_scrubbed_param_keys, key) -> |
193 | | - @scrubbed_value |
| 185 | + defp scrub_map(map) do |
| 186 | + Enum.map(map, fn {key, value} -> |
| 187 | + value = |
| 188 | + cond do |
| 189 | + Enum.member?(@default_scrubbed_param_keys, key) -> |
| 190 | + @scrubbed_value |
194 | 191 |
|
195 | | - is_binary(value) && Regex.match?(@credit_card_regex, value) -> |
196 | | - @scrubbed_value |
| 192 | + is_binary(value) && Regex.match?(@credit_card_regex, value) -> |
| 193 | + @scrubbed_value |
197 | 194 |
|
198 | | - is_map(value) && !Map.has_key?(value, :__struct__) -> |
199 | | - scrub_map(value) |
| 195 | + is_map(value) && !Map.has_key?(value, :__struct__) -> |
| 196 | + scrub_map(value) |
200 | 197 |
|
201 | | - true -> |
202 | | - value |
203 | | - end |
| 198 | + true -> |
| 199 | + value |
| 200 | + end |
204 | 201 |
|
205 | | - {key, value} |
206 | | - end) |
207 | | - |> Enum.into(%{}) |
208 | | - end |
| 202 | + {key, value} |
| 203 | + end) |
| 204 | + |> Enum.into(%{}) |
209 | 205 | end |
210 | | -) |
| 206 | + end |
| 207 | +end |
0 commit comments