Skip to content

Commit 76c8c42

Browse files
solnicCodex
andcommitted
feat(sidekiq): migrate to ActiveJob-first integration
Co-Authored-By: Codex <noreply@example.com>
1 parent db6e8f6 commit 76c8c42

17 files changed

Lines changed: 1676 additions & 211 deletions

‎sentry-rails/lib/sentry/rails/active_job.rb‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ def perform_now
2121
super
2222
else
2323
data = _sentry || {}
24+
transaction = Sentry.get_current_scope.get_span
25+
transaction = nil unless transaction.is_a?(Sentry::Transaction) && transaction.span_id == data["transaction_span_id"]
2426
SentryReporter.record(
2527
self,
2628
trace_headers: data["trace_propagation_headers"],
2729
user: data["user"],
28-
payload: _sentry_job_data
30+
delivery_queue: data["delivery_queue"],
31+
payload: _sentry_job_data,
32+
transaction: transaction
2933
) { super }
3034
end
3135
end
@@ -139,7 +143,7 @@ def log_producer_span_error(e)
139143
Sentry.sdk_logger&.error("sentry-rails: failed to record producer span: #{e.class}: #{e.message}\n #{Array(e.backtrace).first(5).join("\n ")}")
140144
end
141145

142-
def record(job, trace_headers: nil, user: nil, payload: nil, &block)
146+
def record(job, trace_headers: nil, user: nil, delivery_queue: nil, payload: nil, transaction: nil, &block)
143147
original_hub = Sentry.get_current_hub_internal
144148
Sentry.clone_hub_to_current_thread
145149

@@ -150,7 +154,7 @@ def record(job, trace_headers: nil, user: nil, payload: nil, &block)
150154
consumer.apply_scope_data(scope, job)
151155

152156
scope.set_transaction_name(consumer.transaction_name(job) || job.class.name, source: :task)
153-
scope.set_tags(queue: job.queue_name)
157+
scope.set_tags(queue: delivery_queue || job.queue_name)
154158

