Times every Oban job, kind: "job", and reports a genuinely failed job (retries exhausted, or
the worker explicitly gave up) the same way an unhandled exception anywhere else already is. Call
once at startup:
ForgeOpsTracker.Integrations.Oban.attach()Attaches to [:oban, :job, :stop] and [:oban, :job, :exception] (confirmed directly against
the installed oban source, not assumed from docs), both global event names covering every Oban
instance in the host app. measurements.duration is native time units, available on both events,
and is recorded on both: a failed attempt's own duration is still useful data, the same "always
time it regardless of outcome" choice this repo's own Ruby/Python job integrations already make.
metadata.job.worker (the worker module's own name, as a string) is the transaction_name.
On :exception specifically, metadata.state is :failure (this attempt failed but will
retry) or :discard (no further retries, either exhausted or the worker explicitly returned
:discard/{:discard, reason}), per Oban's own source. capture_exception/3 is only called
when state == :discard, not on every retryable :failure: the same "report once retries are
exhausted, not on every retry" scope this repo's own PHP Queue::failing (as opposed to
Queue::exceptionOccurred) integration already settled on, applied here since Oban's own
:discard vs :failure distinction maps onto exactly that same "final vs will-retry" question.
metadata.reason is always exception-shaped by the time it reaches here (Oban's own docs: a
raised exception passes through as-is, a crash wraps in Oban.CrashError, a timeout in
Oban.TimeoutError, everything else normalizes into Oban.PerformError), so no special-casing
is needed before handing it to capture_exception/3.