Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wise-jobs-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-rails': patch
---

Avoid double-capturing ActiveJob exceptions through the Rails error subscriber.
25 changes: 25 additions & 0 deletions posthog-rails/lib/posthog/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ module PostHog
module Rails
# Thread-local key for tracking web request context
IN_WEB_REQUEST_KEY = :posthog_in_web_request
ACTIVE_JOB_CAPTURED_EXCEPTION_IVAR = :@posthog_active_job_exception_captured

private_constant :ACTIVE_JOB_CAPTURED_EXCEPTION_IVAR

class << self
# @return [PostHog::Rails::Configuration] Rails integration configuration.
Expand Down Expand Up @@ -60,6 +63,28 @@ def exit_web_request
def in_web_request?
Thread.current[IN_WEB_REQUEST_KEY] == true
end

# Mark an exception as already captured by the ActiveJob integration.
# Used by ErrorSubscriber to avoid duplicate captures when Rails reports
# the same re-raised job exception via Rails.error.
# @api private
# @param exception [Exception]
# @return [void]
def mark_active_job_exception_captured(exception)
exception.instance_variable_set(ACTIVE_JOB_CAPTURED_EXCEPTION_IVAR, true)
rescue StandardError
nil
end

# Check whether an exception was already captured by ActiveJobExtensions.
# @api private
# @param exception [Exception]
# @return [Boolean]
def active_job_exception_captured?(exception)
exception.instance_variable_get(ACTIVE_JOB_CAPTURED_EXCEPTION_IVAR) == true
rescue StandardError
false
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions posthog-rails/lib/posthog/rails/active_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def capture_job_exception(exception)
properties,
mechanism: { 'type' => 'active_job', 'handled' => false }
)
PostHog::Rails.mark_active_job_exception_captured(exception)
rescue StandardError => e
# Don't let PostHog errors break job processing
PostHog::Logging.logger.error("Failed to capture job exception: #{e.message}")
Expand Down
2 changes: 2 additions & 0 deletions posthog-rails/lib/posthog/rails/error_subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def report(error, handled:, severity:, context:, source: nil)
# 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?
return if PostHog::Rails.config&.auto_instrument_active_job &&
PostHog::Rails.active_job_exception_captured?(error)

distinct_id = context[:user_id] || context[:distinct_id]

Expand Down
62 changes: 62 additions & 0 deletions spec/posthog/rails/exception_mechanism_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,41 @@
)
end
end

it 'skips exceptions already captured by ActiveJobExtensions' do
PostHog::Rails.config.auto_instrument_active_job = true
error = StandardError.new('boom')
PostHog::Rails.mark_active_job_exception_captured(error)

described_class.new.report(
error,
handled: false,
severity: :error,
context: {},
source: 'application.active_support'
)

expect(PostHog).not_to have_received(:capture_exception)
end

it 'still captures unmarked ActiveSupport reports when ActiveJob instrumentation is enabled' do
PostHog::Rails.config.auto_instrument_active_job = true

described_class.new.report(
StandardError.new('boom'),
handled: false,
severity: :error,
context: {},
source: 'application.active_support'
)

expect(PostHog).to have_received(:capture_exception).with(
an_instance_of(StandardError),
anything,
hash_including('$exception_source' => 'application.active_support'),
mechanism: { 'type' => 'rails_error_reporter', 'handled' => false }
)
end
end

describe PostHog::Rails::ActiveJobExtensions do
Expand Down Expand Up @@ -112,5 +147,32 @@ def perform_now
mechanism: { 'type' => 'active_job', 'handled' => false }
)
end

it 'prevents Rails error subscriber from capturing the same job exception again' do
PostHog::Rails.config.auto_instrument_active_job = true
error = nil

begin
job_class.new.perform_now
rescue StandardError => e
error = e
end

PostHog::Rails::ErrorSubscriber.new.report(
error,
handled: false,
severity: :error,
context: {},
source: 'application.active_support'
)

expect(PostHog).to have_received(:capture_exception).once
expect(PostHog).to have_received(:capture_exception).with(
an_instance_of(StandardError),
anything,
hash_including('$exception_source' => 'active_job'),
mechanism: { 'type' => 'active_job', 'handled' => false }
)
end
end
end
Loading