155159
scope.set_contexts(active_job: {
156160
job_class: job.class.name,
@@ -161,6 +165,7 @@ def record(job, trace_headers: nil, user: nil, payload: nil, &block)
161165

162166
transaction = consumer.start_transaction(
163167
scope, job,
168+
transaction: transaction.is_a?(Sentry::Transaction) ? transaction : nil,
164169
trace_headers: trace_headers,
165170
payload: payload,
166171
messaging_data: -> {

‎sentry-sidekiq/Rakefile‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
# frozen_string_literal: true
22

33
require "bundler/gem_tasks"
4-
require_relative "../lib/sentry/test/rake_tasks"
5-
6-
Sentry::Test::RakeTasks.define_spec_tasks(
7-
spec_pattern: "spec/sentry/**/*_spec.rb",
8-
spec_rspec_opts: "--order rand --format progress",
9-
isolated_specs_pattern: "spec/isolated/**/*_spec.rb",
10-
isolated_rspec_opts: "--format progress"
11-
)
4+
require "rspec/core/rake_task"
5+
6+
RSpec::Core::RakeTask.new(:spec) do |task|
7+
task.pattern = "spec/sentry/**/*_spec.rb"
8+
task.rspec_opts = "--order rand --format progress"
9+
end
10+
11+
namespace :spec do
12+
isolated_tasks = Dir["spec/isolated/**/*_spec.rb"].sort.map do |file|
13+
task_name = "isolated:#{file.delete_prefix("spec/isolated/").delete_suffix("_spec.rb")}"
14+
15+
RSpec::Core::RakeTask.new(task_name) do |task|
16+
task.pattern = file
17+
task.rspec_opts = "--format progress"
18+
end
19+
20+
task_name
21+
end
22+
23+
task isolated: isolated_tasks
24+
end
1225

1326
task default: [:spec, :"spec:isolated"]

‎sentry-sidekiq/lib/sentry-sidekiq.rb‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "sentry/integrable"
66
require "sentry/sidekiq/version"
77
require "sentry/sidekiq/configuration"
8+
require "sentry/sidekiq/job_extension"
89
require "sentry/sidekiq/error_handler"
910
require "sentry/sidekiq/sentry_context_middleware"
1011

@@ -14,12 +15,18 @@ module Sidekiq
1415

1516
register_integration name: "sidekiq", version: Sentry::Sidekiq::VERSION
1617

18+
def self.job_extension
19+
@job_extension ||= JobExtension.new
20+
end
21+
22+
Sentry.register_job_extension(job_extension)
23+
1724
if defined?(::Rails::Railtie)
1825
class Railtie < ::Rails::Railtie
1926
config.after_initialize do
2027
next unless Sentry.initialized? && defined?(::Sentry::Rails)
2128

22-
Sentry.configuration.rails.skippable_job_adapters << "ActiveJob::QueueAdapters::SidekiqAdapter"
29+
Sentry.configuration.rails.skippable_job_adapters << JobExtension::ACTIVE_JOB_ADAPTER
2330
end
2431
end
2532
end

‎sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,7 @@ def filtered
3535
end
3636

3737
def transaction_name
38-
class_name = (context["wrapped"] || context["class"] ||
39-
(context[:job] && (context[:job]["wrapped"] || context[:job]["class"]))
40-
)
41-
42-
if class_name
43-
"#{SIDEKIQ_NAME}/#{class_name}"
44-
elsif context[:event]
45-
"#{SIDEKIQ_NAME}/#{context[:event]}"
46-
else
47-
SIDEKIQ_NAME
48-
end
38+
Sentry::Sidekiq.job_extension.error_context_transaction_name(context)
4939
end
5040

5141
private
Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# frozen_string_literal: true
22

33
require "sentry/sidekiq/context_filter"
4+
require "sentry/sidekiq/failure_reporting"
5+
require "sentry/sidekiq/sentry_context_middleware"
46

57
module Sentry
68
module Sidekiq
79
class ErrorHandler
8-
WITH_SIDEKIQ_7 = ::Gem::Version.new(::Sidekiq::VERSION) >= ::Gem::Version.new("7.0")
10+
HANDLED_SIGNATURE = :@__sentry_sidekiq_handled
11+
12+
def initialize(failure_reporting: FailureReporting.new, consumer: SentryContextServerMiddleware.consumer, job_extension: Sentry::Sidekiq.job_extension)
13+
@failure_reporting = failure_reporting
14+
@consumer = consumer
15+
@job_extension = job_extension
16+
end
917

1018
# @param ex [Exception] the exception / error that occured
1119
# @param context [Hash or Array] Sidekiq error context
@@ -15,84 +23,21 @@ class ErrorHandler
1523
# https://github.com/sidekiq/sidekiq/pull/6051
1624
def call(ex, context, sidekiq_config = nil)
1725
return unless Sentry.initialized?
18-
19-
context_filter = Sentry::Sidekiq::ContextFilter.new(context)
26+
return if ex.instance_variable_get(HANDLED_SIGNATURE)
2027

2128
scope = Sentry.get_current_scope
22-
scope.set_transaction_name(context_filter.transaction_name, source: :task) unless scope.transaction_name
23-
24-
retry_lim = retry_limit(context, sidekiq_config)
25-
26-
# If Sentry is configured to only report an error _after_ all retries have been exhausted,
27-
# and if the job is retryable, and have not exceeded the retry_limit,
28-
# return early.
29-
if Sentry.configuration.sidekiq.report_after_job_retries && retryable?(context)
30-
retry_count = context.dig(:job, "retry_count")
31-
if retry_count.nil? || retry_count < retry_lim - 1
32-
return
33-
end
34-
end
35-
36-
# See if we want to ignore jobs that have dead: false
37-
return if Sentry.configuration.sidekiq.report_only_dead_jobs && context.dig(:job, "dead") == false
38-
39-
# Check if the retry count is below the attempt_threshold
40-
attempt_threshold = context.dig(:job, "attempt_threshold")
41-
if attempt_threshold && retryable?(context)
42-
attempt_threshold = attempt_threshold.to_i
43-
retry_count = context.dig(:job, "retry_count")
44-
# attempt 1 - retry_count is nil
45-
# attempt 2 - this is your first retry so retry_count is 0
46-
# attempt 3 - you have retried once, retry_count is 1
47-
attempt = retry_count.nil? ? 1 : retry_count.to_i + 2
48-
# Cap at the final attempt so jobs with fewer retries than the threshold still report.
49-
effective_threshold = [attempt_threshold, retry_lim + 1].min
29+
scope.set_transaction_name(@job_extension.error_context_transaction_name(context), source: :task) unless scope.transaction_name
5030

51-
return if attempt < effective_threshold
52-
end
31+
job = context[:job]
32+
return unless @failure_reporting.report?(job, sidekiq_config)
5333

54-
Sentry::Sidekiq.capture_exception(
55-
ex,
56-
contexts: { sidekiq: context_filter.filtered },
57-
hint: { background: false }
34+
@consumer.capture_exception(
35+
job, ex,
36+
contexts: { sidekiq: ContextFilter.new(context).filtered }
5837
)
5938
ensure
6039
scope&.clear
6140
end
62-
63-
private
64-
65-
def retryable?(context)
66-
retry_option = context.dig(:job, "retry")
67-
# when `retry` is not specified, it's default is `true` and it means 25 retries.
68-
retry_option.nil? || retry_option == true || (retry_option.is_a?(Integer) && retry_option.positive?)
69-
end
70-
71-
# @return [Integer] the number of retries allowed for the job
72-
# Tries to fetch the retry limit from the job config first,
73-
# then falls back to Sidekiq's configuration.
74-
def retry_limit(context, sidekiq_config)
75-
limit = context.dig(:job, "retry")
76-
77-
case limit
78-
when Integer
79-
limit
80-
when TrueClass, NilClass
81-
max_retries =
82-
if WITH_SIDEKIQ_7
83-
# Sidekiq 7.1.5+ passes the config to the error handler, so we should use that.
84-
# Sidekiq 7.0 -> 7.1.5 provides ::Sidekiq.default_configuration.
85-
sidekiq_config.is_a?(::Sidekiq::Config) ?
86-
sidekiq_config[:max_retries] :
87-
::Sidekiq.default_configuration[:max_retries]
88-
else
89-
::Sidekiq.options[:max_retries]
90-
end
91-
max_retries || 25
92-
else
93-
0
94-
end
95-
end
9641
end
9742
end
9843
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/sidekiq/retry_policy"
4+
5+
module Sentry
6+
module Sidekiq
7+
class FailureReporting
8+
def initialize(retry_policy: RetryPolicy.build)
9+
@retry_policy = retry_policy
10+
end
11+
12+
def report?(job, sidekiq_config = nil, retry_limit: nil)
13+
config = Sentry.configuration.sidekiq
14+
retry_limit ||= @retry_policy.limit(job, sidekiq_config)
15+
16+
return false if config.report_after_job_retries && retries_pending?(job, retry_limit)
17+
return false if config.report_only_dead_jobs && job_value(job, "dead") == false
18+
return false if below_attempt_threshold?(job, retry_limit)
19+
20+
true
21+
end
22+
23+
private
24+
25+
def retries_pending?(job, retry_limit)
26+
return false unless @retry_policy.retryable?(job)
27+
28+
retry_count = job_value(job, "retry_count")
29+
retry_count.nil? || retry_count < retry_limit - 1
30+
end
31+
32+
def below_attempt_threshold?(job, retry_limit)
33+
attempt_threshold = job_value(job, "attempt_threshold")
34+
return false unless attempt_threshold && @retry_policy.retryable?(job)
35+
36+
[attempt_threshold.to_i, retry_limit + 1].min > attempt(job)
37+
end
38+
39+
def attempt(job)
40+
retry_count = job_value(job, "retry_count")
41+
retry_count.nil? ? 1 : retry_count.to_i + 2
42+
end
43+
44+
def job_value(job, key)
45+
job && job[key]
46+
end
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)