Skip to content

Commit b197090

Browse files
authored
Add integrated support for capturing Oban errors (#705)
1 parent 97ba33d commit b197090

File tree

5 files changed

+136
-0
lines changed

5 files changed

+136
-0
lines changed

lib/sentry/application.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ defmodule Sentry.Application do
5252
Sentry.Cron.Oban.attach_telemetry_handler()
5353
end
5454

55+
if config[:oban][:capture_errors] do
56+
Sentry.Integrations.Oban.ErrorReporter.attach()
57+
end
58+
5559
if config[:quantum][:cron][:enabled] do
5660
Sentry.Cron.Quantum.attach_telemetry_handler()
5761
end

lib/sentry/config.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ defmodule Sentry.Config do
99
since v10.2.0*.
1010
""",
1111
keys: [
12+
capture_errors: [
13+
type: :boolean,
14+
default: false,
15+
doc: """
16+
Whether to capture errors from Oban jobs. When enabled, the Sentry SDK will capture
17+
errors that happen in Oban jobs, including when errors return `{:error, reason}`
18+
tuples. *Available since 10.3.0*.
19+
"""
20+
],
1221
cron: [
1322
doc: """
1423
Configuration options for configuring [*crons*](https://docs.sentry.io/product/crons/)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule Sentry.Integrations.Oban.ErrorReporter do
2+
@moduledoc false
3+
4+
# See this blog post:
5+
# https://getoban.pro/articles/enhancing-error-reporting
6+
7+
@spec attach() :: :ok
8+
def attach do
9+
_ =
10+
:telemetry.attach(
11+
__MODULE__,
12+
[:oban, :job, :exception],
13+
&__MODULE__.handle_event/4,
14+
:no_config
15+
)
16+
17+
:ok
18+
end
19+
20+
@spec handle_event(
21+
[atom(), ...],
22+
term(),
23+
%{required(:job) => struct(), optional(term()) => term()},
24+
:no_config
25+
) :: :ok
26+
def handle_event([:oban, :job, :exception], _measurements, %{job: job} = _metadata, :no_config) do
27+
oban_worker_mod = Oban.Worker
28+
%{reason: exception, stacktrace: stacktrace} = job.unsaved_error
29+
30+
stacktrace =
31+
case {oban_worker_mod.from_string(job.worker), stacktrace} do
32+
{{:ok, atom_worker}, []} -> [{atom_worker, :process, 1, []}]
33+
_ -> stacktrace
34+
end
35+
36+
_ =
37+
Sentry.capture_exception(exception,
38+
stacktrace: stacktrace,
39+
tags: %{oban_worker: job.worker, oban_queue: job.queue, oban_state: job.state},
40+
fingerprint: [
41+
inspect(exception.__struct__),
42+
inspect(job.worker),
43+
Exception.message(exception)
44+
],
45+
extra: Map.take(job, [:args, :attempt, :id, :max_attempts, :meta, :queue, :tags, :worker])
46+
)
47+
48+
:ok
49+
end
50+
end

pages/oban-integration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ The Oban integration is available since *v10.2.0* of the Sentry SDK, and it requ
88
1. Oban version 2.17.6 or greater.
99
1. Elixir 1.13 or later, since that is required by Oban itself.
1010

11+
## Automatic Error Capturing
12+
13+
*Available since 10.3.0*.
14+
15+
You can enable automatic capturing of errors that happen in Oban jobs. This includes jobs that return `{:error, reason}`, raise an exception, exit, and so on.
16+
17+
To enable support:
18+
19+
```elixir
20+
config :sentry,
21+
integrations: [
22+
oban: [
23+
capture_errors: true
24+
]
25+
]
26+
```
27+
1128
## Cron Support
1229

1330
To enable support for monitoring Oban jobs via [Sentry Cron](https://docs.sentry.io/product/crons/), make sure the following `:oban` configuration is in your Sentry configuration:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# TODO: Oban requires Elixir 1.13+, remove this once we depend on that too.
2+
if Version.match?(System.version(), "~> 1.13") do
3+
defmodule Sentry.Integrations.Oban.ErrorReporterTest do
4+
use ExUnit.Case, async: true
5+
6+
alias Sentry.Integrations.Oban.ErrorReporter
7+
8+
defmodule MyWorker do
9+
use Oban.Worker
10+
11+
@impl Oban.Worker
12+
def perform(%Oban.Job{}), do: :ok
13+
end
14+
15+
describe "handle_event/4" do
16+
test "reports the correct error to Sentry" do
17+
# Any worker is okay here, this is just an easier way to get a job struct.
18+
job =
19+
%{"id" => "123", "entity" => "user", "type" => "delete"}
20+
|> MyWorker.new()
21+
|> Ecto.Changeset.apply_action!(:validate)
22+
|> Map.replace!(:unsaved_error, %{
23+
reason: %RuntimeError{message: "oops"},
24+
kind: :error,
25+
stacktrace: []
26+
})
27+
28+
Sentry.Test.start_collecting()
29+
30+
assert :ok =
31+
ErrorReporter.handle_event(
32+
[:oban, :job, :exception],
33+
%{},
34+
%{job: job},
35+
:no_config
36+
)
37+
38+
assert [event] = Sentry.Test.pop_sentry_reports()
39+
assert event.original_exception == %RuntimeError{message: "oops"}
40+
assert [%{stacktrace: %{frames: [stacktrace]}} = exception] = event.exception
41+
42+
assert exception.type == "RuntimeError"
43+
assert exception.value == "oops"
44+
assert exception.mechanism.handled == true
45+
assert stacktrace.module == MyWorker
46+
47+
assert stacktrace.function ==
48+
"Sentry.Integrations.Oban.ErrorReporterTest.MyWorker.process/1"
49+
50+
assert event.tags.oban_queue == "default"
51+
assert event.tags.oban_state == "available"
52+
assert event.tags.oban_worker == "Sentry.Integrations.Oban.ErrorReporterTest.MyWorker"
53+
end
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)