From 59f75d1721fde754fdc9c1ad107123fa6406625e Mon Sep 17 00:00:00 2001 From: Gonzalo <456459+grzuy@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:21:04 -0300 Subject: [PATCH] feat: reporter --- lib/tower_sentry.ex | 13 ------ lib/tower_sentry/reporter.ex | 28 +++++++++++++ mix.exs | 9 +++- mix.lock | 26 ++++++++++++ test/tower_sentry_test.exs | 80 +++++++++++++++++++++++++++++++++++- 5 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 lib/tower_sentry/reporter.ex create mode 100644 mix.lock diff --git a/lib/tower_sentry.ex b/lib/tower_sentry.ex index ec56e02..2f75ce9 100644 --- a/lib/tower_sentry.ex +++ b/lib/tower_sentry.ex @@ -2,17 +2,4 @@ defmodule TowerSentry do @moduledoc """ Documentation for `TowerSentry`. """ - - @doc """ - Hello world. - - ## Examples - - iex> TowerSentry.hello() - :world - - """ - def hello do - :world - end end diff --git a/lib/tower_sentry/reporter.ex b/lib/tower_sentry/reporter.ex new file mode 100644 index 0000000..75ab117 --- /dev/null +++ b/lib/tower_sentry/reporter.ex @@ -0,0 +1,28 @@ +defmodule TowerSentry.Reporter do + @behaviour Tower.Reporter + + @impl true + def report_event(%Tower.Event{ + kind: :error, + reason: exception, + stacktrace: stacktrace, + id: id, + metadata: metadata + }) do + if enabled?() do + # TODO: Include plug conn data if available + Sentry.capture_exception( + exception, + stacktrace: stacktrace, + extra: %{id: id, metadata: metadata} + ) + else + IO.puts("TowerSentry NOT enabled, ignoring...") + end + end + + defp enabled? do + Sentry.Config.dsn() + # Application.get_env(:sentry, :dsn) + end +end diff --git a/mix.exs b/mix.exs index 52b7a08..d651dbf 100644 --- a/mix.exs +++ b/mix.exs @@ -21,8 +21,13 @@ defmodule TowerSentry.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + {:tower, "~> 0.5.1"}, + {:sentry, "~> 10.7"}, + {:jason, "~> 1.4", optional: true}, + {:hackney, "~> 1.20", optional: true}, + + # Test + {:bypass, "~> 2.1", only: :test} ] end end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..abfed3f --- /dev/null +++ b/mix.lock @@ -0,0 +1,26 @@ +%{ + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "sentry": {:hex, :sentry, "10.7.0", "22af74d002229238b027d051d2bdd26bcd9789db5fea7335170aa41de7a658ce", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, 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]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20", [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", "ca732b35480b8023db2d6dd36418a603c54bdcb9111dbcc246f9d7219669b531"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "tower": {:hex, :tower, "0.5.1", "140510528f028920090defedf3e4600be5b067b6e147991d1313fac14d762383", [:mix], [{:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:uniq, "~> 0.6.0", [hex: :uniq, repo: "hexpm", optional: false]}], "hexpm", "b6c783dff05bb61128d67df826554d3518723e247514e53f42f2219bc8f6e5b5"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "uniq": {:hex, :uniq, "0.6.1", "369660ecbc19051be526df3aa85dc393af5f61f45209bce2fa6d7adb051ae03c", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "6426c34d677054b3056947125b22e0daafd10367b85f349e24ac60f44effb916"}, +} diff --git a/test/tower_sentry_test.exs b/test/tower_sentry_test.exs index a795bc4..e41474b 100644 --- a/test/tower_sentry_test.exs +++ b/test/tower_sentry_test.exs @@ -2,7 +2,83 @@ defmodule TowerSentryTest do use ExUnit.Case doctest TowerSentry - test "greets the world" do - assert TowerSentry.hello() == :world + setup do + bypass = Bypass.open() + + Application.put_env(:tower, :reporters, [TowerSentry.Reporter]) + Sentry.put_config(:dsn, "http://public:secret@localhost:#{bypass.port}/1") + Sentry.put_config(:environment_name, :test) + + Tower.attach() + + on_exit(fn -> + Sentry.put_config(:dsn, nil) + Tower.detach() + end) + + {:ok, bypass: bypass} + end + + test "reports arithmetic error when a Plug.Conn NOT present", %{bypass: bypass} do + # ref message synchronization trick copied from + # https://github.com/PSPDFKit-labs/bypass/issues/112 + parent = self() + ref = make_ref() + + Bypass.expect_once(bypass, "POST", "/api/1/envelope", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + + assert [_id, _header, event] = String.split(body, "\n", trim: true) + + assert( + { + :ok, + %{ + "level" => "error", + "environment" => "test", + "exception" => [exception] + } + } = Jason.decode(event) + ) + + assert( + %{ + "type" => "ArithmeticError", + "value" => "bad argument in arithmetic expression", + "stacktrace" => %{"frames" => frames} + } = exception + ) + + assert( + %{ + "function" => + ~s(anonymous fn/0 in TowerSentryTest."test reports arithmetic error when a Plug.Conn NOT present"/1), + "filename" => "test/tower_sentry_test.exs", + "lineno" => 71 + } = List.last(frames) + ) + + send(parent, {ref, :sent}) + + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.resp(200, Jason.encode!(%{"id" => "123"})) + end) + + ExUnit.CaptureLog.capture_log(fn -> + in_unlinked_process(fn -> + 1 / 0 + end) + end) + + assert_receive({^ref, :sent}, 500) + end + + defp in_unlinked_process(fun) when is_function(fun, 0) do + {:ok, pid} = Task.Supervisor.start_link() + + pid + |> Task.Supervisor.async_nolink(fun) + |> Task.yield() end end