appsignal 4.2.0 → 4.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +443 -0
  3. data/README.md +0 -3
  4. data/Rakefile +1 -1
  5. data/appsignal.gemspec +2 -1
  6. data/build_matrix.yml +33 -0
  7. data/ext/agent.rb +27 -27
  8. data/ext/appsignal_extension.c +90 -73
  9. data/ext/base.rb +3 -1
  10. data/lib/appsignal/check_in/event.rb +55 -0
  11. data/lib/appsignal/check_in/scheduler.rb +8 -2
  12. data/lib/appsignal/check_in.rb +9 -8
  13. data/lib/appsignal/config.rb +36 -15
  14. data/lib/appsignal/custom_marker.rb +72 -0
  15. data/lib/appsignal/environment.rb +1 -0
  16. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +4 -6
  17. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +4 -6
  18. data/lib/appsignal/helpers/instrumentation.rb +5 -0
  19. data/lib/appsignal/hooks/active_job.rb +25 -5
  20. data/lib/appsignal/hooks/at_exit.rb +18 -4
  21. data/lib/appsignal/hooks/ownership.rb +44 -0
  22. data/lib/appsignal/hooks.rb +1 -0
  23. data/lib/appsignal/integrations/capistrano/appsignal.cap +4 -8
  24. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +8 -11
  25. data/lib/appsignal/integrations/http.rb +2 -1
  26. data/lib/appsignal/integrations/ownership.rb +51 -0
  27. data/lib/appsignal/integrations/rake.rb +14 -2
  28. data/lib/appsignal/integrations/sidekiq.rb +14 -3
  29. data/lib/appsignal/internal_errors.rb +19 -0
  30. data/lib/appsignal/logger.rb +121 -69
  31. data/lib/appsignal/marker.rb +1 -1
  32. data/lib/appsignal/probes/sidekiq.rb +5 -1
  33. data/lib/appsignal/rack/body_wrapper.rb +1 -1
  34. data/lib/appsignal/rack/event_handler.rb +7 -5
  35. data/lib/appsignal/transaction.rb +91 -13
  36. data/lib/appsignal/version.rb +1 -1
  37. data/lib/appsignal.rb +82 -11
  38. data/resources/cacert.pem +164 -87
  39. metadata +8 -4
@@ -58,6 +58,12 @@ module Appsignal
58
58
  error_message failed_at jid retried_at retry wrapped
59
59
  ].freeze
60
60
 
61
+ def self.sidekiq8?
62
+ return false unless ::Sidekiq.respond_to?(:gem_version)
63
+
64
+ @sidekiq8 ||= ::Sidekiq.gem_version >= Gem::Version.new("8.0.0")
65
+ end
66
+
61
67
  def call(_worker, item, _queue, &block)
62
68
  job_status = nil
63
69
  transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
@@ -74,7 +80,14 @@ module Appsignal
74
80
  ensure
75
81
  if transaction
76
82
  transaction.add_params_if_nil { parse_arguments(item) }
77
- queue_start = (item["enqueued_at"].to_f * 1000.0).to_i # Convert seconds to milliseconds
83
+ enqueued_at = item["enqueued_at"]
84
+ queue_start =
85
+ if self.class.sidekiq8?
86
+ enqueued_at.to_i # Sidekiq 8 stores it as epoc milliseconds
87
+ else
88
+ # Convert seconds to milliseconds for Sidekiq 7 and older
89
+ (enqueued_at.to_f * 1000.0).to_i
90
+ end
78
91
  transaction.set_queue_start(queue_start)
79
92
  transaction.add_tags(:request_id => item["jid"])
80
93
  Appsignal::Transaction.complete_current! unless exception
@@ -143,8 +156,6 @@ module Appsignal
143
156
  safe_load(args[0], args) do |_, _, arg|
144
157
  arg
145
158
  end
146
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
147
- nil # Set in the ActiveJob integration
148
159
  else
149
160
  # Sidekiq Enterprise argument encryption.
150
161
  # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ # @api private
