Skip to content

Commit

Permalink
Add before_complete transaction hook
Browse files Browse the repository at this point in the history
Add a `before_complete` transaction hook, which is executed right
before a transaction is completed. Note that, for transactions
containing multiple errors, this hook is called once for each of
the duplicate transactions that is used to report each of the
errors. This allows the context to be customised for each error.
  • Loading branch information
unflxw committed Jan 9, 2025
1 parent 72c9474 commit 56cdb61
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 1 deletion.
33 changes: 32 additions & 1 deletion lib/appsignal/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Transaction

class << self
@after_create = []
@before_complete = []

# Create a new transaction and set it as the currently active
# transaction.
Expand Down Expand Up @@ -50,7 +51,8 @@ def create(namespace)
# @return [Array<Proc>]
# Add a block, if given, to be executed after a transaction is created.
# The block will be called with the transaction as an argument.
# Returns the array of blocks that will be executed.
# Returns the array of blocks that will be executed after a transaction
# is created.
def after_create(&block)
@after_create ||= []

Expand All @@ -59,6 +61,26 @@ def after_create(&block)
@after_create << block
end

# @api private
# @return [Array<Proc>]
# Add a block, if given, to be executed before a transaction is completed.
# This happens after duplicating the transaction for each error that was
# reported in the transaction -- that is, when a transaction with
# several errors is completed, the block will be called once for each
# error, with the transaction (either the original one or a duplicate of it)
# that has each of the errors set.
# The block will be called with the transaction as the first argument,
# and the error reported by the transaction, if any, as the second argument.
# Returns the array of blocks that will be executed before a transaction is
# completed.
def before_complete(&block)
@before_complete ||= []

return @before_complete if block.nil?

@before_complete << block
end

# @api private
def set_current_transaction(transaction)
Thread.current[:appsignal_transaction] = transaction
Expand Down Expand Up @@ -210,6 +232,9 @@ def complete
end
end
sample_data if should_sample

before_complete

@ext.complete
end

Expand Down Expand Up @@ -619,6 +644,12 @@ def after_create
end
end

def before_complete
self.class.before_complete.each do |block|
block.call(self, @error_set)
end
end

def _set_error(error)
backtrace = cleaned_backtrace(error.backtrace)
@ext.set_error(
Expand Down
119 changes: 119 additions & 0 deletions spec/lib/appsignal/transaction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,125 @@ def to_s
end
end

describe "#before_complete" do
after do
Appsignal::Transaction.before_complete.clear
end

it "stores the given hooks" do
expect(Appsignal::Transaction.before_complete).to be_empty
Appsignal::Transaction.before_complete do |transaction, error|
transaction.set_action(error.message)
end

expect(Appsignal::Transaction.before_complete).to_not be_empty

transaction = new_transaction
error = ExampleStandardError.new("hook_error")

expect(transaction).to_not have_action("hook_error")
Appsignal::Transaction.before_complete.first.call(transaction, error)
expect(transaction).to have_action("hook_error")
end

context "when the transaction has an error" do
it "calls the given hook with the error when a transaction is completed" do
block = proc do |transaction, error|
transaction.set_action(error.message)
end

Appsignal::Transaction.before_complete(&block)

transaction = new_transaction
error = ExampleStandardError.new("hook_error")
transaction.set_error(error)

expect(block).to(
receive(:call)
.with(transaction, error)
.and_call_original
)

transaction.complete

expect(transaction).to have_action("hook_error")
end
end

context "when the transaction has several errors" do
it "calls the given hook for each of the duplicate error transactions" do
# Store the pairs of errors and transactions seen, since the duplicate
# transaction cannot otherwise be referred to in the test.
seen = []

block = proc do |transaction, error|
seen << [transaction, error]
transaction.set_action(error.message)
end

Appsignal::Transaction.before_complete(&block)

transaction = new_transaction
first_error = ExampleStandardError.new("hook_error_first")
transaction.set_error(first_error)

second_error = ExampleStandardError.new("hook_error_second")
transaction.set_error(second_error)

transaction.complete

expect(seen).to include([transaction, first_error])
expect(transaction).to have_action("hook_error_first")

transaction = seen.find { |t, _e| t != transaction }.first
expect(seen).to include([transaction, second_error])
expect(transaction).to have_action("hook_error_second")
end
end

context "when the transaction does not have an error" do
it "calls the given hook with nil when a transaction is completed" do
block = proc do |transaction|
transaction.set_action("hook_action")
end

Appsignal::Transaction.before_complete(&block)

transaction = new_transaction

expect(block).to(
receive(:call)
.with(transaction, nil)
.and_call_original
)

transaction.complete

expect(transaction).to have_action("hook_action")
end
end

it "calls all the hooks in order" do
Appsignal::Transaction.before_complete do |transaction, error|
transaction.set_namespace(error.message)
transaction.set_action("hook_action_1")
end

Appsignal::Transaction.before_complete do |transaction, error|
transaction.set_action(error.message)
end

transaction = new_transaction
error = ExampleStandardError.new("hook_error")
transaction.set_error(error)

transaction.complete

expect(transaction).to have_namespace("hook_error")
expect(transaction).to have_action("hook_error")
end
end

describe "#start_event" do
let(:transaction) { new_transaction }

Expand Down

0 comments on commit 56cdb61

Please sign in to comment.