diff --git a/Dockerfile b/Dockerfile index 0e6e39d8..5f03f9cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ADD apps apps ADD config config ADD mix.* /root/ -RUN mix do deps.get --only prod, phx.swagger.generate, compile, phx.digest +RUN mix do deps.get --only prod, phx.swagger.generate, compile, phx.digest, sentry.package_source_code RUN mix eval "Application.ensure_all_started(:tzdata); Tzdata.DataBuilder.load_and_save_table()" ADD rel/ rel/ diff --git a/apps/api_web/config/test.exs b/apps/api_web/config/test.exs index f788cc77..212abe13 100644 --- a/apps/api_web/config/test.exs +++ b/apps/api_web/config/test.exs @@ -18,6 +18,10 @@ config :api_web, RateLimiter.Memcache, config :api_web, ApiWeb.Plugs.ModifiedSinceHandler, check_caller: true +config :sentry, + test_mode: true, + before_send: {ApiWeb.SentryEventFilter, :filter_event} + # Credentials that always show widget and pass backend validation: config :recaptcha, enabled: true, diff --git a/apps/api_web/lib/api_web/sentry_event_filter.ex b/apps/api_web/lib/api_web/sentry_event_filter.ex index 84bd3921..405ec15e 100644 --- a/apps/api_web/lib/api_web/sentry_event_filter.ex +++ b/apps/api_web/lib/api_web/sentry_event_filter.ex @@ -2,12 +2,23 @@ defmodule ApiWeb.SentryEventFilter do @moduledoc """ Provides a filter for exceptions coming from 404 errors """ - @behaviour Sentry.EventFilter - def exclude_exception?(%Phoenix.Router.NoRouteError{}, :plug), do: true + # Sentry allows this callback to both modify events before they get sent, + # and filter events to prevent them from being sent at all. + # We only do the latter. Returning false prevents sending. + @spec filter_event(Sentry.Event.t()) :: Sentry.Event.t() | false + def filter_event(%Sentry.Event{ + source: :plug, + original_exception: %Phoenix.Router.NoRouteError{} + }) do + false + end - def exclude_exception?(%Sentry.CrashError{} = error, :logger), - do: String.contains?(error.message, "{{{%Phoenix.Router.NoRouteError") + def filter_event(%Sentry.Event{message: %Sentry.Interfaces.Message{}} = event) do + if String.contains?(event.message.formatted, "{{{%Phoenix.Router.NoRouteError"), + do: false, + else: event + end - def exclude_exception?(_exception, _source), do: false + def filter_event(event), do: event end diff --git a/apps/api_web/mix.exs b/apps/api_web/mix.exs index 33b8823c..a2f0cf87 100644 --- a/apps/api_web/mix.exs +++ b/apps/api_web/mix.exs @@ -83,7 +83,7 @@ defmodule ApiWeb.Mixfile do {:stream_data, "~> 1.2", only: :test}, {:sobelow, "~> 0.11", only: :dev, runtime: false}, {:recaptcha, git: "https://github.com/samueljseay/recaptcha.git", ref: "71cd746be987f6834c1a933f5d2f934350e55060"}, - {:sentry, "~> 8.0"}, + {:sentry, "~> 11.0"}, {:qr_code, "~> 3.0"}, {:nimble_totp, "~> 1.0"} ] diff --git a/apps/api_web/test/api_web/sentry_event_filter_test.exs b/apps/api_web/test/api_web/sentry_event_filter_test.exs new file mode 100644 index 00000000..f8c9786b --- /dev/null +++ b/apps/api_web/test/api_web/sentry_event_filter_test.exs @@ -0,0 +1,34 @@ +defmodule ApiWeb.SentryEventFilterTest do + use ApiWeb.ConnCase, async: true + + describe "filter_event/1" do + setup do + Sentry.Test.start_collecting_sentry_reports() + end + + test "filters out `NoRouteError`s rescued by plugs", %{conn: conn} do + conn = get(conn, "/a_nonexistent_route") + assert response(conn, 404) + + assert [] = Sentry.Test.pop_sentry_reports() + end + + test "filters out `NoRouteError`s surfaced as messages via crashes" do + Sentry.capture_message("Something something {{{%Phoenix.Router.NoRouteError}}}") + assert [] = Sentry.Test.pop_sentry_reports() + + Sentry.capture_message("Something something {{{%SomeOtherError}}}") + assert [%Sentry.Event{} = err] = Sentry.Test.pop_sentry_reports() + assert err.message.formatted =~ "SomeOtherError" + end + + test "does not filter out other exceptions" do + err = RuntimeError.exception("An error other than NoRouteError") + + Sentry.capture_exception(err) + + assert [%Sentry.Event{} = event] = Sentry.Test.pop_sentry_reports() + assert ^err = event.original_exception + end + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 0c6fc042..624b915b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -10,15 +10,27 @@ if is_prod? and is_release? do if not is_nil(sentry_env) do config :sentry, - filter: ApiWeb.SentryEventFilter, + before_send: {ApiWeb.SentryEventFilter, :filter_event}, dsn: System.fetch_env!("SENTRY_DSN"), environment_name: sentry_env, enable_source_code_context: true, - root_source_code_path: File.cwd!(), + root_source_code_paths: + [ + "api_web", + "health", + "parse", + "alb_monitor", + "state", + "fetch", + "model", + "events", + "api_accounts", + "state_mediator" + ] + |> Enum.map(&Path.join([File.cwd!(), "apps", &1])), tags: %{ env: sentry_env - }, - included_environments: [sentry_env] + } config :logger, Sentry.LoggerBackend, level: :error end @@ -31,7 +43,6 @@ if is_prod? and is_release? do ], json_codec: Jason - config :alb_monitor, ecs_metadata_uri: System.fetch_env!("ECS_CONTAINER_METADATA_URI"), target_group_arn: System.fetch_env!("ALB_TARGET_GROUP_ARN") diff --git a/mix.lock b/mix.lock index 40f4e718..9895737c 100644 --- a/mix.lock +++ b/mix.lock @@ -61,6 +61,7 @@ "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"}, @@ -83,7 +84,7 @@ "recaptcha": {:git, "https://github.com/samueljseay/recaptcha.git", "71cd746be987f6834c1a933f5d2f934350e55060", [ref: "71cd746be987f6834c1a933f5d2f934350e55060"]}, "result": {:hex, :result, "1.7.2", "a57c569f7cf5c158d2299d3b5624a48b69bd1520d0771dc711bcf9f3916e8ab6", [:mix], [], "hexpm", "89f98e98cfbf64237ecf4913aa36b76b80463e087775d19953dc4b435a35f087"}, "rstar": {:git, "https://github.com/armon/erl-rstar.git", "a406b2cce609029bf65b9ccfbe93a0416c0ee0cd", []}, - "sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"}, + "sentry": {:hex, :sentry, "11.0.4", "60371c96cefd247e0fc98840bba2648f64f19aa0b8db8e938f5a98421f55b619", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "feaafc284dc204c82aadaddc884227aeaa3480decb274d30e184b9d41a700c66"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},