From 234dce2e15d68b7b2dc34a076622bb1a8dc1075d Mon Sep 17 00:00:00 2001 From: Stephen Turley Date: Fri, 30 Jan 2026 11:15:45 -0500 Subject: [PATCH] fix(oban): Handle @reboot cron schedule with timezone Previously, Oban jobs using the @reboot schedule with a timezone set would crash the telemetry handler with a {badkey, schedule} error. This happened because: 1. @reboot correctly returns an empty schedule (can't be expressed as cron/interval) 2. The timezone was still being added to monitor_config 3. CheckIn.new/1 crashed trying to access the missing :schedule key The fix checks for an empty schedule first and returns nil (skipping check-in) before adding timezone or other monitor config options. This ensures @reboot jobs are gracefully skipped from Sentry cron monitoring, since they cannot be represented as a scheduled monitor. Co-Authored-By: Claude Opus 4.5 --- lib/sentry/integrations/oban/cron.ex | 29 ++++++++++++--------- test/sentry/integrations/oban/cron_test.exs | 13 +++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/sentry/integrations/oban/cron.ex b/lib/sentry/integrations/oban/cron.ex index 9c9d8c88..4825112c 100644 --- a/lib/sentry/integrations/oban/cron.ex +++ b/lib/sentry/integrations/oban/cron.ex @@ -88,23 +88,26 @@ defmodule Sentry.Integrations.Oban.Cron do end defp job_to_check_in_opts(job, config) when is_struct(job, Oban.Job) do - monitor_config_opts = Sentry.Config.integrations()[:monitor_config_defaults] - monitor_config_opts = maybe_put_timezone_option(monitor_config_opts, job) + # Check schedule first - if empty (e.g., @reboot), skip check-in entirely + # since @reboot can't be expressed as a cron/interval schedule + case schedule_opts(job) do + [] -> + nil - monitor_slug = - case config[:monitor_slug_generator] do - nil -> - slugify(job.worker) + schedule_opts -> + monitor_config_opts = Sentry.Config.integrations()[:monitor_config_defaults] + monitor_config_opts = maybe_put_timezone_option(monitor_config_opts, job) + monitor_config_opts = Keyword.merge(monitor_config_opts, schedule_opts) - {mod, fun} when is_atom(mod) and is_atom(fun) -> - mod |> apply(fun, [job]) |> slugify() - end + monitor_slug = + case config[:monitor_slug_generator] do + nil -> + slugify(job.worker) - case Keyword.merge(monitor_config_opts, schedule_opts(job)) do - [] -> - nil + {mod, fun} when is_atom(mod) and is_atom(fun) -> + mod |> apply(fun, [job]) |> slugify() + end - monitor_config_opts -> id = CheckInIDMappings.lookup_or_insert_new(job.id) opts = [ diff --git a/test/sentry/integrations/oban/cron_test.exs b/test/sentry/integrations/oban/cron_test.exs index 6b8c74e1..566a1d97 100644 --- a/test/sentry/integrations/oban/cron_test.exs +++ b/test/sentry/integrations/oban/cron_test.exs @@ -48,6 +48,19 @@ defmodule Sentry.Integrations.Oban.CronTest do }) end + test "ignores #{event_type} events with a cron expr of @reboot even with timezone", %{ + bypass: bypass + } do + Bypass.down(bypass) + + :telemetry.execute([:oban, :job, unquote(event_type)], %{}, %{ + job: %Oban.Job{ + worker: "Sentry.MyWorker", + meta: %{"cron" => true, "cron_expr" => "@reboot", "cron_tz" => "Etc/UTC"} + } + }) + end + test "ignores #{event_type} events with a cron expr that is not a string", %{bypass: bypass} do Bypass.down(bypass)