From 6ca13f1542d79c513318a2ba71d84ba300b47062 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Apr 2026 12:28:08 +0000 Subject: [PATCH 01/33] test: Add active_job spec harness scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce sentry-rails/spec/active_job/ with the adapter-agnostic harness, FailingJob fixture, and composing shared example. No backend wiring yet — that lands in a follow-up. Refs #2933. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sentry_instrumented_backend.rb | 16 ++++++++ .../spec/active_job/support/harness.rb | 40 +++++++++++++++++++ sentry-rails/spec/active_job/support/jobs.rb | 20 ++++++++++ sentry-rails/spec/spec_helper.rb | 2 + 4 files changed, 78 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb create mode 100644 sentry-rails/spec/active_job/support/harness.rb create mode 100644 sentry-rails/spec/active_job/support/jobs.rb diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb new file mode 100644 index 000000000..bbc0a3b7f --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do + it "captures an error event when a job fails" do + expect do + Sentry::Specs::ActiveJob::FailingJob.perform_later + drain + end.to raise_error(Sentry::Specs::ActiveJob::FailingJob::Boom) + + event = last_sentry_event + expect(event).not_to be_nil + + exception = extract_sentry_exceptions(event).first + expect(exception.type).to eq("Sentry::Specs::ActiveJob::FailingJob::Boom") + end +end diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb new file mode 100644 index 000000000..95721d178 --- /dev/null +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_context "active_job backend harness" do |adapter:| + before do + make_basic_app + setup_sentry_test + + @previous_queue_adapter = ::ActiveJob::Base.queue_adapter + ::ActiveJob::Base.queue_adapter = adapter + + boot_adapter(adapter) + end + + after do + reset_adapter(adapter) + + ::ActiveJob::Base.queue_adapter = @previous_queue_adapter + + teardown_sentry_test + end + + def boot_adapter(_adapter) + # Per-adapter setup hook. Backends extend this when they need to load + # schemas, start supervisors, or otherwise prepare the environment. + end + + def reset_adapter(_adapter) + # Per-adapter teardown hook. Backends extend this to truncate tables + # or otherwise clean up state between examples. + end + + define_method(:drain) do + case adapter + when :test + perform_enqueued_jobs + else + raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" + end + end +end diff --git a/sentry-rails/spec/active_job/support/jobs.rb b/sentry-rails/spec/active_job/support/jobs.rb new file mode 100644 index 000000000..ed5fed731 --- /dev/null +++ b/sentry-rails/spec/active_job/support/jobs.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "active_job/railtie" + +module Sentry + module Specs + module ActiveJob + class FailingJob < ::ActiveJob::Base + self.logger = nil + + class Boom < RuntimeError + end + + def perform + raise Boom, "Boom!" + end + end + end + end +end diff --git a/sentry-rails/spec/spec_helper.rb b/sentry-rails/spec/spec_helper.rb index 420f47dfa..cd5d5ca80 100644 --- a/sentry-rails/spec/spec_helper.rb +++ b/sentry-rails/spec/spec_helper.rb @@ -27,6 +27,8 @@ end Dir["#{__dir__}/support/**/*.rb"].each { |file| require file } +Dir["#{__dir__}/active_job/support/**/*.rb"].each { |file| require file } +Dir["#{__dir__}/active_job/shared_examples/**/*.rb"].each { |file| require file } RAILS_VERSION = Rails.version.to_f From ede573a64dab8ba32b4d0213dc8169617a78fbd4 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Apr 2026 12:58:47 +0000 Subject: [PATCH 02/33] fixup: simplify --- .../sentry_instrumented_backend.rb | 14 ++++++++++--- .../spec/active_job/support/harness.rb | 20 ++++++++++++------- sentry-rails/spec/active_job/support/jobs.rb | 20 ------------------- 3 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 sentry-rails/spec/active_job/support/jobs.rb diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index bbc0a3b7f..5ce9e4be4 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -1,16 +1,24 @@ # frozen_string_literal: true RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do + let(:failing_job) do + job_fixture do + def perform + raise "boom from failing_job spec" + end + end + end + it "captures an error event when a job fails" do expect do - Sentry::Specs::ActiveJob::FailingJob.perform_later + failing_job.perform_later drain - end.to raise_error(Sentry::Specs::ActiveJob::FailingJob::Boom) + end.to raise_error(RuntimeError, /boom from failing_job spec/) event = last_sentry_event expect(event).not_to be_nil exception = extract_sentry_exceptions(event).first - expect(exception.type).to eq("Sentry::Specs::ActiveJob::FailingJob::Boom") + expect(exception.value).to match(/boom from failing_job spec/) end end diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 95721d178..c54b738d5 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true RSpec.shared_context "active_job backend harness" do |adapter:| - before do + let(:adapter) { adapter } + + around do |example| make_basic_app setup_sentry_test - @previous_queue_adapter = ::ActiveJob::Base.queue_adapter ::ActiveJob::Base.queue_adapter = adapter boot_adapter(adapter) - end - after do + example.run + ensure reset_adapter(adapter) - ::ActiveJob::Base.queue_adapter = @previous_queue_adapter - teardown_sentry_test end @@ -29,7 +28,7 @@ def reset_adapter(_adapter) # or otherwise clean up state between examples. end - define_method(:drain) do + def drain case adapter when :test perform_enqueued_jobs @@ -37,4 +36,11 @@ def reset_adapter(_adapter) raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end end + + def job_fixture(name = nil, &block) + name ||= "JobFixture_#{SecureRandom.hex(4)}" + klass = Class.new(::ActiveJob::Base, &block) + stub_const(name, klass) + klass + end end diff --git a/sentry-rails/spec/active_job/support/jobs.rb b/sentry-rails/spec/active_job/support/jobs.rb deleted file mode 100644 index ed5fed731..000000000 --- a/sentry-rails/spec/active_job/support/jobs.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require "active_job/railtie" - -module Sentry - module Specs - module ActiveJob - class FailingJob < ::ActiveJob::Base - self.logger = nil - - class Boom < RuntimeError - end - - def perform - raise Boom, "Boom!" - end - end - end - end -end From 291aa2c5c5c45f0604c8f12060a557c8bccc1739 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Apr 2026 12:58:55 +0000 Subject: [PATCH 03/33] test: Add spec for Sentry + ActiveJob on the test adapter --- sentry-rails/spec/active_job/test_adapter_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 sentry-rails/spec/active_job/test_adapter_spec.rb diff --git a/sentry-rails/spec/active_job/test_adapter_spec.rb b/sentry-rails/spec/active_job/test_adapter_spec.rb new file mode 100644 index 000000000..4d5e704de --- /dev/null +++ b/sentry-rails/spec/active_job/test_adapter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Sentry + ActiveJob on the test adapter", type: :job do + include_context "active_job backend harness", adapter: :test + + it_behaves_like "a Sentry-instrumented ActiveJob backend" +end From 5d16eacdf91c8d58518dec0ab070403bf2e19c35 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Mon, 27 Apr 2026 13:16:01 +0000 Subject: [PATCH 04/33] test: add shared examples for error capturing and retry semantics in ActiveJob --- .../shared_examples/error_capture.rb | 23 +++++++++++++++++ .../shared_examples/retry_semantics.rb | 25 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 22 ++-------------- 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 sentry-rails/spec/active_job/shared_examples/error_capture.rb create mode 100644 sentry-rails/spec/active_job/shared_examples/retry_semantics.rb diff --git a/sentry-rails/spec/active_job/shared_examples/error_capture.rb b/sentry-rails/spec/active_job/shared_examples/error_capture.rb new file mode 100644 index 000000000..8f60c42f7 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/error_capture.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that captures errors" do + let(:failing_job) do + job_fixture do + def perform + raise "boom from failing_job spec" + end + end + end + + it "captures an error event when a job fails" do + expect do + failing_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from failing_job spec/) + + expect(sentry_events.size).to eq(1) + + exception = extract_sentry_exceptions(sentry_events.last).first + expect(exception.value).to match(/boom from failing_job spec/) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb new file mode 100644 index 000000000..bd0a9205b --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that respects retry semantics" do + let(:retryable_job) do + job_fixture do + retry_on StandardError, attempts: 3, wait: 0 + + def perform + raise "boom from retryable_job spec" + end + end + end + + it "captures one error event after retries are exhausted" do + expect do + retryable_job.perform_later + 3.times { drain } + end.to raise_error(RuntimeError, /boom from retryable_job spec/) + + expect(sentry_events.size).to eq(1) + + exception = extract_sentry_exceptions(sentry_events.last).first + expect(exception.value).to match(/boom from retryable_job spec/) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 5ce9e4be4..eb0092d6f 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -1,24 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do - let(:failing_job) do - job_fixture do - def perform - raise "boom from failing_job spec" - end - end - end - - it "captures an error event when a job fails" do - expect do - failing_job.perform_later - drain - end.to raise_error(RuntimeError, /boom from failing_job spec/) - - event = last_sentry_event - expect(event).not_to be_nil - - exception = extract_sentry_exceptions(event).first - expect(exception.value).to match(/boom from failing_job spec/) - end + it_behaves_like "an ActiveJob backend that captures errors" + it_behaves_like "an ActiveJob backend that respects retry semantics" end From d8fe9350adf90ee622c3b23b2afb9904479413b8 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:06:11 +0000 Subject: [PATCH 05/33] test: add shared examples for ActiveJob discard semantics --- .../shared_examples/discard_semantics.rb | 22 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 23 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/discard_semantics.rb diff --git a/sentry-rails/spec/active_job/shared_examples/discard_semantics.rb b/sentry-rails/spec/active_job/shared_examples/discard_semantics.rb new file mode 100644 index 000000000..6ccf4fa84 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/discard_semantics.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that respects discard semantics" do + let(:discardable_job) do + job_fixture do + discard_on StandardError + + def perform + raise "boom from discardable_job spec" + end + end + end + + it "does not capture an event when the job is discarded" do + expect do + discardable_job.perform_later + drain + end.not_to raise_error + + expect(sentry_events).to be_empty + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index eb0092d6f..675fb971e 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -3,4 +3,5 @@ RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do it_behaves_like "an ActiveJob backend that captures errors" it_behaves_like "an ActiveJob backend that respects retry semantics" + it_behaves_like "an ActiveJob backend that respects discard semantics" end From c5d292789fc2777848daca6b531d8b654621f62a Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:29:26 +0000 Subject: [PATCH 06/33] test(active_job): add error_context shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/error_context.rb | 34 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 35 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/error_context.rb diff --git a/sentry-rails/spec/active_job/shared_examples/error_context.rb b/sentry-rails/spec/active_job/shared_examples/error_context.rb new file mode 100644 index 000000000..a07a31e81 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/error_context.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that attaches job context to error events" do + let(:failing_job) do + job_fixture do + def perform + raise "boom from failing_job spec" + end + end + end + + it "attaches job context to extras and tags on the captured event" do + expect do + failing_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from failing_job spec/) + + event = last_sentry_event + + expect(event.extra).to include( + active_job: failing_job.name, + arguments: [], + job_id: a_kind_of(String) + ) + expect(event.extra).to have_key(:provider_job_id) + expect(event.extra).to have_key(:locale) + expect(event.extra).to have_key(:scheduled_at) + + expect(event.tags).to include( + job_id: event.extra[:job_id], + provider_job_id: event.extra[:provider_job_id] + ) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 675fb971e..604391c18 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -2,6 +2,7 @@ RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do it_behaves_like "an ActiveJob backend that captures errors" + it_behaves_like "an ActiveJob backend that attaches job context to error events" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From 9662c9b5329e6bf051004e41ddcd7982efca3631 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:33:41 +0000 Subject: [PATCH 07/33] test(active_job): add scope_isolation shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/scope_isolation.rb | 24 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 25 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/scope_isolation.rb diff --git a/sentry-rails/spec/active_job/shared_examples/scope_isolation.rb b/sentry-rails/spec/active_job/shared_examples/scope_isolation.rb new file mode 100644 index 000000000..7f38cd4ee --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/scope_isolation.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that isolates per-job scope" do + let(:scope_polluting_job) do + job_fixture do + def perform + Sentry.get_current_scope.set_extras(scope_marker: "from-job") + raise "boom from scope_polluting_job spec" + end + end + end + + it "applies in-job scope changes to the captured event but does not leak them" do + expect do + scope_polluting_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from scope_polluting_job spec/) + + event = last_sentry_event + expect(event.extra).to include(scope_marker: "from-job") + + expect(Sentry.get_current_scope.extra).to eq({}) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 604391c18..a5720efd8 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -3,6 +3,7 @@ RSpec.shared_examples "a Sentry-instrumented ActiveJob backend" do it_behaves_like "an ActiveJob backend that captures errors" it_behaves_like "an ActiveJob backend that attaches job context to error events" + it_behaves_like "an ActiveJob backend that isolates per-job scope" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From b46686ec036281df83e2b897d9d95592cead5233 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:34:50 +0000 Subject: [PATCH 08/33] test(active_job): add rescue_from_handling shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/rescue_from_handling.rb | 49 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 50 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/rescue_from_handling.rb diff --git a/sentry-rails/spec/active_job/shared_examples/rescue_from_handling.rb b/sentry-rails/spec/active_job/shared_examples/rescue_from_handling.rb new file mode 100644 index 000000000..9c1ad49c7 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/rescue_from_handling.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that respects rescue_from" do + context "when rescue_from suppresses the error" do + let(:rescued_job) do + job_fixture do + rescue_from(StandardError) { |_error| nil } + + def perform + raise "boom from rescued_job spec" + end + end + end + + it "does not capture an event" do + expect do + rescued_job.perform_later + drain + end.not_to raise_error + + expect(sentry_events).to be_empty + end + end + + context "when the rescue_from callback raises a new error" do + let(:problematic_rescued_job) do + job_fixture do + rescue_from(StandardError) { |_error| raise "boom from rescue callback" } + + def perform + raise "original boom from problematic_rescued_job spec" + end + end + end + + it "captures one event chaining the original and callback errors" do + expect do + problematic_rescued_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from rescue callback/) + + expect(sentry_events.size).to eq(1) + + messages = extract_sentry_exceptions(sentry_events.last).map(&:value) + expect(messages).to include(match(/original boom from problematic_rescued_job spec/)) + expect(messages).to include(match(/boom from rescue callback/)) + end + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index a5720efd8..24ff9d946 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -4,6 +4,7 @@ it_behaves_like "an ActiveJob backend that captures errors" it_behaves_like "an ActiveJob backend that attaches job context to error events" it_behaves_like "an ActiveJob backend that isolates per-job scope" + it_behaves_like "an ActiveJob backend that respects rescue_from" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From 5b7b09732f8232b6307217aae34e7cca0dbff407 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:35:36 +0000 Subject: [PATCH 09/33] test(active_job): add adapter_skipping shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/adapter_skipping.rb | 24 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 25 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/adapter_skipping.rb diff --git a/sentry-rails/spec/active_job/shared_examples/adapter_skipping.rb b/sentry-rails/spec/active_job/shared_examples/adapter_skipping.rb new file mode 100644 index 000000000..cb7f0045d --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/adapter_skipping.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that respects skippable_job_adapters" do + let(:failing_job) do + job_fixture do + def perform + raise "boom from failing_job spec" + end + end + end + + it "captures no events when the adapter is in skippable_job_adapters" do + Sentry.configuration.rails.skippable_job_adapters = [ + failing_job.queue_adapter.class.to_s + ] + + expect do + failing_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from failing_job spec/) + + expect(sentry_events).to be_empty + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 24ff9d946..9b2c882cd 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -5,6 +5,7 @@ it_behaves_like "an ActiveJob backend that attaches job context to error events" it_behaves_like "an ActiveJob backend that isolates per-job scope" it_behaves_like "an ActiveJob backend that respects rescue_from" + it_behaves_like "an ActiveJob backend that respects skippable_job_adapters" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From af10cc4ccf2f4ced3972bad2fc259d8645d109fb Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:42:55 +0000 Subject: [PATCH 10/33] test(active_job): add argument_serialization shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/argument_serialization.rb | 79 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 80 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/argument_serialization.rb diff --git a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb new file mode 100644 index 000000000..c925d7768 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that serializes complex arguments" do + let(:failing_job) do + job_fixture do + def perform(*_args, **_kwargs) + raise "boom from argument_serialization spec" + end + end + end + + def event_arguments + last_sentry_event.extra[:arguments] + end + + it "serializes ActiveRecord arguments via global id" do + post = Post.create! + + expect do + failing_job.perform_later(post) + drain + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments).to eq([post.to_global_id.to_s]) + end + + it "recursively serializes nested hashes containing global ids" do + post = Post.create! + + expect do + failing_job.perform_later(wrapper: { post: post }) + drain + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments).to eq([{ wrapper: { post: post.to_global_id.to_s } }]) + end + + it "expands integer ranges into arrays" do + expect do + failing_job.perform_later(1..3) + drain + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments).to eq([[1, 2, 3]]) + end + + it "stringifies ActiveSupport::TimeWithZone ranges" do + range = 1.day.ago..Time.zone.now + + expect do + failing_job.perform_later(range) + drain + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments.first).to be_a(String) + expect(event_arguments.first).to include("..") + end + + it "falls back to the original argument when to_global_id raises" do + post = Post.create! + + problematic_job = job_fixture do + define_method(:perform) do |passed_post| + def passed_post.to_global_id + raise "intentional" + end + + raise "boom from argument_serialization spec" + end + end + + expect do + problematic_job.perform_later(post) + drain + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments).to eq([post]) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 9b2c882cd..394bfc1c6 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -6,6 +6,7 @@ it_behaves_like "an ActiveJob backend that isolates per-job scope" it_behaves_like "an ActiveJob backend that respects rescue_from" it_behaves_like "an ActiveJob backend that respects skippable_job_adapters" + it_behaves_like "an ActiveJob backend that serializes complex arguments" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From 5fe607ce1c6fc91b2c03602b5639ec219faf3c34 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:43:34 +0000 Subject: [PATCH 11/33] test(active_job): cover active_job_report_on_retry_error flag Co-Authored-By: Claude Opus 4.7 (1M context) --- .../active_job/shared_examples/retry_semantics.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb index bd0a9205b..8da6e51a9 100644 --- a/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb +++ b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb @@ -22,4 +22,19 @@ def perform exception = extract_sentry_exceptions(sentry_events.last).first expect(exception.value).to match(/boom from retryable_job spec/) end + + context "when active_job_report_on_retry_error is true" do + before do + Sentry.configuration.rails.active_job_report_on_retry_error = true + end + + it "captures one error event per attempt" do + expect do + retryable_job.perform_later + 3.times { drain } + end.to raise_error(RuntimeError, /boom from retryable_job spec/) + + expect(sentry_events.size).to eq(3) + end + end end From 4ab06b275638f0f98d6ea2cf84238c127f0ac50c Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:44:34 +0000 Subject: [PATCH 12/33] test(active_job): add deserialization_error shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/deserialization_error.rb | 25 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 26 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/deserialization_error.rb diff --git a/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb b/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb new file mode 100644 index 000000000..cf01a98da --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that unwraps DeserializationError" do + let(:deserialization_error_job) do + job_fixture do + def perform + 1 / 0 + rescue + raise ActiveJob::DeserializationError + end + end + end + + it "captures the root cause when wrapped in ActiveJob::DeserializationError" do + expect do + deserialization_error_job.perform_later + drain + end.to raise_error(ActiveJob::DeserializationError) + + expect(sentry_events.size).to eq(1) + + types = extract_sentry_exceptions(sentry_events.last).map(&:type) + expect(types.first).to eq("ZeroDivisionError") + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 394bfc1c6..5ca2ff11e 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -7,6 +7,7 @@ it_behaves_like "an ActiveJob backend that respects rescue_from" it_behaves_like "an ActiveJob backend that respects skippable_job_adapters" it_behaves_like "an ActiveJob backend that serializes complex arguments" + it_behaves_like "an ActiveJob backend that unwraps DeserializationError" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From e268ceb60def23856db569a2109fc0cc60ccffcc Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:47:02 +0000 Subject: [PATCH 13/33] test(active_job): add consumer_transaction tracing shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sentry_instrumented_backend.rb | 1 + .../tracing/consumer_transaction.rb | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 5ca2ff11e..e7598c11b 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -8,6 +8,7 @@ it_behaves_like "an ActiveJob backend that respects skippable_job_adapters" it_behaves_like "an ActiveJob backend that serializes complex arguments" it_behaves_like "an ActiveJob backend that unwraps DeserializationError" + it_behaves_like "an ActiveJob backend that emits a consumer transaction" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end diff --git a/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb b/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb new file mode 100644 index 000000000..cb5106164 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that emits a consumer transaction" do + let(:successful_job) do + job_fixture do + def perform; end + end + end + + let(:failing_job) do + job_fixture do + def perform + raise "boom from tracing spec" + end + end + end + + context "with traces_sample_rate = 1.0" do + before { Sentry.configuration.traces_sample_rate = 1.0 } + + it "captures a successful transaction with name, op, origin, source, and ok status" do + successful_job.perform_later + drain + + transaction = sentry_events.find { |e| e.is_a?(Sentry::TransactionEvent) } + expect(transaction).not_to be_nil + + expect(transaction.transaction).to eq(successful_job.name) + expect(transaction.transaction_info).to eq(source: :task) + expect(transaction.contexts.dig(:trace, :op)).to eq("queue.active_job") + expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job") + expect(transaction.contexts.dig(:trace, :status)).to eq("ok") + end + + it "marks the failing transaction internal_error and links the error event by trace_id" do + expect do + failing_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from tracing spec/) + + transaction = sentry_events.find { |e| e.is_a?(Sentry::TransactionEvent) } + error_event = sentry_events.find { |e| e.is_a?(Sentry::ErrorEvent) } + + expect(transaction.contexts.dig(:trace, :status)).to eq("internal_error") + expect(error_event.contexts.dig(:trace, :trace_id)).to eq(transaction.contexts.dig(:trace, :trace_id)) + end + end + + context "with traces_sample_rate = 0" do + before { Sentry.configuration.traces_sample_rate = 0 } + + it "does not capture a transaction" do + expect do + failing_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from tracing spec/) + + transactions = sentry_events.select { |e| e.is_a?(Sentry::TransactionEvent) } + expect(transactions).to be_empty + end + end +end From e4f2c5c572864f45b7c40fe9f43c31b730b95274 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:48:44 +0000 Subject: [PATCH 14/33] test(active_job): add scheduled_jobs shared example Extend the harness drain helper to accept an at: keyword so backends that distinguish scheduled from immediate jobs can be drained deterministically. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/scheduled_jobs.rb | 20 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + .../spec/active_job/support/harness.rb | 4 ++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb diff --git a/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb b/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb new file mode 100644 index 000000000..7b2af62b5 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that records scheduled_at on delayed jobs" do + let(:failing_job) do + job_fixture do + def perform + raise "boom from scheduled_jobs spec" + end + end + end + + it "records scheduled_at in the event extras" do + expect do + failing_job.set(wait: 5.seconds).perform_later + drain(at: 1.minute.from_now) + end.to raise_error(RuntimeError, /boom from scheduled_jobs spec/) + + expect(last_sentry_event.extra[:scheduled_at]).not_to be_nil + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index e7598c11b..7f8cdb9fb 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -9,6 +9,7 @@ it_behaves_like "an ActiveJob backend that serializes complex arguments" it_behaves_like "an ActiveJob backend that unwraps DeserializationError" it_behaves_like "an ActiveJob backend that emits a consumer transaction" + it_behaves_like "an ActiveJob backend that records scheduled_at on delayed jobs" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index c54b738d5..b34942a3c 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -28,10 +28,10 @@ def reset_adapter(_adapter) # or otherwise clean up state between examples. end - def drain + def drain(at: nil) case adapter when :test - perform_enqueued_jobs + perform_enqueued_jobs(at: at) else raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end From 120549cb12ed1786fa7f5ae4922a340fb740b346 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:49:26 +0000 Subject: [PATCH 15/33] test(active_job): add cron_check_ins shared example Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared_examples/cron_check_ins.rb | 51 +++++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 52 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb diff --git a/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb b/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb new file mode 100644 index 000000000..e9aefae26 --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that emits cron check-ins for monitor jobs" do + let(:cron_job) do + job_fixture do + include Sentry::Cron::MonitorCheckIns + sentry_monitor_check_ins + + def perform + "ok" + end + end + end + + let(:failing_cron_job) do + job_fixture do + include Sentry::Cron::MonitorCheckIns + sentry_monitor_check_ins + + def perform + raise "boom from failing_cron_job spec" + end + end + end + + it "emits in_progress and ok check-ins for a successful job" do + cron_job.perform_later + drain + + check_ins = sentry_events.select { |e| e.is_a?(Sentry::CheckInEvent) } + expect(check_ins.size).to eq(2) + + first, second = check_ins + expect(first.to_h).to include(type: "check_in", status: :in_progress) + expect(second.to_h).to include(type: "check_in", status: :ok, check_in_id: first.check_in_id) + expect(second.to_h).to have_key(:duration) + end + + it "emits in_progress and error check-ins plus an exception event for a failing job" do + expect do + failing_cron_job.perform_later + drain + end.to raise_error(RuntimeError, /boom from failing_cron_job spec/) + + check_ins = sentry_events.select { |e| e.is_a?(Sentry::CheckInEvent) } + error_events = sentry_events.select { |e| e.is_a?(Sentry::ErrorEvent) } + + expect(check_ins.map { |e| e.to_h[:status] }).to eq(%i[in_progress error]) + expect(error_events.size).to eq(1) + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 7f8cdb9fb..8f2c30b11 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -10,6 +10,7 @@ it_behaves_like "an ActiveJob backend that unwraps DeserializationError" it_behaves_like "an ActiveJob backend that emits a consumer transaction" it_behaves_like "an ActiveJob backend that records scheduled_at on delayed jobs" + it_behaves_like "an ActiveJob backend that emits cron check-ins for monitor jobs" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end From 103eb4f3cea9c61dee960df398130e01a7184a96 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:51:04 +0000 Subject: [PATCH 16/33] test(active_job): add structured_logging shared example Extend the harness with a configure_sentry hook so shared examples can override Sentry.init at make_basic_app time, then add a shared example asserting Job enqueued / Job performed structured log entries. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sentry_instrumented_backend.rb | 1 + .../shared_examples/structured_logging.rb | 37 +++++++++++++++++++ .../spec/active_job/support/harness.rb | 3 +- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 sentry-rails/spec/active_job/shared_examples/structured_logging.rb diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index 8f2c30b11..d541ecb0d 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -11,6 +11,7 @@ it_behaves_like "an ActiveJob backend that emits a consumer transaction" it_behaves_like "an ActiveJob backend that records scheduled_at on delayed jobs" it_behaves_like "an ActiveJob backend that emits cron check-ins for monitor jobs" + it_behaves_like "an ActiveJob backend that produces structured logs" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" end diff --git a/sentry-rails/spec/active_job/shared_examples/structured_logging.rb b/sentry-rails/spec/active_job/shared_examples/structured_logging.rb new file mode 100644 index 000000000..7a6fbc5df --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/structured_logging.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that produces structured logs" do + let(:configure_sentry) do + proc do |config, _app| + config.enable_logs = true + config.rails.structured_logging.enabled = true + config.rails.structured_logging.subscribers = { + active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber + } + end + end + + let(:simple_job) do + job_fixture do + def perform; end + end + end + + it "emits structured log entries for enqueue and perform events" do + simple_job.perform_later + drain + Sentry.get_current_client.flush + + enqueue_log = sentry_logs.find { |log| log[:body]&.include?("Job enqueued") } + perform_log = sentry_logs.find { |log| log[:body]&.include?("Job performed") } + + expect(enqueue_log).not_to be_nil + expect(enqueue_log[:level]).to eq("info") + expect(enqueue_log[:attributes][:job_class][:value]).to eq(simple_job.name) + + expect(perform_log).not_to be_nil + expect(perform_log[:level]).to eq("info") + expect(perform_log[:attributes][:job_class][:value]).to eq(simple_job.name) + expect(perform_log[:attributes][:duration_ms][:value]).to be >= 0 + end +end diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index b34942a3c..0095db116 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -2,9 +2,10 @@ RSpec.shared_context "active_job backend harness" do |adapter:| let(:adapter) { adapter } + let(:configure_sentry) { proc {} } around do |example| - make_basic_app + make_basic_app(&configure_sentry) setup_sentry_test ::ActiveJob::Base.queue_adapter = adapter From 9e15b088c02b42f560691991a460d86d3aa80ae9 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:51:18 +0000 Subject: [PATCH 17/33] style(active_job): fix space inside empty proc braces Co-Authored-By: Claude Opus 4.7 (1M context) --- sentry-rails/spec/active_job/support/harness.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 0095db116..54b93dde9 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -2,7 +2,7 @@ RSpec.shared_context "active_job backend harness" do |adapter:| let(:adapter) { adapter } - let(:configure_sentry) { proc {} } + let(:configure_sentry) { proc { } } around do |example| make_basic_app(&configure_sentry) From eaabdbffd04de1bf10c3f27ae2710d61c1a22567 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 08:53:16 +0000 Subject: [PATCH 18/33] test(active_job): remove monolithic activejob_spec.rb Every it block from spec/sentry/rails/activejob_spec.rb is now covered by a per-area shared example under spec/active_job/shared_examples/, exercised on the test adapter via spec/active_job/test_adapter_spec.rb. The named test_jobs.rb fixtures are kept in place for the legacy log-subscriber and Ruby 2.7 specs that still depend on them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../spec/sentry/rails/activejob_spec.rb | 400 ------------------ 1 file changed, 400 deletions(-) delete mode 100644 sentry-rails/spec/sentry/rails/activejob_spec.rb diff --git a/sentry-rails/spec/sentry/rails/activejob_spec.rb b/sentry-rails/spec/sentry/rails/activejob_spec.rb deleted file mode 100644 index b16b272a8..000000000 --- a/sentry-rails/spec/sentry/rails/activejob_spec.rb +++ /dev/null @@ -1,400 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require_relative "../../support/test_jobs" - -RSpec.describe "without Sentry initialized", type: :job do - it "runs job" do - expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) - end - - it "returns #perform method's return value" do - expect(NormalJob.perform_now).to eq("foo") - end -end - -RSpec.describe "ActiveJob integration", type: :job do - let(:event) do - transport.events.last.to_json_compatible - end - - let(:transport) do - Sentry.get_current_client.transport - end - - it "returns #perform method's return value" do - expect(NormalJob.perform_now).to eq("foo") - end - - describe "ActiveJob arguments serialization" do - before do - make_basic_app - end - - it "serializes ActiveRecord arguments in globalid form" do - post = Post.create! - post2 = Post.create! - - expect do - JobWithArgument.perform_now("foo", { bar: Sentry }, integer: 1, post: post, nested: { another_level: { post: post2 } }) - end.to raise_error(RuntimeError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event.dig("extra", "arguments")).to eq( - [ - "foo", - { "bar" => "Sentry" }, - { - "integer" => 1, - "post" => post.to_global_id.to_s, - "nested" => { "another_level" => { "post" => post2.to_global_id.to_s } } - } - ] - ) - end - - it "handles problematic globalid conversion gracefully" do - post = Post.create! - - def post.to_global_id - raise - end - - expect do - JobWithArgument.perform_now(integer: 1, post: post) - end.to raise_error(RuntimeError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event.dig("extra", "arguments")).to eq( - [ - { - "integer" => 1, - "post" => post.to_s - } - ] - ) - end - - it "serializes range arguments gracefully when Range#map is implemented" do - post = Post.create! - - expect do - JobWithArgument.perform_now("foo", { bar: Sentry }, integer: 1, post: post, range: 1..3) - end.to raise_error(RuntimeError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event.dig("extra", "arguments")).to eq( - [ - "foo", - { "bar" => "Sentry" }, - { - "integer" => 1, - "post" => post.to_global_id.to_s, - "range" => [1, 2, 3] - } - ] - ) - end - - it "serializes range arguments gracefully when Range consists of ActiveSupport::TimeWithZone" do - post = Post.create! - range = 5.days.ago...1.day.ago - - expect do - JobWithArgument.perform_now("foo", { bar: Sentry }, integer: 1, post: post, range: range) - end.to raise_error(RuntimeError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event.dig("extra", "arguments")).to eq( - [ - "foo", - { "bar" => "Sentry" }, - { - "integer" => 1, - "post" => post.to_global_id.to_s, - "range" => "#{range.first}...#{range.last}" - } - ] - ) - end - end - - describe "handling context" do - before do - make_basic_app - end - - it "adds useful context to extra" do - expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event.dig("extra", "active_job")).to eq("FailedJob") - expect(event.dig("extra", "job_id")).to be_a(String) - expect(event.dig("extra", "provider_job_id")).to be_nil - expect(event.dig("extra", "arguments")).to eq([]) - - expect(event.dig("tags", "job_id")).to eq(event.dig("extra", "job_id")) - expect(event.dig("tags", "provider_job_id")).to eq(event.dig("extra", "provider_job_id")) - last_frame = event.dig("exception", "values", 0, "stacktrace", "frames").last - expect(last_frame["vars"]).to include({ "a" => "1", "b" => "0" }) - end - - it "clears context" do - expect { FailedWithExtraJob.perform_now }.to raise_error(FailedWithExtraJob::TestError) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - - expect(event["extra"]["foo"]).to eq("bar") - - expect(Sentry.get_current_scope.extra).to eq({}) - end - end - - context "with tracing enabled" do - before do - make_basic_app do |config| - config.traces_sample_rate = 1.0 - end - end - - it "sends transaction" do - QueryPostJob.perform_now - - expect(transport.events.size).to be(1) - - transaction = transport.events.last - expect(transaction.transaction).to eq("QueryPostJob") - expect(transaction.transaction_info).to eq({ source: :task }) - expect(transaction.contexts.dig(:trace, :trace_id)).to be_present - expect(transaction.contexts.dig(:trace, :span_id)).to be_present - expect(transaction.contexts.dig(:trace, :status)).to eq("ok") - expect(transaction.contexts.dig(:trace, :op)).to eq("queue.active_job") - expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job") - - expect(transaction.spans.count).to eq(1) - expect(transaction.spans.first[:op]).to eq("db.sql.active_record") - end - - context "with error" do - it "sends transaction and associates it with the event" do - expect { FailedWithExtraJob.perform_now }.to raise_error(FailedWithExtraJob::TestError) - - expect(transport.events.size).to be(2) - - transaction = transport.events.first - expect(transaction.transaction).to eq("FailedWithExtraJob") - expect(transaction.transaction_info).to eq({ source: :task }) - expect(transaction.contexts.dig(:trace, :trace_id)).to be_present - expect(transaction.contexts.dig(:trace, :span_id)).to be_present - expect(transaction.contexts.dig(:trace, :status)).to eq("internal_error") - expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job") - - event = transport.events.last - expect(event.transaction).to eq("FailedWithExtraJob") - expect(event.contexts.dig(:trace, :trace_id)).to eq(transaction.contexts.dig(:trace, :trace_id)) - end - end - end - - context "when DeserializationError happens in user's jobs" do - before do - make_basic_app - end - - class DeserializationErrorJob < ActiveJob::Base - def perform - 1/0 - rescue - raise ActiveJob::DeserializationError - end - end - - it "reports the root cause to Sentry" do - expect do - DeserializationErrorJob.perform_now - end.to raise_error(ActiveJob::DeserializationError, /divided by 0/) - - expect(transport.events.size).to be(1) - - event = transport.events.last.to_json_compatible - expect(event.dig("exception", "values", 0, "type")).to eq("ZeroDivisionError") - end - end - - context "using rescue_from" do - before do - make_basic_app - end - - it 'does not trigger Sentry' do - expect_any_instance_of(RescuedActiveJob).to receive(:rescue_callback).once.and_call_original - - expect { RescuedActiveJob.perform_now }.not_to raise_error - - expect(transport.events.size).to eq(0) - end - - context "with exception in rescue_from" do - it "reports both the original error and callback error" do - expect_any_instance_of(ProblematicRescuedActiveJob).to receive(:rescue_callback).once.and_call_original - - expect { ProblematicRescuedActiveJob.perform_now }.to raise_error(RuntimeError) - - expect(transport.events.size).to eq(1) - - event = transport.events.first - exceptions_data = event.exception.to_h[:values] - - expect(exceptions_data.count).to eq(2) - expect(exceptions_data[0][:type]).to eq("FailedJob::TestError") - expect(exceptions_data[1][:type]).to eq("RuntimeError") - end - end - end - - context "when we are using an adapter which has a specific integration" do - before do - make_basic_app do |config| - config.rails.skippable_job_adapters = ["ActiveJob::QueueAdapters::TestAdapter"] - end - end - - it "does not trigger sentry and re-raises" do - expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) - expect(transport.events.size).to eq(0) - end - end - - context "with cron monitoring mixin" do - before do - make_basic_app - end - - context "normal job" do - it "returns #perform method's return value" do - expect(NormalJobWithCron.perform_now).to eq("foo") - end - - it "captures two check ins" do - NormalJobWithCron.perform_now - - expect(transport.events.size).to eq(2) - - first = transport.events[0] - check_in_id = first.check_in_id - expect(first).to be_a(Sentry::CheckInEvent) - expect(first.to_h).to include( - type: 'check_in', - check_in_id: check_in_id, - monitor_slug: "normaljobwithcron", - status: :in_progress - ) - - second = transport.events[1] - expect(second).to be_a(Sentry::CheckInEvent) - expect(second.to_h).to include( - :duration, - type: 'check_in', - check_in_id: check_in_id, - monitor_slug: "normaljobwithcron", - status: :ok - ) - end - end - - context "failed job" do - it "captures two check ins" do - expect { FailedJobWithCron.perform_now }.to raise_error(FailedJob::TestError) - - expect(transport.events.size).to eq(3) - - first = transport.events[0] - check_in_id = first.check_in_id - expect(first).to be_a(Sentry::CheckInEvent) - expect(first.to_h).to include( - type: 'check_in', - check_in_id: check_in_id, - monitor_slug: "failed_job", - status: :in_progress, - monitor_config: { schedule: { type: :crontab, value: "5 * * * *" } } - ) - - second = transport.events[1] - expect(second).to be_a(Sentry::CheckInEvent) - expect(second.to_h).to include( - :duration, - type: 'check_in', - check_in_id: check_in_id, - monitor_slug: "failed_job", - status: :error, - monitor_config: { schedule: { type: :crontab, value: "5 * * * *" } } - ) - end - end - end - - describe "Reporting on retry errors", skip: RAILS_VERSION < 7.0 do - before do - make_basic_app - end - - context "when active_job_report_on_retry_error is true" do - before do - Sentry.configuration.rails.active_job_report_on_retry_error = true - end - - after do - Sentry.configuration.rails.active_job_report_on_retry_error = false - end - - it "reports 3 exceptions" do - allow(Sentry::Rails::ActiveJobExtensions::SentryReporter) - .to receive(:capture_exception).and_call_original - - FailedJobWithRetryOn.perform_later rescue nil - - perform_enqueued_jobs - perform_enqueued_jobs - perform_enqueued_jobs rescue nil - - expect(Sentry::Rails::ActiveJobExtensions::SentryReporter) - .to have_received(:capture_exception) - .exactly(3).times - end - end - - context "when active_job_report_on_retry_error is false" do - it "reports 1 exception on final attempt failure" do - allow(Sentry::Rails::ActiveJobExtensions::SentryReporter) - .to receive(:capture_exception).and_call_original - - FailedJobWithRetryOn.perform_later rescue nil - - perform_enqueued_jobs - perform_enqueued_jobs - perform_enqueued_jobs rescue nil - - expect(Sentry::Rails::ActiveJobExtensions::SentryReporter) - .to have_received(:capture_exception) - .exactly(1).times - end - end - end -end From 1df034e9a19b5fe2c071cd001bd5dbfab96ba06b Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:27:10 +0000 Subject: [PATCH 19/33] test(active_job): update spec pattern to include active_job specs --- sentry-rails/Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/Rakefile b/sentry-rails/Rakefile index abb28f1a5..a591eba8e 100644 --- a/sentry-rails/Rakefile +++ b/sentry-rails/Rakefile @@ -4,7 +4,7 @@ require "bundler/gem_tasks" require_relative "../lib/sentry/test/rake_tasks" Sentry::Test::RakeTasks.define_spec_tasks( - spec_pattern: "spec/sentry/**/*_spec.rb", + spec_pattern: "{spec/sentry,spec/active_job}/**/*_spec.rb", spec_rspec_opts: "--order rand --format progress", isolated_specs_pattern: "spec/isolated/**/*_spec.rb", isolated_rspec_opts: "--format progress" From 1a9f28546f22091af7608ca89f417343f1c5f111 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:27:19 +0000 Subject: [PATCH 20/33] test(active_job): enhance error context spec with variable tracking --- .../spec/active_job/shared_examples/error_context.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry-rails/spec/active_job/shared_examples/error_context.rb b/sentry-rails/spec/active_job/shared_examples/error_context.rb index a07a31e81..897d8dede 100644 --- a/sentry-rails/spec/active_job/shared_examples/error_context.rb +++ b/sentry-rails/spec/active_job/shared_examples/error_context.rb @@ -4,6 +4,8 @@ let(:failing_job) do job_fixture do def perform + a = 1 + b = 0 raise "boom from failing_job spec" end end @@ -30,5 +32,8 @@ def perform job_id: event.extra[:job_id], provider_job_id: event.extra[:provider_job_id] ) + + last_frame = event.exception.values.first.stacktrace.frames.last + expect(last_frame.vars).to include(a: "1", b: "0") end end From a02d99f4606d9209b23409be5be1d7ec6a5fe4e2 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:27:33 +0000 Subject: [PATCH 21/33] test(active_job): add shared example for preserving job return value --- .../shared_examples/return_value_preservation.rb | 16 ++++++++++++++++ .../sentry_instrumented_backend.rb | 1 + 2 files changed, 17 insertions(+) create mode 100644 sentry-rails/spec/active_job/shared_examples/return_value_preservation.rb diff --git a/sentry-rails/spec/active_job/shared_examples/return_value_preservation.rb b/sentry-rails/spec/active_job/shared_examples/return_value_preservation.rb new file mode 100644 index 000000000..ab97562fd --- /dev/null +++ b/sentry-rails/spec/active_job/shared_examples/return_value_preservation.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples "an ActiveJob backend that preserves the job return value" do + let(:returning_job) do + job_fixture do + def perform + "return value from job" + end + end + end + + it "returns the job's perform value from perform_now" do + result = returning_job.perform_now + expect(result).to eq("return value from job") + end +end diff --git a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb index d541ecb0d..c11f17df3 100644 --- a/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb +++ b/sentry-rails/spec/active_job/shared_examples/sentry_instrumented_backend.rb @@ -14,4 +14,5 @@ it_behaves_like "an ActiveJob backend that produces structured logs" it_behaves_like "an ActiveJob backend that respects retry semantics" it_behaves_like "an ActiveJob backend that respects discard semantics" + it_behaves_like "an ActiveJob backend that preserves the job return value" end From 1c7dd7804142b77728e25ffeeb83a460366a062a Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:27:43 +0000 Subject: [PATCH 22/33] test(active_job): enhance serialization spec for ActiveSupport::TimeWithZone ranges --- .../shared_examples/argument_serialization.rb | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb index c925d7768..bef955dd7 100644 --- a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb +++ b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb @@ -44,16 +44,17 @@ def event_arguments expect(event_arguments).to eq([[1, 2, 3]]) end - it "stringifies ActiveSupport::TimeWithZone ranges" do - range = 1.day.ago..Time.zone.now + it "stringifies ActiveSupport::TimeWithZone ranges preserving the boundary operator" do + range = 1.day.ago...Time.zone.now expect do failing_job.perform_later(range) drain end.to raise_error(RuntimeError, /boom from argument_serialization spec/) - expect(event_arguments.first).to be_a(String) - expect(event_arguments.first).to include("..") + serialized = event_arguments.first + expect(serialized).to be_a(String) + expect(serialized).to eq("#{range.first}...#{range.last}") end it "falls back to the original argument when to_global_id raises" do @@ -76,4 +77,20 @@ def passed_post.to_global_id expect(event_arguments).to eq([post]) end + + it "passes through objects that do not respond to to_global_id unchanged" do + mod = Module.new + + module_job = job_fixture do + define_method(:perform) do |_mod| + raise "boom from argument_serialization spec" + end + end + + expect do + module_job.perform_now(mod) + end.to raise_error(RuntimeError, /boom from argument_serialization spec/) + + expect(event_arguments).to eq([mod]) + end end From 3916d0f1d7dc54e12fd78386e216cae7122c9dfc Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:27:54 +0000 Subject: [PATCH 23/33] test(active_job): add spec for ActiveJob behavior without Sentry initialized --- .../spec/active_job/without_sentry_spec.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 sentry-rails/spec/active_job/without_sentry_spec.rb diff --git a/sentry-rails/spec/active_job/without_sentry_spec.rb b/sentry-rails/spec/active_job/without_sentry_spec.rb new file mode 100644 index 000000000..d3622134d --- /dev/null +++ b/sentry-rails/spec/active_job/without_sentry_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "spec_helper" + +# These examples exercise the `if !Sentry.initialized?` short-circuit in +# ActiveJobExtensions#perform_now. They MUST run with Sentry not initialized, +# so each example resets all SDK globals before running. +RSpec.describe "ActiveJob without Sentry initialized", type: :job do + around do |example| + reset_sentry_globals! + example.run + end + + it "runs the job normally (raises the original error)" do + expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) + end + + it "returns the #perform method's return value" do + expect(NormalJob.perform_now).to eq("foo") + end +end From ef05dbafc388217d695daf4dabf9ab6dd511628d Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:28:00 +0000 Subject: [PATCH 24/33] test(active_job): refactor Sentry configuration setup and add db span tracking --- .../tracing/consumer_transaction.rb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb b/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb index cb5106164..5cc2d1fb3 100644 --- a/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb +++ b/sentry-rails/spec/active_job/shared_examples/tracing/consumer_transaction.rb @@ -16,7 +16,7 @@ def perform end context "with traces_sample_rate = 1.0" do - before { Sentry.configuration.traces_sample_rate = 1.0 } + let(:configure_sentry) { proc { |config| config.traces_sample_rate = 1.0 } } it "captures a successful transaction with name, op, origin, source, and ok status" do successful_job.perform_later @@ -32,6 +32,23 @@ def perform expect(transaction.contexts.dig(:trace, :status)).to eq("ok") end + it "records a db.sql.active_record child span when the job performs a query" do + query_job = job_fixture do + def perform + Post.all.to_a + end + end + + query_job.perform_later + drain + + transaction = sentry_events.find { |e| e.is_a?(Sentry::TransactionEvent) } + expect(transaction).not_to be_nil + + db_span = transaction.spans.find { |s| s[:op] == "db.sql.active_record" } + expect(db_span).not_to be_nil + end + it "marks the failing transaction internal_error and links the error event by trace_id" do expect do failing_job.perform_later From 8e9e231a112d64c0e4bf2cade5fd372d3abc5d74 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:28:04 +0000 Subject: [PATCH 25/33] test(active_job): enhance cron check-ins specs with slugs and monitor config --- .../shared_examples/cron_check_ins.rb | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb b/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb index e9aefae26..6de6c5209 100644 --- a/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb +++ b/sentry-rails/spec/active_job/shared_examples/cron_check_ins.rb @@ -4,7 +4,7 @@ let(:cron_job) do job_fixture do include Sentry::Cron::MonitorCheckIns - sentry_monitor_check_ins + sentry_monitor_check_ins slug: "test-cron-ok-job" def perform "ok" @@ -15,7 +15,10 @@ def perform let(:failing_cron_job) do job_fixture do include Sentry::Cron::MonitorCheckIns - sentry_monitor_check_ins + sentry_monitor_check_ins( + slug: "test-cron-fail-job", + monitor_config: Sentry::Cron::MonitorConfig.from_crontab("5 * * * *") + ) def perform raise "boom from failing_cron_job spec" @@ -23,7 +26,7 @@ def perform end end - it "emits in_progress and ok check-ins for a successful job" do + it "emits in_progress and ok check-ins with correct slug for a successful job" do cron_job.perform_later drain @@ -31,12 +34,26 @@ def perform expect(check_ins.size).to eq(2) first, second = check_ins - expect(first.to_h).to include(type: "check_in", status: :in_progress) - expect(second.to_h).to include(type: "check_in", status: :ok, check_in_id: first.check_in_id) + expect(first.to_h).to include( + type: "check_in", + status: :in_progress, + monitor_slug: "test-cron-ok-job" + ) + expect(second.to_h).to include( + type: "check_in", + status: :ok, + check_in_id: first.check_in_id, + monitor_slug: "test-cron-ok-job" + ) expect(second.to_h).to have_key(:duration) end - it "emits in_progress and error check-ins plus an exception event for a failing job" do + it "returns the job's perform value through the cron mixin" do + result = cron_job.perform_now + expect(result).to eq("ok") + end + + it "emits in_progress and error check-ins with monitor_config for a failing job" do expect do failing_cron_job.perform_later drain @@ -46,6 +63,10 @@ def perform error_events = sentry_events.select { |e| e.is_a?(Sentry::ErrorEvent) } expect(check_ins.map { |e| e.to_h[:status] }).to eq(%i[in_progress error]) + expect(check_ins.map { |e| e.to_h[:monitor_slug] }).to all(eq("test-cron-fail-job")) + expect(check_ins.map { |e| e.to_h[:monitor_config] }).to all(include( + schedule: { type: :crontab, value: "5 * * * *" } + )) expect(error_events.size).to eq(1) end end From 7b149f5bcef3b08bd47898d2b6972fc466be8e7b Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 09:47:34 +0000 Subject: [PATCH 26/33] fixup(active_job): update drain method for old rubies Co-authored-by: Copilot --- sentry-rails/spec/active_job/support/harness.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 54b93dde9..693f0d1ea 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -32,7 +32,8 @@ def reset_adapter(_adapter) def drain(at: nil) case adapter when :test - perform_enqueued_jobs(at: at) + kwargs = at ? { at: at } : {} + perform_enqueued_jobs(**kwargs) else raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end From f72ae068cec585f3b6b5800c2e4d2a1a834ab973 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 10:43:58 +0000 Subject: [PATCH 27/33] fix(active_job): remove nil from global serializers set during app re-initialization --- .../dummy/test_rails_app/config/application.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sentry-rails/spec/dummy/test_rails_app/config/application.rb b/sentry-rails/spec/dummy/test_rails_app/config/application.rb index 3d06d8e9a..6275220de 100644 --- a/sentry-rails/spec/dummy/test_rails_app/config/application.rb +++ b/sentry-rails/spec/dummy/test_rails_app/config/application.rb @@ -143,6 +143,20 @@ def before_initialize! end def after_initialize! + # The active_job.custom_serializers railtie initializer calls + # add_serializers(app.config.active_job.custom_serializers). Under some + # Rails/Ruby combinations custom_serializers resolves to nil instead of the + # railtie default of [], inserting nil into the global serializers Set. + # Remove it right after initialization so it cannot affect any test. + # Rails < 8 uses mattr_accessor _additional_serializers; Rails 8+ uses @serializers. + if defined?(::ActiveJob::Serializers) + if ::ActiveJob::Serializers.respond_to?(:_additional_serializers) + ::ActiveJob::Serializers._additional_serializers.delete(nil) + elsif ::ActiveJob::Serializers.instance_variable_defined?(:@serializers) + ::ActiveJob::Serializers.instance_variable_get(:@serializers).delete(nil) + end + end + if Sentry.initialized? # Run a query to make sure the schema metadata gets loaded and cached Post.all.to_a.inspect From 1ba54c56eb87df629e509ca3bd9eca873861efb6 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:08:57 +0000 Subject: [PATCH 28/33] test(active_job): skip range serialization examples on Rails < 7.0 --- .../spec/active_job/shared_examples/argument_serialization.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb index bef955dd7..8c7332306 100644 --- a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb +++ b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb @@ -35,7 +35,7 @@ def event_arguments expect(event_arguments).to eq([{ wrapper: { post: post.to_global_id.to_s } }]) end - it "expands integer ranges into arrays" do + it "expands integer ranges into arrays", skip: RAILS_VERSION < 7.0 do expect do failing_job.perform_later(1..3) drain @@ -44,7 +44,7 @@ def event_arguments expect(event_arguments).to eq([[1, 2, 3]]) end - it "stringifies ActiveSupport::TimeWithZone ranges preserving the boundary operator" do + it "stringifies ActiveSupport::TimeWithZone ranges preserving the boundary operator", skip: RAILS_VERSION < 7.0 do range = 1.day.ago...Time.zone.now expect do From 459136ff7b005b78cfa50c87f5671263e9f05656 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:15:27 +0000 Subject: [PATCH 29/33] test(active_job): skip scheduled_at example on Rails < 6.1 (no perform_enqueued_jobs at:) --- sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb b/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb index 7b2af62b5..64e19dda9 100644 --- a/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb +++ b/sentry-rails/spec/active_job/shared_examples/scheduled_jobs.rb @@ -9,7 +9,7 @@ def perform end end - it "records scheduled_at in the event extras" do + it "records scheduled_at in the event extras", skip: RAILS_VERSION < 6.1 do expect do failing_job.set(wait: 5.seconds).perform_later drain(at: 1.minute.from_now) From 71afe16b7f7fcd23509a92f99fc67b8fe48cd85d Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:22:51 +0000 Subject: [PATCH 30/33] test(active_job): fix drain on Rails 5.2 (perform_enqueued_jobs requires block) --- sentry-rails/spec/active_job/support/harness.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sentry-rails/spec/active_job/support/harness.rb b/sentry-rails/spec/active_job/support/harness.rb index 693f0d1ea..4e489fb20 100644 --- a/sentry-rails/spec/active_job/support/harness.rb +++ b/sentry-rails/spec/active_job/support/harness.rb @@ -32,8 +32,16 @@ def reset_adapter(_adapter) def drain(at: nil) case adapter when :test - kwargs = at ? { at: at } : {} - perform_enqueued_jobs(**kwargs) + if RAILS_VERSION < 6.0 + # Rails 5.2: perform_enqueued_jobs always requires a block and only runs + # jobs enqueued *inside* the block. Manually flush already-enqueued jobs. + jobs = queue_adapter.enqueued_jobs.dup + queue_adapter.enqueued_jobs.clear + jobs.each { |payload| send(:instantiate_job, payload).perform_now } + else + kwargs = at ? { at: at } : {} + perform_enqueued_jobs(**kwargs) + end else raise NotImplementedError, "active_job backend harness has no drain strategy for adapter: #{adapter.inspect}" end From 5948fef831f9f9a4e5fa120cb03edc46a5bdae51 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:28:29 +0000 Subject: [PATCH 31/33] test(active_job): skip retry semantics examples on Rails < 6.0 --- .../spec/active_job/shared_examples/retry_semantics.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb index 8da6e51a9..4feead2f8 100644 --- a/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb +++ b/sentry-rails/spec/active_job/shared_examples/retry_semantics.rb @@ -11,7 +11,7 @@ def perform end end - it "captures one error event after retries are exhausted" do + it "captures one error event after retries are exhausted", skip: RAILS_VERSION < 6.0 do expect do retryable_job.perform_later 3.times { drain } @@ -28,7 +28,7 @@ def perform Sentry.configuration.rails.active_job_report_on_retry_error = true end - it "captures one error event per attempt" do + it "captures one error event per attempt", skip: RAILS_VERSION < 6.0 do expect do retryable_job.perform_later 3.times { drain } From 7ebf3998e9158fffac18608851900bcd49621030 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:46:22 +0000 Subject: [PATCH 32/33] test(active_job): compact DeserializationError backtrace to avoid JRuby NPE --- .../active_job/shared_examples/deserialization_error.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb b/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb index cf01a98da..2e275514a 100644 --- a/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb +++ b/sentry-rails/spec/active_job/shared_examples/deserialization_error.rb @@ -6,7 +6,12 @@ def perform 1 / 0 rescue - raise ActiveJob::DeserializationError + err = ActiveJob::DeserializationError.new + # DeserializationError#initialize copies $!.backtrace, which on JRuby can + # contain nil elements for frames defined in anonymous Class.new blocks. + # Compact the backtrace to avoid a JRuby NPE in traceRaise at shutdown. + err.set_backtrace(Array(err.backtrace).compact) + raise err end end end From 146e48b94746c9ddeaedcfd926827166bfcee958 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 28 Apr 2026 11:59:45 +0000 Subject: [PATCH 33/33] test(active_job): use def instead of define_method to avoid JRuby DynamicScope NPE --- .../spec/active_job/shared_examples/argument_serialization.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb index 8c7332306..43eb300df 100644 --- a/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb +++ b/sentry-rails/spec/active_job/shared_examples/argument_serialization.rb @@ -61,7 +61,7 @@ def event_arguments post = Post.create! problematic_job = job_fixture do - define_method(:perform) do |passed_post| + def perform(passed_post) def passed_post.to_global_id raise "intentional" end @@ -82,7 +82,7 @@ def passed_post.to_global_id mod = Module.new module_job = job_fixture do - define_method(:perform) do |_mod| + def perform(_mod) raise "boom from argument_serialization spec" end end