Bug description
When both auto_capture_exceptions and auto_instrument_active_job are enabled, every unhandled ActiveJob exception is captured twice — once by ActiveJobExtensions and once by ErrorSubscriber.
ActiveJobExtensions#perform_now rescues the error, calls PostHog.capture_exception with $exception_source: 'active_job', and re-raises. The re-raised error then reaches the Rails error reporter (ActiveJob's executor reports it with source application.active_support), and ErrorSubscriber#report captures it again.
ErrorSubscriber already has a guard for exactly this class of problem on the web path:
# lib/posthog/rails/error_subscriber.rb
# Skip if in a web request - CaptureExceptions middleware will handle it
# with richer context (URL, params, controller, etc.)
return if PostHog::Rails.in_web_request?
…but there is no equivalent guard for the ActiveJob path, and no dedupe key ties the two captures together, so both events land in error tracking.
Observed
In our project the split is exact — every job failure produces one event of each source:
$exception_source |
count (30d) |
active_job |
506 |
application.active_support |
506 |
The active_job events carry the richer context ($job_class, $job_id, $job_arguments, …); the application.active_support duplicates carry none of it. Besides doubling event volume, the duplicates skew issue occurrence counts in error tracking.
How to reproduce
- Rails 8.1 app,
posthog-rails 3.16.0, with:
PostHog::Rails.configure do |config|
config.auto_capture_exceptions = true
config.auto_instrument_active_job = true
end
- Enqueue any job that raises, e.g.
raise ArgumentError in perform.
- Two
$exception events are captured for the single failure: $exception_source: 'active_job' and $exception_source: 'application.active_support'.
Expected
One captured exception per job failure — presumably the ActiveJobExtensions one, since it has the job context.
Suggested fix
Mirror the web-request guard in ErrorSubscriber#report: skip sources that ActiveJobExtensions already covers (e.g. skip when the reported source is ActiveJob's executor and auto_instrument_active_job is enabled), or set a thread/execution-local flag in capture_job_exception and check it in ErrorSubscriber, the same way in_web_request? works.
Happy to open a PR if you'd like — the thread-local approach looks like a small change.
Environment
- posthog-ruby / posthog-rails 3.16.0
- Rails 8.1.3, Ruby 3.3.8
- Jobs running inline/async in a standard Rails app (observed with
Turbo::Streams::BroadcastStreamJob among others)
Bug description
When both
auto_capture_exceptionsandauto_instrument_active_jobare enabled, every unhandled ActiveJob exception is captured twice — once byActiveJobExtensionsand once byErrorSubscriber.ActiveJobExtensions#perform_nowrescues the error, callsPostHog.capture_exceptionwith$exception_source: 'active_job', and re-raises. The re-raised error then reaches the Rails error reporter (ActiveJob's executor reports it with sourceapplication.active_support), andErrorSubscriber#reportcaptures it again.ErrorSubscriberalready has a guard for exactly this class of problem on the web path:…but there is no equivalent guard for the ActiveJob path, and no dedupe key ties the two captures together, so both events land in error tracking.
Observed
In our project the split is exact — every job failure produces one event of each source:
$exception_sourceactive_jobapplication.active_supportThe
active_jobevents carry the richer context ($job_class,$job_id,$job_arguments, …); theapplication.active_supportduplicates carry none of it. Besides doubling event volume, the duplicates skew issue occurrence counts in error tracking.How to reproduce
posthog-rails3.16.0, with:raise ArgumentErrorinperform.$exceptionevents are captured for the single failure:$exception_source: 'active_job'and$exception_source: 'application.active_support'.Expected
One captured exception per job failure — presumably the
ActiveJobExtensionsone, since it has the job context.Suggested fix
Mirror the web-request guard in
ErrorSubscriber#report: skip sources thatActiveJobExtensionsalready covers (e.g. skip when the reported source is ActiveJob's executor andauto_instrument_active_jobis enabled), or set a thread/execution-local flag incapture_job_exceptionand check it inErrorSubscriber, the same wayin_web_request?works.Happy to open a PR if you'd like — the thread-local approach looks like a small change.
Environment
Turbo::Streams::BroadcastStreamJobamong others)