5
+ class InternalError < StandardError; end
6
+
7
+ # @api private
8
+ class NotStartedError < InternalError
9
+ MESSAGE = <<~MESSAGE
10
+ The AppSignal Ruby gem was not started!
11
+
12
+ This error was raised by calling `Appsignal.check_if_started!`
13
+ MESSAGE
14
+
15
+ def message
16
+ MESSAGE
17
+ end
18
+ end
19
+ end
@@ -9,6 +9,41 @@ module Appsignal
9
9
  # @see https://docs.appsignal.com/logging/platforms/integrations/ruby.html
10
10
  # AppSignal Ruby logging documentation.
11
11
  class Logger < ::Logger
12
+ # A wrapper for a block that ensures it is only called once.
13
+ # If called again, it will return the result of the first call.
14
+ # This is useful for ensuring that a block is not executed multiple
15
+ # times when it is broadcasted to multiple loggers.
16
+ class BlockOnce
17
+ def initialize(&block)
18
+ @block = block
19
+ @called = false
20
+ @success = nil
21
+ @result = nil
22
+ @error = nil
23
+ end
24
+
25
+ def call(*args, **kwargs)
26
+ if @called
27
+ return @result if @success
28
+
29
+ raise @error
30
+ end
31
+
32
+ @called = true
33
+ @result = @block.call(*args, **kwargs)
34
+ @success = true
35
+ @result
36
+ rescue StandardError => e
37
+ @success = false
38
+ @error = e
39
+ raise @error
40
+ end
41
+
42
+ def to_proc
43
+ method(:call).to_proc
44
+ end
45
+ end
46
+
12
47
  PLAINTEXT = 0
13
48
  LOGFMT = 1
14
49
  JSON = 2
@@ -34,45 +69,75 @@ module Appsignal
34
69
 
35
70
  @group = group
36
71
  @level = level
72
+ @silenced = false
37
73
  @format = format
38
74
  @mutex = Mutex.new
39
75
  @default_attributes = attributes
76
+ @appsignal_attributes = attributes
77
+ @loggers = []
78
+ end
79
+
80
+ # When a formatter is set on the logger (e.g. when wrapping the logger in
81
+ # `ActiveSupport::TaggedLogging`) we want to set that formatter on all the
82
+ # loggers that are being broadcasted to.
83
+ def formatter=(formatter)
84
+ super
85
+ @loggers.each { |logger| logger.formatter = formatter }
40
86
  end
41
87
 
42
88
  # We support the various methods in the Ruby
43
89
  # logger class by supplying this method.
44
90
  # @api private
45
- def add(severity, message = nil, group = nil)
91
+ def add(severity, message = nil, group = nil, &block) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
92
+ # If we do not need to broadcast to any loggers and the severity is
93
+ # below the log level, we can return early.
46
94
  severity ||= UNKNOWN
47
- return true if severity < level
95
+ return true if severity < level && @loggers.empty?
96
+
97
+ # If the logger is silenced, we do not log *or broadcast* messages
98
+ # below the log level.
99
+ return true if @silenced && severity < @level
48
100
 
101
+ # Ensure that the block is only run once, even if several loggers
102
+ # are being broadcasted to.
103
+ block = BlockOnce.new(&block) unless block.nil?
104
+
105
+ # If the group is not set, we use the default group.
49
106
  group = @group if group.nil?
50
- if message.nil?
51
- if block_given?
52
- message = yield
53
- else
54
- message = group
55
- group = @group
56
- end
107
+
108
+ did_not_log = true
109
+
110
+ @loggers.each do |logger|
111
+ # Loggers should return true if they did *not* log the message.
112
+ # If any of the broadcasted loggers logs the message, that counts
113
+ # as having logged the message.
114
+ did_not_log &&= logger.add(severity, message, group, &block)
115
+ rescue
116
+ nil
57
117
  end
58
- return if message.nil?
59
118
 
60
- message = formatter.call(severity, Time.now, group, message) if formatter
119
+ # If the severity is below the log level, we do not log the message.
120
+ return did_not_log if severity < level
61
121
 
