Skip to content

Commit

Permalink
feat: add before_event hook for intercepting events (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
roelbondoc authored Jun 18, 2024
1 parent 57090ef commit 2f86728
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 23 deletions.
28 changes: 11 additions & 17 deletions lib/honeybadger/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'honeybadger/config'
require 'honeybadger/context_manager'
require 'honeybadger/notice'
require 'honeybadger/event'
require 'honeybadger/plugin'
require 'honeybadger/logging'
require 'honeybadger/worker'
Expand Down Expand Up @@ -392,28 +393,21 @@ def stop(force = false)
def event(event_type, payload = {})
init_events_worker

ts = Time.now.utc.strftime("%FT%T.%LZ")
merged = {ts: ts}

if event_type.is_a?(String)
merged[:event_type] = event_type
else
merged.merge!(Hash(event_type))
extra_payload = {}.tap do |p|
p[:request_id] = context_manager.get_request_id if context_manager.get_request_id
p[:hostname] = config[:hostname].to_s if config[:'events.attach_hostname']
end

if (request_id = context_manager.get_request_id)
merged[:request_id] = request_id
end
event = Event.new(event_type, extra_payload.merge(payload))

if config[:'events.attach_hostname']
merged[:hostname] = config[:hostname].to_s
config.before_event_hooks.each do |hook|
with_error_handling { hook.call(event) }
end

merged.merge!(Hash(payload))

return if config.ignored_events.any? { |check| merged[:event_type]&.match?(check) }
return if config.ignored_events.any? { |check| event.event_type&.match?(check) }
return if event.halted?

events_worker.push(merged)
events_worker.push(event.as_json)
end

# @api private
Expand Down Expand Up @@ -590,7 +584,7 @@ def init_metrics_worker
def with_error_handling
yield
rescue => ex
error { "Rescued an error in a before notify hook: #{ex.message}" }
error { "Rescued an error in a before hook: #{ex.message}" }
end

@instance = new(Config.new)
Expand Down
4 changes: 4 additions & 0 deletions lib/honeybadger/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ def before_notify_hooks
(ruby[:before_notify] || []).clone
end

def before_event_hooks
(ruby[:before_event] || []).clone
end

def exception_filter(&block)
if block_given?
warn('DEPRECATED: exception_filter is deprecated. Please use before_notify instead. See https://docs.honeybadger.io/ruby/support/v4-upgrade#exception_filter')
Expand Down
22 changes: 17 additions & 5 deletions lib/honeybadger/config/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,27 @@ def backend
def before_notify(action = nil, &block)
hooks = Array(get(:before_notify)).dup

if action && validate_before_action(action)
if action && validate_before_action(action, 'notify')
hooks << action
elsif block_given? && validate_before_action(block)
elsif block_given? && validate_before_action(block, 'notify')
hooks << block
end

hash[:before_notify] = hooks
end

def before_event(action = nil, &block)
hooks = Array(get(:before_event)).dup

if action && validate_before_action(action, 'event')
hooks << action
elsif block_given? && validate_before_action(block, 'event')
hooks << block
end

hash[:before_event] = hooks
end

def backtrace_filter(&block)
if block_given?
logger.warn('DEPRECATED: backtrace_filter is deprecated. Please use before_notify instead. See https://docs.honeybadger.io/ruby/support/v4-upgrade#backtrace_filter')
Expand Down Expand Up @@ -127,17 +139,17 @@ def exception_fingerprint(&block)

private

def validate_before_action(action)
def validate_before_action(action, type)
if !action.respond_to?(:call)
logger.warn(
'You attempted to add a before notify hook that does not respond ' \
"You attempted to add a before #{type} hook that does not respond " \
'to #call. We are discarding this hook so your intended behavior ' \
'will not occur.'
)
false
elsif action.arity != 1
logger.warn(
'You attempted to add a before notify hook that has an arity ' \
"You attempted to add a before #{type} hook that has an arity " \
'other than one. We are discarding this hook so your intended ' \
'behavior will not occur.'
)
Expand Down
56 changes: 56 additions & 0 deletions lib/honeybadger/event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'forwardable'

module Honeybadger
class Event
extend Forwardable

# The timestamp of the event
attr_reader :ts

# The event_type of the event
attr_reader :event_type

# The payload data of the event
attr_reader :payload

def_delegator :payload, :[]

# @api private
def initialize(event_type_or_payload, payload={})
if event_type_or_payload.is_a?(String)
@event_type = event_type_or_payload
@payload = payload
elsif event_type_or_payload.is_a?(Hash)
@event_type = event_type_or_payload[:event_type] || event_type_or_payload["event_type"]
@payload = event_type_or_payload
end

@ts = payload[:ts] || Time.now.utc.strftime("%FT%T.%LZ")
@halted = false
end

# Halts the event and the before_event callback chain.
#
# Returns nothing.
def halt!
@halted ||= true
end

# @api private
# Determines if this event will be discarded.
def halted?
!!@halted
end

# @api private
# Template used to create JSON payload.
#
# @return [Hash] JSON representation of the event.
def as_json(*args)
payload.tap do |p|
p[:ts] = ts
p[:event_type] = event_type if event_type
end
end
end
end
13 changes: 12 additions & 1 deletion spec/unit/honeybadger/agent_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,18 @@
end
end

describe "ignoring events" do
describe "ignoring events using before_event callback" do
let(:halt_hook) { ->(event) { event.halt! } }

before { subject.configure { |config| config.before_event(halt_hook) } }
after { subject.event(event_type: "event_type", some_data: "is here") }

it "does not push an event" do
expect(events_worker).not_to receive(:push)
end
end

describe "ignoring events using events.ignore config" do
let(:config) { Honeybadger::Config.new(api_key:'fake api key', logger: NULL_LOGGER, backend: :debug, :'events.ignore' => ignored_events) }

after { subject.event(event_type: event_type, some_data: "is here") }
Expand Down
56 changes: 56 additions & 0 deletions spec/unit/honeybadger/event_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'honeybadger/event'
require 'timecop'

describe Honeybadger::Event do
let(:event_type) { "event_type" }
let(:payload) { {} }

subject { described_class.new(event_type, payload) }

describe "initialize" do
context "event_type is passed as string" do
let(:event_type) { "action" }
let(:payload) { {} }

its(:event_type) { should eq "action" }
its(:payload) { should eq payload }
end

context "event_type is passed as part of payload" do
subject { described_class.new(event_type) }

let(:event_type) { { event_type: "action" } }

its(:event_type) { should eq "action" }
its(:payload) { should eq({ event_type: "action" }) }
end
end

describe "ts" do
before { Timecop.freeze }
after { Timecop.unfreeze }

its(:ts) { should eq Time.now.utc.strftime("%FT%T.%LZ") }
end

describe "halted" do
context "halt! is not called" do
its(:halted?) { should be false }
end

context "halt! is called" do
before { subject.halt! }
its(:halted?) { should be true }
end
end

describe "as_json" do
let(:event_type) { "action" }
let(:payload) { { data1: 1 } }

before { Timecop.freeze }
after { Timecop.unfreeze }

its(:as_json) { should eq({ event_type: "action", ts: Time.now.utc.strftime("%FT%T.%LZ"), data1: 1 }) }
end
end

0 comments on commit 2f86728

Please sign in to comment.