You spend hours trying to understand errors. Sometimes it’s so difficult you give up.
Not having enough context around the error.
Alongside every error, store structured context.
class StructuredError < StandardError
attr_reader :context
def initialize(message, context: nil)
@context = context || {}
super(message)
end
def to_h
@context.merge(message: message)
end
end
A factory method .from with named arguments can be useful:
class InvalidOrder < StructuredError
def self.from(order:)
new(
"Invalid order",
context: {
app: {
order: {
id: order.id
status: order.status,
errors: order.errors.full_messages,
customer: {
id: order.customer.id
}
},
},
}
)
end
end
class OrdersController
def process
@order = Order.find(params[:id])
@order.process!
end
end
class Order
def process!
# ...
raise InvalidOrder.from(order: self) if invalid?
# ...
end
end
Before we send the error, extract the context and merge into the tags.
# Example for Sentry - ask me for others!
Sentry.init do |config|
# ...
config.before_send = lambda do |event, hint|
# Deep merge context into tags to allow searching in the UI
event.tags.deep_merge!(hint[:exception].context)
event
rescue NoMethodError => _
event
end
end