62
- unless message.is_a?(String)
63
- Appsignal.internal_logger.warn(
64
- "Logger message was ignored, because it was not a String: #{message.inspect}"
65
- )
66
- return
122
+ message = block.call if block && message.nil?
123
+
124
+ return if message.nil?
125
+
126
+ if message.is_a?(Exception)
127
+ message = "#{message.class}: #{message.message} (#{message.backtrace[0]})"
67
128
  end
68
129
 
130
+ message = formatter.call(severity, Time.now, group, message) if formatter
131
+
69
132
  Appsignal::Extension.log(
70
133
  group,
71
134
  SEVERITY_MAP.fetch(severity, 0),
72
135
  @format,
73
- message,
136
+ message.to_s,
74
137
  Appsignal::Utils::Data.generate(appsignal_attributes)
75
138
  )
139
+
140
+ false
76
141
  end
77
142
  alias log add
78
143
 
@@ -80,96 +145,83 @@ module Appsignal
80
145
  # @param message Message to log
81
146
  # @param attributes Attributes to tag the log with
82
147
  # @return [void]
83
- def debug(message = nil, attributes = {})
84
- return if level > DEBUG
85
-
86
- message = yield if message.nil? && block_given?
87
- return if message.nil?
88
-
89
- add_with_attributes(DEBUG, message, @group, attributes)
148
+ def debug(message = nil, attributes = {}, &block)
149
+ add_with_attributes(DEBUG, message, @group, attributes, &block)
90
150
  end
91
151
 
92
152
  # Log an info level message
93
153
  # @param message Message to log
94
154
  # @param attributes Attributes to tag the log with
95
155
  # @return [void]
96
- def info(message = nil, attributes = {})
97
- return if level > INFO
98
-
99
- message = yield if message.nil? && block_given?
100
- return if message.nil?
101
-
102
- add_with_attributes(INFO, message, @group, attributes)
156
+ def info(message = nil, attributes = {}, &block)
157
+ add_with_attributes(INFO, message, @group, attributes, &block)
103
158
  end
104
159
 
105
160
  # Log a warn level message
106
161
  # @param message Message to log
107
162
  # @param attributes Attributes to tag the log with
108
163
  # @return [void]
109
- def warn(message = nil, attributes = {})
110
- return if level > WARN
111
-
112
- message = yield if message.nil? && block_given?
113
- return if message.nil?
114
-
115
- add_with_attributes(WARN, message, @group, attributes)
164
+ def warn(message = nil, attributes = {}, &block)
165
+ add_with_attributes(WARN, message, @group, attributes, &block)
116
166
  end
117
167
 
118
168
  # Log an error level message
119
169
  # @param message Message to log
120
170
  # @param attributes Attributes to tag the log with
121
171
  # @return [void]
122
- def error(message = nil, attributes = {})
123
- return if level > ERROR
124
-
125
- message = yield if message.nil? && block_given?
126
- return if message.nil?
127
-
128
- message = "#{message.class}: #{message.message}" if message.is_a?(Exception)
129
-
130
- add_with_attributes(ERROR, message, @group, attributes)
172
+ def error(message = nil, attributes = {}, &block)
173
+ add_with_attributes(ERROR, message, @group, attributes, &block)
131
174
  end
132
175
 
133
176
  # Log a fatal level message
134
177
  # @param message Message to log
135
178
  # @param attributes Attributes to tag the log with
136
179
  # @return [void]
137
- def fatal(message = nil, attributes = {})
138
- return if level > FATAL
139
-
140
- message = yield if message.nil? && block_given?
141
- return if message.nil?
180
+ def fatal(message = nil, attributes = {}, &block)
181
+ add_with_attributes(FATAL, message, @group, attributes, &block)
182
+ end
142
183
 
143
- add_with_attributes(FATAL, message, @group, attributes)
184
+ # Log an info level message
185
+ #
186
+ # Returns the number of characters written.
187
+ #
188
+ # @param message Message to log
189
+ # @return [Integer]
190
+ def <<(message)
191
+ info(message)
192
+ message.length
144
193
  end
145
194
 
146
195
  # When using ActiveSupport::TaggedLogging without the broadcast feature,
147
196
  # the passed logger is required to respond to the `silence` method.
