Skip to content

Commit 8b97936

Browse files
savhappySavannah Manningwhatyouhide
authored
Support per-module custom options for check ins (#833)
Co-authored-by: Savannah Manning <[email protected]> Co-authored-by: Andrea Leopardi <[email protected]>
1 parent 2b5dc13 commit 8b97936

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed

lib/sentry/integrations/oban/cron.ex

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
11
defmodule Sentry.Integrations.Oban.Cron do
2-
@moduledoc false
2+
@moduledoc """
3+
This module provides built-in integration for cron jobs managed by Oban.
4+
"""
5+
6+
@moduledoc since: "10.9.0"
7+
38
alias Sentry.Integrations.CheckInIDMappings
49

10+
@doc """
11+
The Oban integration calls this callback (if present) to customize
12+
the configuration options for the check-in.
13+
14+
This function must return options compatible with the ones passed to `Sentry.CheckIn.new/1`.
15+
16+
Options returned by this function overwrite any option inferred by the specific
17+
integration for the check in. We perform *deep merging* of nested keyword options.
18+
"""
19+
@doc since: "10.9.0"
20+
@callback sentry_check_in_configuration(oban_job :: struct()) :: options_to_merge :: keyword()
21+
522
@events [
623
[:oban, :job, :start],
724
[:oban, :job, :stop],
825
[:oban, :job, :exception]
926
]
1027

28+
@doc false
1129
@spec attach_telemetry_handler(keyword()) :: :ok
1230
def attach_telemetry_handler(config) when is_list(config) do
1331
_ = :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, config)
1432
:ok
1533
end
1634

35+
@doc false
1736
@spec handle_event([atom()], term(), term(), keyword()) :: :ok
1837
def handle_event(event, measurements, metadata, config)
1938

@@ -87,15 +106,56 @@ defmodule Sentry.Integrations.Oban.Cron do
87106
monitor_config_opts ->
88107
id = CheckInIDMappings.lookup_or_insert_new(job.id)
89108

90-
[
109+
opts = [
91110
check_in_id: id,
92111
# This is already a binary.
93112
monitor_slug: monitor_slug,
94113
monitor_config: monitor_config_opts
95114
]
115+
116+
resolve_custom_opts(opts, job)
96117
end
97118
end
98119

120+
defp resolve_custom_opts(opts, %{worker: worker} = job)
121+
when is_struct(job, Oban.Job) and is_binary(worker) do
122+
job.worker |> String.split(".") |> Module.safe_concat()
123+
rescue
124+
ArgumentError -> opts
125+
else
126+
worker ->
127+
if Code.ensure_loaded?(worker) do
128+
resolve_custom_opts(opts, worker, job)
129+
else
130+
opts
131+
end
132+
end
133+
134+
defp resolve_custom_opts(opts, _job) do
135+
opts
136+
end
137+
138+
defp resolve_custom_opts(options, mod, per_integration_term) do
139+
custom_opts =
140+
if function_exported?(mod, :sentry_check_in_configuration, 1) do
141+
mod.sentry_check_in_configuration(per_integration_term)
142+
else
143+
[]
144+
end
145+
146+
deep_merge_keyword(options, custom_opts)
147+
end
148+
149+
defp deep_merge_keyword(left, right) do
150+
Keyword.merge(left, right, fn _key, left_val, right_val ->
151+
if Keyword.keyword?(left_val) and Keyword.keyword?(right_val) do
152+
deep_merge_keyword(left_val, right_val)
153+
else
154+
right_val
155+
end
156+
end)
157+
end
158+
99159
defp schedule_opts(%{meta: meta} = job) when is_struct(job, Oban.Job) do
100160
case meta["cron_expr"] do
101161
"@hourly" -> [schedule: [type: :interval, value: 1, unit: :hour]]

test/sentry/integrations/oban/cron_test.exs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,18 @@ defmodule Sentry.Integrations.Oban.CronTest do
4141
Bypass.down(bypass)
4242

4343
:telemetry.execute([:oban, :job, unquote(event_type)], %{}, %{
44-
job: %Oban.Job{meta: %{"cron" => true, "cron_expr" => "@reboot"}}
44+
job: %Oban.Job{
45+
worker: "Sentry.MyWorker",
46+
meta: %{"cron" => true, "cron_expr" => "@reboot"}
47+
}
4548
})
4649
end
4750

4851
test "ignores #{event_type} events with a cron expr that is not a string", %{bypass: bypass} do
4952
Bypass.down(bypass)
5053

5154
:telemetry.execute([:oban, :job, unquote(event_type)], %{}, %{
52-
job: %Oban.Job{meta: %{"cron" => true, "cron_expr" => 123}}
55+
job: %Oban.Job{worker: "Sentry.MyWorker", meta: %{"cron" => true, "cron_expr" => 123}}
5356
})
5457
end
5558
end
@@ -297,6 +300,52 @@ defmodule Sentry.Integrations.Oban.CronTest do
297300
assert_receive {^ref, :done}, 1000
298301
end
299302

303+
test "custom options", %{bypass: bypass} do
304+
test_pid = self()
305+
ref = make_ref()
306+
307+
Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
308+
{:ok, body, conn} = Plug.Conn.read_body(conn)
309+
assert [{_headers, check_in_body}] = decode_envelope!(body)
310+
311+
assert check_in_body["monitor_slug"] == "this-is-a-custom-slug-123"
312+
assert check_in_body["monitor_config"]["schedule"]["type"] == "interval"
313+
assert check_in_body["monitor_config"]["timezone"] == "Europe/Rome"
314+
315+
send(test_pid, {ref, :done})
316+
317+
Plug.Conn.send_resp(conn, 200, ~s<{"id": "1923"}>)
318+
end)
319+
320+
defmodule WorkerWithCustomOptions do
321+
use Oban.Worker
322+
323+
@behaviour Sentry.Integrations.Oban.Cron
324+
325+
@impl Oban.Worker
326+
def perform(_job), do: :ok
327+
328+
@impl Sentry.Integrations.Oban.Cron
329+
def sentry_check_in_configuration(job) do
330+
[
331+
monitor_slug: "this-is-a-custom-slug-#{job.id}",
332+
monitor_config: [timezone: "Europe/Rome"]
333+
]
334+
end
335+
end
336+
337+
:telemetry.execute([:oban, :job, :start], %{}, %{
338+
job: %Oban.Job{
339+
worker: inspect(WorkerWithCustomOptions),
340+
id: 123,
341+
args: %{},
342+
meta: %{"cron" => true, "cron_expr" => "@daily"}
343+
}
344+
})
345+
346+
assert_receive {^ref, :done}, 1000
347+
end
348+
300349
def custom_name_generator(%Oban.Job{worker: "Sentry.ClientWorker", args: %{"client" => client}}) do
301350
"Sentry.ClientWorker.#{client}"
302351
end

0 commit comments

Comments
 (0)