diff --git a/lib/sentry/plug_capture.ex b/lib/sentry/plug_capture.ex index 3475e275..aa077b3f 100644 --- a/lib/sentry/plug_capture.ex +++ b/lib/sentry/plug_capture.ex @@ -1,51 +1,49 @@ -if Code.ensure_loaded?(Plug) do - defmodule Sentry.PlugCapture do - @moduledoc """ - Provides basic functionality to handle and send errors occurring within - Plug applications, including Phoenix. - It is intended for usage with `Sentry.PlugContext`. - - #### Usage - In a Phoenix application, it is important to use this module before - the Phoenix endpoint itself. It should be added to your endpoint.ex: - - - defmodule MyApp.Endpoint - use Sentry.PlugCapture - use Phoenix.Endpoint, otp_app: :my_app - # ... - end +defmodule Sentry.PlugCapture do + @moduledoc """ + Provides basic functionality to handle and send errors occurring within + Plug applications, including Phoenix. + It is intended for usage with `Sentry.PlugContext`. - In a Plug application, it can be added below your router: + #### Usage + In a Phoenix application, it is important to use this module before + the Phoenix endpoint itself. It should be added to your endpoint.ex: - defmodule MyApp.PlugRouter do - use Plug.Router - use Sentry.PlugCapture - # ... - end - """ - defmacro __using__(_opts) do - quote do - @before_compile Sentry.PlugCapture + + defmodule MyApp.Endpoint + use Sentry.PlugCapture + use Phoenix.Endpoint, otp_app: :my_app + # ... end + + In a Plug application, it can be added below your router: + + defmodule MyApp.PlugRouter do + use Plug.Router + use Sentry.PlugCapture + # ... + end + """ + defmacro __using__(_opts) do + quote do + @before_compile Sentry.PlugCapture end + end - defmacro __before_compile__(_) do - quote do - defoverridable call: 2 - - def call(conn, opts) do - try do - super(conn, opts) - rescue - e in Plug.Conn.WrapperError -> - Sentry.capture_exception(e.reason, stacktrace: e.stack, event_source: :plug) - Plug.Conn.WrapperError.reraise(e) - - e -> - Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug) - :erlang.raise(:error, e, __STACKTRACE__) - end + defmacro __before_compile__(_) do + quote do + defoverridable call: 2 + + def call(conn, opts) do + try do + super(conn, opts) + rescue + e in Plug.Conn.WrapperError -> + Sentry.capture_exception(e.reason, stacktrace: e.stack, event_source: :plug) + Plug.Conn.WrapperError.reraise(e) + + e -> + Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug) + :erlang.raise(:error, e, __STACKTRACE__) end end end diff --git a/lib/sentry/plug_context.ex b/lib/sentry/plug_context.ex index 8210fcd3..f0f54cb0 100644 --- a/lib/sentry/plug_context.ex +++ b/lib/sentry/plug_context.ex @@ -1,199 +1,199 @@ -if Code.ensure_loaded?(Plug) do - defmodule Sentry.PlugContext do - @moduledoc """ - This module adds Sentry context metadata during the request in a Plug - application. It includes defaults for scrubbing sensitive data, and - options for customizing it by default. - - It is intended for usage with `Sentry.PlugCapture` as metadata added here - will appear in events captured. - - ### Sending Post Body Params - - In order to send post body parameters you should first scrub them of sensitive - information. By default, they will be scrubbed with - `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing - the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map - to send. Setting `:body_scrubber` to `nil` will not send any data back. - If you would like to make use of Sentry's default scrubber behavior in a custom - scrubber, it can be called directly. An example configuration may look like - the following: - - def scrub_params(conn) do - # Makes use of the default body_scrubber to avoid sending password - # and credit card information in plain text. To also prevent sending - # our sensitive "my_secret_field" and "other_sensitive_data" fields, - # we simply drop those keys. - Sentry.Plug.default_body_scrubber(conn) - |> Map.drop(["my_secret_field", "other_sensitive_data"]) - end +defmodule Sentry.PlugContext do + @moduledoc """ + This module adds Sentry context metadata during the request in a Plug + application. It includes defaults for scrubbing sensitive data, and + options for customizing it by default. + + It is intended for usage with `Sentry.PlugCapture` as metadata added here + will appear in events captured. + + ### Sending Post Body Params + + In order to send post body parameters you should first scrub them of sensitive + information. By default, they will be scrubbed with + `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing + the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map + to send. Setting `:body_scrubber` to `nil` will not send any data back. + If you would like to make use of Sentry's default scrubber behavior in a custom + scrubber, it can be called directly. An example configuration may look like + the following: + + def scrub_params(conn) do + # Makes use of the default body_scrubber to avoid sending password + # and credit card information in plain text. To also prevent sending + # our sensitive "my_secret_field" and "other_sensitive_data" fields, + # we simply drop those keys. + Sentry.Plug.default_body_scrubber(conn) + |> Map.drop(["my_secret_field", "other_sensitive_data"]) + end - Then pass it into Sentry.Plug: + Then pass it into Sentry.Plug: - use Sentry.PlugContext, body_scrubber: &scrub_params/1 + use Sentry.PlugContext, body_scrubber: &scrub_params/1 - You can also pass it in as a `{module, fun}` like so: + You can also pass it in as a `{module, fun}` like so: - use Sentry.PlugContext, body_scrubber: {MyModule, :scrub_params} + use Sentry.PlugContext, body_scrubber: {MyModule, :scrub_params} - *Please Note*: If you are sending large files you will want to scrub them out. + *Please Note*: If you are sending large files you will want to scrub them out. - ### Headers Scrubber + ### Headers Scrubber - By default Sentry will scrub Authorization and Authentication headers from all - requests before sending them. It can be configured similarly to the body params - scrubber, but is configured with the `:header_scrubber` key. + By default Sentry will scrub Authorization and Authentication headers from all + requests before sending them. It can be configured similarly to the body params + scrubber, but is configured with the `:header_scrubber` key. - def scrub_headers(conn) do - # default is: Sentry.Plug.default_header_scrubber(conn) - # - # We do not want to include Content-Type or User-Agent in reported - # headers, so we drop them. - Enum.into(conn.req_headers, %{}) - |> Map.drop(["content-type", "user-agent"]) - end + def scrub_headers(conn) do + # default is: Sentry.Plug.default_header_scrubber(conn) + # + # We do not want to include Content-Type or User-Agent in reported + # headers, so we drop them. + Enum.into(conn.req_headers, %{}) + |> Map.drop(["content-type", "user-agent"]) + end - Then pass it into Sentry.Plug: + Then pass it into Sentry.Plug: - use Sentry.PlugContext, header_scrubber: &scrub_headers/1 + use Sentry.PlugContext, header_scrubber: &scrub_headers/1 - It can also be passed in as a `{module, fun}` like so: + It can also be passed in as a `{module, fun}` like so: - use Sentry.PlugContext, header_scrubber: {MyModule, :scrub_headers} + use Sentry.PlugContext, header_scrubber: {MyModule, :scrub_headers} - ### Cookie Scrubber + ### Cookie Scrubber - By default Sentry will scrub all cookies before sending events. - It can be configured similarly to the headers scrubber, but is configured with the `:cookie_scrubber` key. + By default Sentry will scrub all cookies before sending events. + It can be configured similarly to the headers scrubber, but is configured with the `:cookie_scrubber` key. - To configure scrubbing, we can set all configuration keys: + To configure scrubbing, we can set all configuration keys: - use Sentry.PlugContext, header_scrubber: &scrub_headers/1, - body_scrubber: &scrub_params/1, cookie_scrubber: &scrub_cookies/1 + use Sentry.PlugContext, header_scrubber: &scrub_headers/1, + body_scrubber: &scrub_params/1, cookie_scrubber: &scrub_cookies/1 - ### Including Request Identifiers + ### Including Request Identifiers - If you're using Phoenix, Plug.RequestId, or another method to set a request ID - response header, and would like to include that information with errors - reported by Sentry.PlugContext, the `:request_id_header` option allows you to set - which header key Sentry should check. It will default to "x-request-id", - which Plug.RequestId (and therefore Phoenix) also default to. + If you're using Phoenix, Plug.RequestId, or another method to set a request ID + response header, and would like to include that information with errors + reported by Sentry.PlugContext, the `:request_id_header` option allows you to set + which header key Sentry should check. It will default to "x-request-id", + which Plug.RequestId (and therefore Phoenix) also default to. - use Sentry.PlugContext, request_id_header: "application-request-id" - """ - @behaviour Plug + use Sentry.PlugContext, request_id_header: "application-request-id" + """ - @default_scrubbed_param_keys ["password", "passwd", "secret"] - @default_scrubbed_header_keys ["authorization", "authentication", "cookie"] - @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ - @scrubbed_value "*********" - @default_plug_request_id_header "x-request-id" + if Code.ensure_loaded?(Plug) do + @behaviour Plug + end - def init(opts) do - opts - end + @default_scrubbed_param_keys ["password", "passwd", "secret"] + @default_scrubbed_header_keys ["authorization", "authentication", "cookie"] + @credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/ + @scrubbed_value "*********" + @default_plug_request_id_header "x-request-id" - def call(conn, opts) do - request = build_request_interface_data(conn, opts) + def init(opts) do + opts + end - Sentry.Context.set_request_context(request) - conn - end + def call(conn, opts) do + request = build_request_interface_data(conn, opts) + Sentry.Context.set_request_context(request) + conn + end - @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() - def build_request_interface_data(%Plug.Conn{} = conn, opts) do - body_scrubber = Keyword.get(opts, :body_scrubber, {__MODULE__, :default_body_scrubber}) - - header_scrubber = - Keyword.get(opts, :header_scrubber, {__MODULE__, :default_header_scrubber}) - - cookie_scrubber = - Keyword.get(opts, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber}) - - request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header - - conn = - Plug.Conn.fetch_cookies(conn) - |> Plug.Conn.fetch_query_params() - - %{ - url: Plug.Conn.request_url(conn), - method: conn.method, - data: handle_data(conn, body_scrubber), - query_string: conn.query_string, - cookies: handle_data(conn, cookie_scrubber), - headers: handle_data(conn, header_scrubber), - env: %{ - "REMOTE_ADDR" => remote_address(conn.remote_ip), - "REMOTE_PORT" => Plug.Conn.get_peer_data(conn).port, - "SERVER_NAME" => conn.host, - "SERVER_PORT" => conn.port, - "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() - } + @spec build_request_interface_data(Plug.Conn.t(), keyword()) :: map() + def build_request_interface_data(conn, opts) do + body_scrubber = Keyword.get(opts, :body_scrubber, {__MODULE__, :default_body_scrubber}) + + header_scrubber = + Keyword.get(opts, :header_scrubber, {__MODULE__, :default_header_scrubber}) + + cookie_scrubber = + Keyword.get(opts, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber}) + + request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header + + conn = + Plug.Conn.fetch_cookies(conn) + |> Plug.Conn.fetch_query_params() + + %{ + url: Plug.Conn.request_url(conn), + method: conn.method, + data: handle_data(conn, body_scrubber), + query_string: conn.query_string, + cookies: handle_data(conn, cookie_scrubber), + headers: handle_data(conn, header_scrubber), + env: %{ + "REMOTE_ADDR" => remote_address(conn.remote_ip), + "REMOTE_PORT" => Plug.Conn.get_peer_data(conn).port, + "SERVER_NAME" => conn.host, + "SERVER_PORT" => conn.port, + "REQUEST_ID" => Plug.Conn.get_resp_header(conn, request_id) |> List.first() } - end + } + end - defp remote_address(address) do - address - |> :inet.ntoa() - |> case do - {:error, _} -> - "" + defp remote_address(address) do + address + |> :inet.ntoa() + |> case do + {:error, _} -> + "" - address -> - to_string(address) - end + address -> + to_string(address) end + end - defp handle_data(_conn, nil), do: %{} + defp handle_data(_conn, nil), do: %{} - defp handle_data(conn, {module, fun}) do - apply(module, fun, [conn]) - end + defp handle_data(conn, {module, fun}) do + apply(module, fun, [conn]) + end - defp handle_data(conn, fun) when is_function(fun) do - fun.(conn) - end + defp handle_data(conn, fun) when is_function(fun) do + fun.(conn) + end - @spec default_cookie_scrubber(Plug.Conn.t()) :: map() - def default_cookie_scrubber(_conn) do - %{} - end + @spec default_cookie_scrubber(Plug.Conn.t()) :: map() + def default_cookie_scrubber(_conn) do + %{} + end - @spec default_header_scrubber(Plug.Conn.t()) :: map() - def default_header_scrubber(conn) do - Enum.into(conn.req_headers, %{}) - |> Map.drop(@default_scrubbed_header_keys) - end + @spec default_header_scrubber(Plug.Conn.t()) :: map() + def default_header_scrubber(conn) do + Enum.into(conn.req_headers, %{}) + |> Map.drop(@default_scrubbed_header_keys) + end - @spec default_body_scrubber(Plug.Conn.t()) :: map() - def default_body_scrubber(conn) do - scrub_map(conn.params) - end + @spec default_body_scrubber(Plug.Conn.t()) :: map() + def default_body_scrubber(conn) do + scrub_map(conn.params) + end - defp scrub_map(map) do - Enum.into(map, %{}, fn {key, value} -> - value = - cond do - Enum.member?(@default_scrubbed_param_keys, key) -> - @scrubbed_value + defp scrub_map(map) do + Enum.into(map, %{}, fn {key, value} -> + value = + cond do + Enum.member?(@default_scrubbed_param_keys, key) -> + @scrubbed_value - is_binary(value) && Regex.match?(@credit_card_regex, value) -> - @scrubbed_value + is_binary(value) && Regex.match?(@credit_card_regex, value) -> + @scrubbed_value - is_map(value) && Map.has_key?(value, :__struct__) -> - Map.from_struct(value) - |> scrub_map() + is_map(value) && Map.has_key?(value, :__struct__) -> + Map.from_struct(value) + |> scrub_map() - is_map(value) -> - scrub_map(value) + is_map(value) -> + scrub_map(value) - true -> - value - end + true -> + value + end - {key, value} - end) - end + {key, value} + end) end end diff --git a/mix.exs b/mix.exs index 6a91ac21..3c3da453 100644 --- a/mix.exs +++ b/mix.exs @@ -15,7 +15,7 @@ defmodule Sentry.Mixfile do plt_add_apps: [:mix, :plug, :hackney] ], docs: [extras: ["README.md"], main: "readme"], - xref: [exclude: [:hackney, :hackney_pool]] + xref: [exclude: [:hackney, :hackney_pool, Plug.Conn]] ] end