148
- # In our case it behaves as the broadcast feature of the Rails logger, but
149
- # we don't have to check if the parent logger has the `silence` method defined
150
- # as our logger directly inherits from Ruby base logger.
151
197
  #
152
- # Links:
198
+ # Reference links:
153
199
  #
154
200
  # - https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger.rb#L60-L76
155
- # - https://github.com/rails/rails/blob/main/activesupport/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/active_support/logger_silence.rb
156
- def silence(_severity = ERROR, &block)
157
- block.call
201
+ # - https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger_silence.rb
202
+ def silence(severity = ERROR, &block)
203
+ previous_level = @level
204
+ @level = severity
205
+ @silenced = true
206
+ block.call(self)
207
+ ensure
208
+ @level = previous_level
209
+ @silenced = false
210
+ end
211
+
212
+ def broadcast_to(logger)
213
+ @loggers << logger
158
214
  end
159
215
 
160
216
  private
161
217
 
162
- attr_reader :default_attributes
218
+ attr_reader :default_attributes, :appsignal_attributes
163
219
 
164
- def add_with_attributes(severity, message, group, attributes)
165
- Thread.current[:appsignal_logger_attributes] = default_attributes.merge(attributes)
166
- add(severity, message, group)
220
+ def add_with_attributes(severity, message, group, attributes, &block)
221
+ @appsignal_attributes = default_attributes.merge(attributes)
222
+ add(severity, message, group, &block)
167
223
  ensure
168
- Thread.current[:appsignal_logger_attributes] = nil
169
- end
170
-
171
- def appsignal_attributes
172
- Thread.current.fetch(:appsignal_logger_attributes, {})
224
+ @appsignal_attributes = default_attributes
173
225
  end
174
226
  end
175
227
  end
@@ -49,7 +49,7 @@ module Appsignal
49
49
  # @return [void]
50
50
  def transmit
51
51
  transmitter = Transmitter.new(ACTION, config)
52
- puts "Notifying AppSignal of deploy with: " \
52
+ puts "Notifying AppSignal of '#{config.env}' deploy with " \
53
53
  "revision: #{marker_data[:revision]}, user: #{marker_data[:user]}"
54
54
 
55
55
  response = transmitter.transmit(marker_data)
@@ -59,7 +59,11 @@ module Appsignal
59
59
  is_sidekiq7 = self.class.sidekiq7_and_greater?
60
60
  @adapter = is_sidekiq7 ? Sidekiq7Adapter : Sidekiq6Adapter
61
61
 
62
- config_string = " with config: #{config}" unless config.empty?
62
+ unless config.empty?
63
+ formatted_config =
64
+ config.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
65
+ config_string = " with config: #{formatted_config}"
66
+ end
63
67
  Appsignal.internal_logger.debug("Initializing Sidekiq probe#{config_string}")
64
68
  require "sidekiq/api"
65
69
  end
@@ -4,7 +4,7 @@ module Appsignal
4
4
  module Rack
5
5
  # @api private
6
6
  class BodyWrapper
7
- IGNORED_ERRORS = [Errno::EPIPE].freeze
7
+ IGNORED_ERRORS = [Errno::EPIPE, Errno::ECONNRESET].freeze
8
8
 
9
9
  def self.wrap(original_body, appsignal_transaction)
10
10
  # The logic of how Rack treats a response body differs based on which methods
@@ -59,6 +59,7 @@ module Appsignal
59
59
  return unless request_handler?(request.env[APPSIGNAL_EVENT_HANDLER_ID])
60
60
 
61
61
  transaction = Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
62
+ transaction.start_event
62
63
  request.env[APPSIGNAL_TRANSACTION] = transaction
63
64
 
64
65
  request.env[RACK_AFTER_REPLY] ||= []
@@ -67,7 +68,7 @@ module Appsignal
67
68
 
68
69
  Appsignal::Rack::EventHandler
69
70
  .safe_execution("Appsignal::Rack::EventHandler's after_reply") do
70
- transaction.finish_event("process_request.rack", "", "")
71
+ transaction.finish_event("process_request.rack", "callback: after_reply", "")
71
72
  queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
