From 012a3033833e8459df92946e286bd114ba82bf66 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:38:22 +0000 Subject: [PATCH] fix(sidekiq): Start a new trace for each sidekiq-scheduler enqueue Jobs enqueued by sidekiq-scheduler all continued the scheduler thread's long-lived propagation context, so every scheduled job in a process shared one trace_id and, since 6.0, one sample_rand (an all-or-nothing sampling decision per boot). Wrap SidekiqScheduler::Scheduler#enqueue_job in a fresh hub, scope and transaction, mirroring the sidekiq-cron patch. Fixes #3144 Co-Authored-By: Neel Shah <6536764+sl0thentr0py@users.noreply.github.com> --- .../lib/sentry/sidekiq-scheduler/scheduler.rb | 40 ++++++++++++++++ .../sidekiq-scheduler/scheduler_spec.rb | 47 ++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/sentry-sidekiq/lib/sentry/sidekiq-scheduler/scheduler.rb b/sentry-sidekiq/lib/sentry/sidekiq-scheduler/scheduler.rb index 5832cdcf2..bfcc74f8a 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq-scheduler/scheduler.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq-scheduler/scheduler.rb @@ -61,6 +61,46 @@ def new_job(name, interval_type, config, schedule, options) rufus_job end + + def enqueue_job(job_config, *args) + # make sure the current thread has a clean hub + Sentry.clone_hub_to_current_thread + + Sentry.with_scope do |scope| + Sentry.with_session_tracking do + begin + scope.set_transaction_name(job_config["class"].to_s) + + transaction = sentry_start_transaction(scope) + scope.set_span(transaction) if transaction + super + + sentry_finish_transaction(transaction, 200) + rescue + sentry_finish_transaction(transaction, 500) + raise + end + end + end + end + + private + + def sentry_start_transaction(scope) + Sentry.start_transaction( + name: scope.transaction_name, + source: scope.transaction_source, + op: "queue.sidekiq-scheduler", + origin: "auto.queue.sidekiq.scheduler" + ) + end + + def sentry_finish_transaction(transaction, status_code) + return unless transaction + + transaction.set_http_status(status_code) + transaction.finish + end end end end diff --git a/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb index ce7676811..8ed8c6464 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true require 'spec_helper' +require 'sidekiq/api' return unless defined?(SidekiqScheduler::Scheduler) RSpec.describe Sentry::SidekiqScheduler::Scheduler do before do - perform_basic_setup { |c| c.enabled_patches += [:sidekiq_scheduler] } + perform_basic_setup do |c| + c.enabled_patches += [:sidekiq_scheduler] + c.traces_sample_rate = 1.0 + end end before do @@ -75,4 +79,45 @@ expect(VeryLongOuterModule::VeryVeryVeryVeryLongInnerModule::Job.sentry_monitor_config.schedule).to be_a(Sentry::Cron::MonitorSchedule::Crontab) expect(VeryLongOuterModule::VeryVeryVeryVeryLongInnerModule::Job.sentry_monitor_config.schedule.value).to eq('* * * * *') end + + describe '#enqueue_job' do + let(:transport) { Sentry.get_current_client.transport } + let(:scheduler) { SidekiqScheduler::Scheduler.instance } + + before do + Sidekiq::Queue.all.each(&:clear) + end + + it 'adds job to sidekiq within transaction' do + scheduler.enqueue_job({ 'class' => 'HappyWorkerForScheduler' }) + + expect(::Sidekiq::Queue.new.size).to eq(1) + expect(transport.events.count).to eq(1) + + transaction = transport.events.last + expect(transaction.transaction).to eq('HappyWorkerForScheduler') + expect(transaction.contexts.dig(:trace, :op)).to eq('queue.sidekiq-scheduler') + expect(transaction.contexts.dig(:trace, :origin)).to eq('auto.queue.sidekiq.scheduler') + + span = transaction.spans.detect { |span| span[:op] == 'queue.publish' } + expect(span).not_to be_nil + expect(span[:data]['messaging.destination.name']).to eq('default') + end + + it 'starts a new trace for each enqueue' do + scheduler.enqueue_job({ 'class' => 'HappyWorkerForScheduler' }) + scheduler.enqueue_job({ 'class' => 'HappyWorkerForScheduler' }) + + events = transport.events + expect(events.count).to eq(2) + expect(events[0].dynamic_sampling_context['trace_id']).not_to eq(events[1].dynamic_sampling_context['trace_id']) + + jobs = ::Sidekiq::Queue.new.map(&:item) + trace_ids = jobs.map { |job| job.dig('trace_propagation_headers', 'sentry-trace').split('-').first } + sample_rands = jobs.map { |job| job.dig('trace_propagation_headers', 'baggage')[/sentry-sample_rand=([^,]+)/, 1] } + + expect(trace_ids).to match_array(events.map { |event| event.dynamic_sampling_context['trace_id'] }) + expect(sample_rands.uniq.size).to eq(2) + end + end end