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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions sentry-rails/lib/sentry/rails/active_job.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "set"
require "sentry/rails/serializer"
require "sentry/rails/error_reporter_context"

module Sentry
module Rails
Expand Down Expand Up @@ -81,6 +83,8 @@ class SentryReporter
}

class << self
include ErrorReporterContext

def producer_callback_registered?
@producer_callback_registered ||= false
end
Expand Down Expand Up @@ -223,6 +227,7 @@ def capture_exception(job, e)
job_id: job.job_id,
provider_job_id: job.provider_job_id
},
contexts: execution_context,
# Send synchronously: a worker process may exit before the async
# background worker flushes its queue, which would drop the event.
hint: { background: false }
Expand Down Expand Up @@ -278,22 +283,7 @@ def sentry_context(job)
end

def sentry_serialize_arguments(argument)
case argument
when Range
if (argument.begin || argument.end).is_a?(ActiveSupport::TimeWithZone)
argument.to_s
else
argument.map { |v| sentry_serialize_arguments(v) }
end
when Hash
argument.transform_values { |v| sentry_serialize_arguments(v) }
when Array, Enumerable
argument.map { |v| sentry_serialize_arguments(v) }
when ->(v) { v.respond_to?(:to_global_id) }
argument.to_global_id.to_s rescue argument
else
argument
end
Sentry::Rails::Serializer.serialize(argument)
end

private
Expand Down
6 changes: 5 additions & 1 deletion sentry-rails/lib/sentry/rails/capture_exceptions.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# frozen_string_literal: true

require "sentry/rails/error_reporter_context"

module Sentry
module Rails
class CaptureExceptions < Sentry::Rack::CaptureExceptions
include ErrorReporterContext

RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha")
SPAN_ORIGIN = "auto.http.rails"

Expand Down Expand Up @@ -30,7 +34,7 @@ def capture_exception(exception, env)
return unless Sentry.initialized?
return if show_exceptions?(exception, env) && !Sentry.configuration.rails.report_rescued_exceptions

Sentry::Rails.capture_exception(exception).tap do |event|
Sentry::Rails.capture_exception(exception, contexts: execution_context).tap do |event|
env[ERROR_EVENT_ID_KEY] = event.event_id if event
end
end
Expand Down
24 changes: 24 additions & 0 deletions sentry-rails/lib/sentry/rails/error_reporter_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "sentry/rails/serializer"

module Sentry
module Rails
module ErrorReporterContext
SUPPORTS_EXECUTION_CONTEXT = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.0.0")

if SUPPORTS_EXECUTION_CONTEXT
def execution_context
context = ::ActiveSupport::ExecutionContext.to_h
return {} if context.empty?

{ "rails.error" => Sentry::Rails::Serializer.serialize(context) }
end
else
def execution_context
{}
end
end
end
end
end
35 changes: 35 additions & 0 deletions sentry-rails/lib/sentry/rails/serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Sentry
module Rails
module Serializer
def self.serialize(value)
case value
when Range
serialize_range(value)
when Hash
value.transform_values { |item| serialize(item) }
when Array, Enumerable
value.map { |item| serialize(item) }
when ->(item) { item.respond_to?(:to_global_id) }
serialize_global_id(value)
else
value
end
Comment thread
solnic marked this conversation as resolved.
end

def self.serialize_global_id(value)
value.to_global_id.to_s
rescue StandardError
value
end

def self.serialize_range(range)
return range.to_s if range.begin.nil? || range.end.nil?
return range.to_s if range.begin.is_a?(::ActiveSupport::TimeWithZone)

range.map { |item| serialize(item) }
end
end
end
end
30 changes: 30 additions & 0 deletions sentry-rails/spec/active_job/shared_examples/error_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,34 @@ def perform
last_frame = event.exception.values.first.stacktrace.frames.last
expect(last_frame.vars).to include(a: "1", b: "0")
end

it "includes Rails.error.set_context data attached before the job raises", skip: RAILS_VERSION < 7.0 do
job_with_context = job_fixture do
def perform
Rails.error.set_context(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
)
raise "boom with rails error context"
end
end

expect do
job_with_context.perform_later
drain
end.to raise_error(RuntimeError, /boom with rails error context/)

event = last_sentry_event

expect(event.contexts).to include(
"rails.error" => hash_including(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
)
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ def exception
raise "An unhandled exception!"
end

def exception_with_error_context
Rails.error.set_context(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
)
raise "An unhandled exception with Rails.error context!"
end

def reporting
render plain: Sentry.last_event_id
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def configure

routes.append do
get "/exception", to: "hello#exception"
get "/exception_with_error_context", to: "hello#exception_with_error_context"
get "/view_exception", to: "hello#view_exception"
get "/view", to: "hello#view"
get "/not_found", to: "hello#not_found"
Expand Down
54 changes: 54 additions & 0 deletions sentry-rails/spec/sentry/rails/serializer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Sentry::Rails::Serializer do
describe ".serialize" do
it "recursively serializes hash values" do
result = described_class.serialize(a: 1, b: { c: 2..3 })

expect(result).to eq(a: 1, b: { c: [2, 3] })
end

it "recursively serializes array elements" do
result = described_class.serialize([1, [2..3]])

expect(result).to eq([1, [[2, 3]]])
end

it "expands ranges into arrays" do
expect(described_class.serialize(1..3)).to eq([1, 2, 3])
end

context "when the range is beginless" do
it "stringifies the range instead of expanding it" do
range = (..10)

expect(described_class.serialize(range)).to eq(range.to_s)
end
end

context "when the range is endless" do
it "stringifies the range instead of expanding it" do
range = (1..)

expect(described_class.serialize(range)).to eq(range.to_s)
end
end

context "when the range boundary is an ActiveSupport::TimeWithZone" do
it "stringifies the range instead of expanding it" do
zone = ActiveSupport::TimeZone["UTC"]
range = (zone.now - 1.day)...zone.now

expect(described_class.serialize(range)).to eq(range.to_s)
end
end

it "returns values without a serialization rule unchanged" do
object = Object.new

expect(described_class.serialize(object)).to equal(object)
end
end
end
16 changes: 16 additions & 0 deletions sentry-rails/spec/sentry/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,22 @@ def capture_in_separate_process(exit_code:)

expect(transport.events.count).to eq(0)
end

it "includes Rails.error.set_context data attached before an unhandled request exception" do
get "/exception_with_error_context"

expect(transport.events.count).to eq(1)

event = transport.events.first
expect(event.contexts).to include(
"rails.error" => hash_including(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
)
)
end
end
end
end
Loading