72
73
  transaction.set_queue_start(queue_start) if queue_start
73
74
  end
@@ -81,9 +82,9 @@ module Appsignal
81
82
  # just a fallback if that doesn't get called.
82
83
  #
83
84
  # One such scenario is when a Puma "lowlevel_error" occurs.
84
- Appsignal::Transaction.complete_current!
85
+ transaction.complete
86
+ Appsignal::Transaction.clear_current_transaction!
85
87
  end
86
- transaction.start_event
87
88
  end
88
89
  end
89
90
 
@@ -111,7 +112,7 @@ module Appsignal
111
112
  return unless transaction
112
113
 
113
114
  self.class.safe_execution("Appsignal::Rack::EventHandler#on_finish") do
114
- transaction.finish_event("process_request.rack", "", "")
115
+ transaction.finish_event("process_request.rack", "callback: on_finish", "")
115
116
  transaction.add_params_if_nil { request.params }
116
117
  transaction.add_headers_if_nil { request.env }
117
118
  transaction.add_session_data_if_nil do
@@ -138,7 +139,8 @@ module Appsignal
138
139
 
139
140
  # Make sure the current transaction is always closed when the request
140
141
  # is finished
141
- Appsignal::Transaction.complete_current!
142
+ transaction.complete
143
+ Appsignal::Transaction.clear_current_transaction!
142
144
  end
143
145
 
144
146
  private
@@ -27,23 +27,62 @@ module Appsignal
27
27
  # @param namespace [String] Namespace of the to be created transaction.
28
28
  # @return [Transaction]
29
29
  def create(namespace)
30
- # Check if we already have a running transaction
30
+ # Reset the transaction if it was already completed but not cleared
31
+ if Thread.current[:appsignal_transaction]&.completed?
32
+ Thread.current[:appsignal_transaction] = nil
33
+ end
34
+
31
35
  if Thread.current[:appsignal_transaction].nil?
32
36
  # If not, start a new transaction
33
37
  set_current_transaction(Appsignal::Transaction.new(namespace))
34
38
  else
39
+ transaction = current
35
40
  # Otherwise, log the issue about trying to start another transaction
36
41
  Appsignal.internal_logger.warn(
37
42
  "Trying to start new transaction, but a transaction " \
38
- "with id '#{current.transaction_id}' is already running. " \
39
- "Using transaction '#{current.transaction_id}'."
43
+ "with id '#{transaction.transaction_id}' is already running. " \
44
+ "Using transaction '#{transaction.transaction_id}'."
40
45
  )
41
46
 
42
47
  # And return the current transaction instead
43
- current
48
+ transaction
44
49
  end
45
50
  end
46
51
 
52
+ # @api private
53
+ # @return [Array<Proc>]
54
+ # Add a block, if given, to be executed after a transaction is created.
55
+ # The block will be called with the transaction as an argument.
56
+ # Returns the array of blocks that will be executed after a transaction
57
+ # is created.
58
+ def after_create(&block)
59
+ @after_create ||= Set.new
60
+
61
+ return @after_create if block.nil?
62
+
63
+ @after_create << block
64
+ end
65
+
66
+ # @api private
67
+ # @return [Array<Proc>]
68
+ # Add a block, if given, to be executed before a transaction is completed.
69
+ # This happens after duplicating the transaction for each error that was
70
+ # reported in the transaction -- that is, when a transaction with
71
+ # several errors is completed, the block will be called once for each
72
+ # error, with the transaction (either the original one or a duplicate of it)
73
+ # that has each of the errors set.
74
+ # The block will be called with the transaction as the first argument,
75
+ # and the error reported by the transaction, if any, as the second argument.
76
+ # Returns the array of blocks that will be executed before a transaction is
77
+ # completed.
78
+ def before_complete(&block)
79
+ @before_complete ||= Set.new
80
+
81
+ return @before_complete if block.nil?
82
+
83
+ @before_complete << block
84
+ end
85
+
47
86
  # @api private
48
87
  def set_current_transaction(transaction)
49
88
  Thread.current[:appsignal_transaction] = transaction
@@ -120,6 +159,7 @@ module Appsignal
120
159
  @namespace = namespace
121
160
  @paused = false
122
161
  @discarded = false
162
+ @completed = false
123
163
  @tags = {}
124
164
  @breadcrumbs = []
125
165
  @store = Hash.new { |hash, key| hash[key] = {} }
@@ -137,6 +177,8 @@ module Appsignal
137
177
  @namespace,
138
178
  0
139
179
  ) || Appsignal::Extension::MockTransaction.new
180
+
181
+ run_after_create_hooks
140
182
  end
141
183
 
142
184
  # @api private
@@ -149,6 +191,11 @@ module Appsignal
149
191
  false
150
192
  end
151
193
 
194
+ # @api private
195
+ def completed?
196
+ @completed
197
+ end
198
+
152
199
  # @api private
153
200
  def complete
154
201
  if discarded?
@@ -177,7 +224,7 @@ module Appsignal
177
224
  # In the duplicate transaction for each error, set an error
178
225
  # with a block that calls all the blocks set for that error
179
226
  # in the original transaction.
180
- transaction.set_error(error) do
227
+ transaction.internal_set_error(error) do
181
228
  blocks.each { |block| block.call(transaction) }
182
229
  end
183
230
 
@@ -192,7 +239,12 @@ module Appsignal
192
239
  end
193
240
  end
194
241
  end
242
+
243
+ run_before_complete_hooks
244
+
195
245
  sample_data if should_sample
246
+
247
+ @completed = true
196
248
  @ext.complete
197
249
  end
198
250
 
@@ -520,17 +572,18 @@ module Appsignal
520
572
  return unless error
521
573
  return unless Appsignal.active?
522
574
 
523
- _set_error(error) if @error_blocks.empty?
524
-
525
- if !@error_blocks.include?(error) && @error_blocks.length >= ERRORS_LIMIT
526
- Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
527
- "than #{ERRORS_LIMIT} distinct errors. Only the first " \
528
- "#{ERRORS_LIMIT} distinct errors will be reported."
575
+ if error.instance_variable_get(:@__appsignal_error_reported) && !@error_blocks.include?(error)
529
576
  return
530
577
  end
531
578
 
532
- @error_blocks[error] << block
533
- @error_blocks[error].compact!
579
+ internal_set_error(error, &block)
580
+
581
+ # Mark errors and their causes as tracked so we don't report duplicates,
582
+ # but also not error causes if the wrapper error is already reported.
583
+ while error
584
+ error.instance_variable_set(:@__appsignal_error_reported, true) unless error.frozen?
585
+ error = error.cause
586
+ end
534
587
  end
535
588
  alias :set_error :add_error
536
589
  alias_method :add_exception, :add_error
@@ -592,10 +645,35 @@ module Appsignal
592
645
  attr_writer :is_duplicate, :tags, :custom_data, :breadcrumbs, :params,
593
646
  :session_data, :headers
594
647
 
648
+ def internal_set_error(error, &block)
649
+ _set_error(error) if @error_blocks.empty?
650
+
651
+ if !@error_blocks.include?(error) && @error_blocks.length >= ERRORS_LIMIT
652
+ Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
653
+ "than #{ERRORS_LIMIT} distinct errors. Only the first " \
654
+ "#{ERRORS_LIMIT} distinct errors will be reported."
655
+ return
656
+ end
657
+ @error_blocks[error] << block
658
+ @error_blocks[error].compact!
659
+ end
660
+
595
661
  private
596
662
 
597
663
  attr_reader :breadcrumbs
598
664
 
665
+ def run_after_create_hooks
666
+ self.class.after_create.each do |block|
667
+ block.call(self)
668
+ end
669
+ end
670
+
671
+ def run_before_complete_hooks
672
+ self.class.before_complete.each do |block|
673
+ block.call(self, @error_set)
674
+ end
675
+ end
676
+
599
677
  def _set_error(error)
600
678
  backtrace = cleaned_backtrace(error.backtrace)
601
679
  @ext.set_error(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.2.0"
4
+ VERSION = "4.5.17"
5
5
  end