appsignal 3.13.1 → 4.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +499 -487
  3. data/CHANGELOG.md +104 -7
  4. data/Rakefile +31 -7
  5. data/benchmark.rake +4 -6
  6. data/build_matrix.yml +45 -39
  7. data/ext/agent.rb +27 -27
  8. data/ext/appsignal_extension.c +25 -0
  9. data/gemfiles/rails-7.2.gemfile +11 -0
  10. data/lib/appsignal/check_in/cron.rb +2 -15
  11. data/lib/appsignal/cli/diagnose.rb +37 -28
  12. data/lib/appsignal/cli/install.rb +5 -1
  13. data/lib/appsignal/config.rb +57 -119
  14. data/lib/appsignal/demo.rb +2 -2
  15. data/lib/appsignal/extension/jruby.rb +14 -0
  16. data/lib/appsignal/helpers/instrumentation.rb +139 -417
  17. data/lib/appsignal/helpers/metrics.rb +0 -16
  18. data/lib/appsignal/hooks/action_cable.rb +8 -8
  19. data/lib/appsignal/hooks/active_job.rb +2 -2
  20. data/lib/appsignal/hooks/at_exit.rb +37 -0
  21. data/lib/appsignal/hooks.rb +1 -16
  22. data/lib/appsignal/integrations/action_cable.rb +2 -2
  23. data/lib/appsignal/integrations/capistrano/appsignal.cap +2 -4
  24. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +1 -4
  25. data/lib/appsignal/integrations/delayed_job_plugin.rb +3 -3
  26. data/lib/appsignal/integrations/que.rb +2 -2
  27. data/lib/appsignal/integrations/railtie.rb +26 -59
  28. data/lib/appsignal/integrations/rake.rb +2 -2
  29. data/lib/appsignal/integrations/resque.rb +2 -2
  30. data/lib/appsignal/integrations/shoryuken.rb +4 -4
  31. data/lib/appsignal/integrations/sidekiq.rb +3 -3
  32. data/lib/appsignal/integrations/webmachine.rb +2 -2
  33. data/lib/appsignal/loaders.rb +1 -1
  34. data/lib/appsignal/probes.rb +0 -9
  35. data/lib/appsignal/rack/abstract_middleware.rb +4 -26
  36. data/lib/appsignal/rack/body_wrapper.rb +0 -12
  37. data/lib/appsignal/rack/event_handler.rb +4 -4
  38. data/lib/appsignal/rack/rails_instrumentation.rb +1 -1
  39. data/lib/appsignal/rack.rb +0 -25
  40. data/lib/appsignal/sample_data.rb +95 -0
  41. data/lib/appsignal/transaction.rb +235 -361
  42. data/lib/appsignal/utils/rails_helper.rb +4 -0
  43. data/lib/appsignal/version.rb +1 -1
  44. data/lib/appsignal.rb +19 -71
  45. data/spec/lib/appsignal/auth_check_spec.rb +1 -1
  46. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  47. data/spec/lib/appsignal/capistrano3_spec.rb +53 -13
  48. data/spec/lib/appsignal/check_in_spec.rb +1 -207
  49. data/spec/lib/appsignal/cli/demo_spec.rb +7 -27
  50. data/spec/lib/appsignal/cli/diagnose_spec.rb +145 -110
  51. data/spec/lib/appsignal/config_spec.rb +304 -379
  52. data/spec/lib/appsignal/extension_install_failure_spec.rb +5 -1
  53. data/spec/lib/appsignal/extension_spec.rb +5 -1
  54. data/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb +1 -1
  55. data/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb +1 -2
  56. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +1 -0
  57. data/spec/lib/appsignal/hooks/activejob_spec.rb +7 -12
  58. data/spec/lib/appsignal/hooks/at_exit_spec.rb +72 -0
  59. data/spec/lib/appsignal/hooks/gvl_spec.rb +10 -5
  60. data/spec/lib/appsignal/hooks/http_spec.rb +3 -3
  61. data/spec/lib/appsignal/hooks/net_http_spec.rb +3 -3
  62. data/spec/lib/appsignal/hooks/rake_spec.rb +6 -9
  63. data/spec/lib/appsignal/hooks/redis_client_spec.rb +5 -10
  64. data/spec/lib/appsignal/hooks/redis_spec.rb +4 -7
  65. data/spec/lib/appsignal/hooks/resque_spec.rb +3 -5
  66. data/spec/lib/appsignal/hooks_spec.rb +0 -41
  67. data/spec/lib/appsignal/integrations/data_mapper_spec.rb +29 -20
  68. data/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb +4 -9
  69. data/spec/lib/appsignal/integrations/railtie_spec.rb +179 -157
  70. data/spec/lib/appsignal/integrations/shoryuken_spec.rb +3 -5
  71. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +48 -62
  72. data/spec/lib/appsignal/loaders/hanami_spec.rb +6 -9
  73. data/spec/lib/appsignal/loaders/padrino_spec.rb +6 -10
  74. data/spec/lib/appsignal/loaders/sinatra_spec.rb +6 -9
  75. data/spec/lib/appsignal/loaders_spec.rb +8 -1
  76. data/spec/lib/appsignal/marker_spec.rb +1 -1
  77. data/spec/lib/appsignal/probes_spec.rb +4 -83
  78. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +4 -63
  79. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +0 -48
  80. data/spec/lib/appsignal/rack/event_handler_spec.rb +18 -15
  81. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +3 -11
  82. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +4 -5
  83. data/spec/lib/appsignal/sample_data_spec.rb +174 -0
  84. data/spec/lib/appsignal/transaction_spec.rb +791 -1031
  85. data/spec/lib/appsignal/transmitter_spec.rb +6 -8
  86. data/spec/lib/appsignal_spec.rb +294 -643
  87. data/spec/spec_helper.rb +1 -3
  88. data/spec/support/fixtures/projects/valid/config/appsignal.yml +4 -7
  89. data/spec/support/fixtures/projects/valid_with_rails_app/config/application.rb +16 -0
  90. data/spec/support/fixtures/projects/valid_with_rails_app/config/appsignal.yml +56 -0
  91. data/spec/support/fixtures/projects/valid_with_rails_app/config/environment.rb +5 -0
  92. data/spec/support/helpers/api_request_helper.rb +3 -2
  93. data/spec/support/helpers/config_helpers.rb +41 -11
  94. data/spec/support/helpers/dependency_helper.rb +8 -0
  95. data/spec/support/helpers/log_helpers.rb +1 -0
  96. data/spec/support/helpers/rails_helper.rb +6 -6
  97. data/spec/support/helpers/transaction_helpers.rb +2 -24
  98. data/spec/support/matchers/transaction.rb +3 -3
  99. data/spec/support/mocks/appsignal_mock.rb +3 -3
  100. data/spec/support/mocks/mock_probe.rb +2 -0
  101. data/spec/support/testing.rb +2 -2
  102. metadata +11 -21
  103. data/gemfiles/que_beta.gemfile +0 -5
  104. data/lib/appsignal/helpers/heartbeat.rb +0 -20
  105. data/lib/appsignal/integrations/grape.rb +0 -35
  106. data/lib/appsignal/integrations/hanami.rb +0 -13
  107. data/lib/appsignal/integrations/padrino.rb +0 -13
  108. data/lib/appsignal/integrations/sinatra.rb +0 -13
  109. data/lib/appsignal/rack/generic_instrumentation.rb +0 -22
  110. data/lib/appsignal/rack/streaming_listener.rb +0 -28
  111. data/spec/lib/appsignal/integrations/grape_spec.rb +0 -36
  112. data/spec/lib/appsignal/integrations/hanami_spec.rb +0 -17
  113. data/spec/lib/appsignal/integrations/padrino_spec.rb +0 -15
  114. data/spec/lib/appsignal/integrations/sinatra_spec.rb +0 -15
  115. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +0 -81
  116. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +0 -69
  117. data/spec/support/fixtures/projects/valid/config/environments/development.rb +0 -0
  118. data/spec/support/fixtures/projects/valid/config/environments/production.rb +0 -0
  119. data/spec/support/fixtures/projects/valid/config/environments/test.rb +0 -0
  120. data/spec/support/rails/my_app.rb +0 -6
  121. /data/spec/support/fixtures/projects/{valid/config/application.rb → valid_with_rails_app/log/.gitkeep} +0 -0
@@ -9,8 +9,6 @@ module Appsignal
9
9
  # @api private
10
10
  ACTION_CABLE = "action_cable"
11
11
  # @api private
12
- FRONTEND = "frontend"
13
- # @api private
14
12
  BLANK = ""
15
13
  # @api private
16
14
  ALLOWED_TAG_KEY_TYPES = [Symbol, String].freeze
@@ -20,69 +18,27 @@ module Appsignal
20
18
  BREADCRUMB_LIMIT = 20
21
19
  # @api private
22
20
  ERROR_CAUSES_LIMIT = 10
21
+ ERRORS_LIMIT = 10
23
22
 
24
23
  class << self
25
24
  # Create a new transaction and set it as the currently active
26
25
  # transaction.
27
26
  #
28
- # @param id_or_namespace [String] Namespace of the to be created transaction.
27
+ # @param namespace [String] Namespace of the to be created transaction.
29
28
  # @return [Transaction]
30
- def create(id_or_namespace, arg_namespace = nil, request = nil, options = {})
31
- if id_or_namespace && arg_namespace
32
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
33
- "Appsignal::Transaction.create: " \
34
- "A new Transaction is created using the transaction ID argument. " \
35
- "This argument is deprecated without replacement."
36
- )
37
- end
38
- if arg_namespace
39
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
40
- "Appsignal::Transaction.create: " \
41
- "A Transaction is created using the namespace argument. " \
42
- "Specify the namespace as the first argument to the 'create' " \
43
- "method without the ID argument."
44
- )
45
- end
46
- if request
47
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
48
- "Appsignal::Transaction.create: " \
49
- "A Transaction is created using the request argument. " \
50
- "This argument is deprecated. Please use the `Appsignal.set_*` helpers instead."
51
- )
52
- end
53
- # Allow middleware to force a new transaction
54
- if options[:force]
55
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
56
- "Appsignal::Transaction.create: " \
57
- "A Transaction is created using the `:force => true` option argument. " \
58
- "The options argument is deprecated without replacement."
59
- )
60
- Thread.current[:appsignal_transaction] = nil
61
- end
62
- if arg_namespace
63
- id = id_or_namespace
64
- namespace = arg_namespace
65
- else
66
- id = SecureRandom.uuid
67
- namespace = id_or_namespace
68
- end
69
-
29
+ def create(namespace)
70
30
  # Check if we already have a running transaction
71
31
  if Thread.current[:appsignal_transaction].nil?
72
32
  # If not, start a new transaction
73
- Thread.current[:appsignal_transaction] =
74
- Appsignal::Transaction.new(
75
- id,
76
- namespace,
77
- request,
78
- options
79
- )
33
+ set_current_transaction(
34
+ Appsignal::Transaction.new(SecureRandom.uuid, namespace)
35
+ )
80
36
  else
81
37
  # Otherwise, log the issue about trying to start another transaction
82
38
  Appsignal.internal_logger.warn(
83
- "Trying to start new transaction with id " \
84
- "'#{id}', but a transaction with id '#{current.transaction_id}' " \
85
- "is already running. Using transaction '#{current.transaction_id}'."
39
+ "Trying to start new transaction, but a transaction " \
40
+ "with id '#{current.transaction_id}' is already running. " \
41
+ "Using transaction '#{current.transaction_id}'."
86
42
  )
87
43
 
88
44
  # And return the current transaction instead
@@ -90,6 +46,23 @@ module Appsignal
90
46
  end
91
47
  end
92
48
 
49
+ # @api private
50
+ def set_current_transaction(transaction)
51
+ Thread.current[:appsignal_transaction] = transaction
52
+ end
53
+
54
+ # Set the current for the duration of the given block.
55
+ # It restores the original transaction (if any) when the block has executed.
56
+ #
57
+ # @api private
58
+ def with_transaction(transaction)
59
+ original_transaction = current if current?
60
+ set_current_transaction(transaction)
61
+ yield
62
+ ensure
63
+ set_current_transaction(original_transaction)
64
+ end
65
+
93
66
  # Returns currently active transaction or a {NilTransaction} if none is
94
67
  # active.
95
68
  #
@@ -125,11 +98,19 @@ module Appsignal
125
98
  def clear_current_transaction!
126
99
  Thread.current[:appsignal_transaction] = nil
127
100
  end
101
+
102
+ # @api private
103
+ def last_errors
104
+ @last_errors ||= []
105
+ end
106
+
107
+ # @api private
108
+ attr_writer :last_errors
128
109
  end
129
110
 
130
111
  # @api private
131
112
  attr_reader :ext, :transaction_id, :action, :namespace, :request, :paused, :tags, :options,
132
- :breadcrumbs, :custom_data
113
+ :breadcrumbs, :is_duplicate, :error_blocks
133
114
 
134
115
  # Use {.create} to create new transactions.
135
116
  #
@@ -137,24 +118,25 @@ module Appsignal
137
118
  # @param namespace [String] Namespace of the to be created transaction.
138
119
  # @see create
139
120
  # @api private
140
- def initialize(transaction_id, namespace, request = nil, options = {})
121
+ def initialize(transaction_id, namespace, ext: nil)
141
122
  @transaction_id = transaction_id
142
123
  @action = nil
143
124
  @namespace = namespace
144
- @request = request || InternalGenericRequest.new({})
145
125
  @paused = false
146
126
  @discarded = false
147
127
  @tags = {}
148
- @custom_data = nil
149
128
  @breadcrumbs = []
150
129
  @store = Hash.new({})
151
- @options = options
152
- @options[:params_method] ||= :params
153
- @params = nil
154
- @session_data = nil
155
- @headers = nil
130
+ @error_blocks = Hash.new { |hash, key| hash[key] = [] }
131
+ @is_duplicate = false
132
+ @error_set = nil
133
+
134
+ @params = Appsignal::SampleData.new(:params)
135
+ @session_data = Appsignal::SampleData.new(:session_data, Hash)
136
+ @headers = Appsignal::SampleData.new(:headers, Hash)
137
+ @custom_data = Appsignal::SampleData.new(:custom_data)
156
138
 
157
- @ext = Appsignal::Extension.start_transaction(
139
+ @ext = ext || Appsignal::Extension.start_transaction(
158
140
  @transaction_id,
159
141
  @namespace,
160
142
  0
@@ -171,8 +153,44 @@ module Appsignal
171
153
  "because it was manually discarded."
172
154
  return
173
155
  end
174
- _sample_data if @ext.finish(0)
175
- @ext.complete
156
+
157
+ # If the transaction is a duplicate, we don't want to finish it,
158
+ # because we want its finish time to be the finish time of the
159
+ # original transaction.
160
+ # Duplicate transactions should always be sampled, as we only
161
+ # create duplicates for errors, which are always sampled.
162
+ should_sample = true
163
+
164
+ unless is_duplicate
165
+ self.class.last_errors = @error_blocks.keys
166
+ should_sample = ext.finish(0)
167
+ end
168
+
169
+ @error_blocks.each do |error, blocks|
170
+ # Ignore the error that is already set in this transaction.
171
+ next if error == @error_set
172
+
173
+ duplicate.tap do |transaction|
174
+ # In the duplicate transaction for each error, set an error
175
+ # with a block that calls all the blocks set for that error
176
+ # in the original transaction.
177
+ transaction.set_error(error) do
178
+ blocks.each { |block| block.call(transaction) }
179
+ end
180
+
181
+ transaction.complete
182
+ end
183
+ end
184
+
185
+ if @error_set && @error_blocks[@error_set].any?
186
+ self.class.with_transaction(self) do
187
+ @error_blocks[@error_set].each do |block|
188
+ block.call(self)
189
+ end
190
+ end
191
+ end
192
+ sample_data if should_sample
193
+ ext.complete
176
194
  end
177
195
 
178
196
  # @api private
@@ -210,69 +228,40 @@ module Appsignal
210
228
  @store[key]
211
229
  end
212
230
 
213
- # @api private
214
- def params
215
- parameters = @params || request_params
216
-
217
- if parameters.respond_to? :call
218
- parameters.call
219
- else
220
- parameters
221
- end
222
- rescue => e
223
- Appsignal.internal_logger.error("Exception while fetching params: #{e.class}: #{e}")
224
- nil
225
- end
226
-
227
- # Set parameters on the transaction.
228
- #
229
- # When no parameters are set this way, the transaction will look for
230
- # parameters on the {#request} environment.
231
+ # Add parameters to the transaction.
231
232
  #
232
- # The parameters set using {#set_params} are leading over those extracted
233
- # from a request's environment.
233
+ # When this method is called multiple times, it will merge the request parameters.
234
234
  #
235
235
  # When both the `given_params` and a block is given to this method, the
236
- # `given_params` argument is leading and the block will _not_ be called.
236
+ # block is leading and the argument will _not_ be used.
237
237
  #
238
- # @since 3.9.1
238
+ # @since 4.0.0
239
239
  # @param given_params [Hash] The parameters to set on the transaction.
240
240
  # @yield This block is called when the transaction is sampled. The block's
241
241
  # return value will become the new parameters.
242
242
  # @return [void]
243
- # @see Helpers::Instrumentation#set_params
244
- def set_params(given_params = nil, &block)
245
- @params = block if block
246
- @params = given_params if given_params
247
- end
248
-
249
- # @deprecated Use {#set_params} or {#set_params_if_nil} instead.
250
- def params=(given_params)
251
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
252
- "Transaction#params= is deprecated." \
253
- "Use Transaction#set_params or #set_params_if_nil instead."
254
- )
255
- set_params(given_params)
243
+ # @see Helpers::Instrumentation#add_params
244
+ def add_params(given_params = nil, &block)
245
+ @params.add(given_params, &block)
256
246
  end
247
+ alias :set_params :add_params
257
248
 
258
- # Set parameters on the transaction if not already set
249
+ # Add parameters to the transaction if not already set.
259
250
  #
260
- # When no parameters are set this way, the transaction will look for
261
- # parameters on the {#request} environment.
262
- #
263
- # @since 3.9.1
251
+ # @api private
252
+ # @since 4.0.0
264
253
  # @param given_params [Hash] The parameters to set on the transaction if none are already set.
265
254
  # @yield This block is called when the transaction is sampled. The block's
266
255
  # return value will become the new parameters.
267
256
  # @return [void]
268
257
  #
269
- # @see #set_params
270
- # @see Helpers::Instrumentation#set_params_if_nil
271
- def set_params_if_nil(given_params = nil, &block)
272
- set_params(given_params, &block) unless @params
258
+ # @see #add_params
259
+ def add_params_if_nil(given_params = nil, &block)
260
+ add_params(given_params, &block) unless @params.value?
273
261
  end
262
+ alias :set_params_if_nil :add_params_if_nil
274
263
 
275
- # Set tags on the transaction.
264
+ # Add tags to the transaction.
276
265
  #
277
266
  # When this method is called multiple times, it will merge the tags.
278
267
  #
@@ -283,32 +272,34 @@ module Appsignal
283
272
  # The name of the tag as a String.
284
273
  # @return [void]
285
274
  #
286
- # @see Helpers::Instrumentation#tag_request
275
+ # @see Helpers::Instrumentation#add_tags
287
276
  # @see https://docs.appsignal.com/ruby/instrumentation/tagging.html
288
277
  # Tagging guide
289
- def set_tags(given_tags = {})
278
+ def add_tags(given_tags = {})
290
279
  @tags.merge!(given_tags)
291
280
  end
281
+ alias :set_tags add_tags
292
282
 
293
- # Set session data on the transaction.
283
+ # Add session data to the transaction.
284
+ #
285
+ # When this method is called multiple times, it will merge the session data.
294
286
  #
295
287
  # When both the `given_session_data` and a block is given to this method,
296
- # the `given_session_data` argument is leading and the block will _not_ be
297
- # called.
288
+ # the block is leading and the argument will _not_ be used.
298
289
  #
299
290
  # @param given_session_data [Hash] A hash containing session data.
300
291
  # @yield This block is called when the transaction is sampled. The block's
301
292
  # return value will become the new session data.
302
293
  # @return [void]
303
294
  #
304
- # @since 3.10.1
305
- # @see Helpers::Instrumentation#set_session_data
295
+ # @since 4.0.0
296
+ # @see Helpers::Instrumentation#add_session_data
306
297
  # @see https://docs.appsignal.com/guides/custom-data/sample-data.html
307
298
  # Sample data guide
308
- def set_session_data(given_session_data = nil, &block)
309
- @session_data = block if block
310
- @session_data = given_session_data if given_session_data
299
+ def add_session_data(given_session_data = nil, &block)
300
+ @session_data.add(given_session_data, &block)
311
301
  end
302
+ alias :set_session_data :add_session_data
312
303
 
313
304
  # Set session data on the transaction if not already set.
314
305
  #
@@ -321,73 +312,64 @@ module Appsignal
321
312
  # return value will become the new session data.
322
313
  # @return [void]
323
314
  #
324
- # @since 3.10.1
325
- # @see #set_session_data
315
+ # @api private
316
+ # @since 4.0.0
317
+ # @see #add_session_data
326
318
  # @see https://docs.appsignal.com/guides/custom-data/sample-data.html
327
319
  # Sample data guide
328
- def set_session_data_if_nil(given_session_data = nil, &block)
329
- set_session_data(given_session_data, &block) unless @session_data
320
+ def add_session_data_if_nil(given_session_data = nil, &block)
321
+ add_session_data(given_session_data, &block) unless @session_data.value?
330
322
  end
323
+ alias :set_session_data_if_nil :add_session_data_if_nil
331
324
 
332
- # Set headers on the transaction.
333
- #
334
- # When both the `given_headers` and a block is given to this method,
335
- # the `given_headers` argument is leading and the block will _not_ be
336
- # called.
325
+ # Add headers to the transaction.
337
326
  #
338
327
  # @param given_headers [Hash] A hash containing headers.
339
328
  # @yield This block is called when the transaction is sampled. The block's
340
329
  # return value will become the new headers.
341
330
  # @return [void]
342
331
  #
343
- # @since 3.10.1
344
- # @see Helpers::Instrumentation#set_headers
332
+ # @since 4.0.0
333
+ # @see Helpers::Instrumentation#add_headers
345
334
  # @see https://docs.appsignal.com/guides/custom-data/sample-data.html
346
335
  # Sample data guide
347
- def set_headers(given_headers = nil, &block)
348
- @headers = block if block
349
- @headers = given_headers if given_headers
336
+ def add_headers(given_headers = nil, &block)
337
+ @headers.add(given_headers, &block)
350
338
  end
339
+ alias :set_headers :add_headers
351
340
 
352
- # Set headers on the transaction if not already set.
341
+ # Add headers to the transaction if not already set.
353
342
  #
354
343
  # When both the `given_headers` and a block is given to this method,
355
- # the `given_headers` argument is leading and the block will _not_ be
356
- # called.
344
+ # the block is leading and the argument will _not_ be used.
357
345
  #
358
346
  # @param given_headers [Hash] A hash containing headers.
359
347
  # @yield This block is called when the transaction is sampled. The block's
360
348
  # return value will become the new headers.
361
349
  # @return [void]
362
350
  #
363
- # @since 3.10.1
364
- # @see #set_headers
351
+ # @api private
352
+ # @since 4.0.0
353
+ # @see #add_headers
365
354
  # @see https://docs.appsignal.com/guides/custom-data/sample-data.html
366
355
  # Sample data guide
367
- def set_headers_if_nil(given_headers = nil, &block)
368
- set_headers(given_headers, &block) unless @headers
356
+ def add_headers_if_nil(given_headers = nil, &block)
357
+ add_headers(given_headers, &block) unless @headers.value?
369
358
  end
359
+ alias :set_headers_if_nil :add_headers_if_nil
370
360
 
371
- # Set custom data on the transaction.
361
+ # Add custom data to the transaction.
372
362
  #
373
- # When this method is called multiple times, it will overwrite the
374
- # previously set value.
375
- #
376
- # @since 3.10.0
377
- # @see Appsignal.set_custom_data
363
+ # @since 4.0.0
364
+ # @see Helpers::Instrumentation#add_custom_data
378
365
  # @see https://docs.appsignal.com/guides/custom-data/sample-data.html
379
366
  # Sample data guide
380
367
  # @param data [Hash/Array]
381
368
  # @return [void]
382
- def set_custom_data(data)
383
- case data
384
- when Array, Hash
385
- @custom_data = data
386
- else
387
- Appsignal.internal_logger
388
- .error("set_custom_data: Unsupported data type #{data.class} received.")
389
- end
369
+ def add_custom_data(data)
370
+ @custom_data.add(data)
390
371
  end
372
+ alias :set_custom_data :add_custom_data
391
373
 
392
374
  # Add breadcrumbs to the transaction.
393
375
  #
@@ -479,18 +461,6 @@ module Appsignal
479
461
  @ext.set_namespace(namespace)
480
462
  end
481
463
 
482
- # @deprecated Use the {#set_action} helper.
483
- # @api private
484
- def set_http_or_background_action(from = request.params)
485
- return unless from
486
-
487
- group_and_action = [
488
- from[:controller] || from[:class],
489
- from[:action] || from[:method]
490
- ]
491
- set_action_if_nil(group_and_action.compact.join("#"))
492
- end
493
-
494
464
  # Set queue start time for transaction.
495
465
  #
496
466
  # @param start [Integer] Queue start time in milliseconds.
@@ -507,35 +477,6 @@ module Appsignal
507
477
  Appsignal.internal_logger.warn("Queue start value #{start} is too big")
508
478
  end
509
479
 
510
- # Set the queue time based on the HTTP header or `:queue_start` env key
511
- # value.
512
- #
513
- # This method will first try to read the queue time from the HTTP headers
514
- # `X-Request-Start` or `X-Queue-Start`. Which are parsed by Rack as
515
- # `HTTP_X_QUEUE_START` and `HTTP_X_REQUEST_START`.
516
- # The header value is parsed by AppSignal as either milliseconds or
517
- # microseconds.
518
- #
519
- # If no headers are found, or the value could not be parsed, it falls back
520
- # on the `:queue_start` env key on this Transaction's {request} environment
521
- # (called like `request.env[:queue_start]`). This value is parsed by
522
- # AppSignal as seconds.
523
- #
524
- # @see https://docs.appsignal.com/ruby/instrumentation/request-queue-time.html
525
- # @deprecated Use {#set_queue_start} instead.
526
- # @return [void]
527
- def set_http_or_background_queue_start
528
- Appsignal::Utils::StdoutAndLoggerMessage.warning \
529
- "The Appsignal::Transaction#set_http_or_background_queue_start " \
530
- "method has been deprecated. " \
531
- "Please use the Appsignal::Transaction#set_queue_start method instead."
532
-
533
- start = http_queue_start || background_queue_start
534
- return unless start
535
-
536
- set_queue_start(start)
537
- end
538
-
539
480
  # @api private
540
481
  def set_metadata(key, value)
541
482
  return unless key && value
@@ -544,81 +485,33 @@ module Appsignal
544
485
  @ext.set_metadata(key, value)
545
486
  end
546
487
 
547
- # @deprecated Use one of the set_tags, set_params, set_session_data,
548
- # set_params or set_custom_data helpers instead.
549
- # @api private
550
- def set_sample_data(key, data)
551
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
552
- "Appsignal::Transaction#set_sample_data is deprecated. " \
553
- "Please use one of the instrumentation helpers: set_tags, " \
554
- "set_params, set_session_data, set_params or set_custom_data."
555
- )
556
- _set_sample_data(key, data)
557
- end
558
-
559
- # @deprecated No replacement.
560
- # @api private
561
- def sample_data
562
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
563
- "Appsignal::Transaction#sample_data is deprecated. " \
564
- "Please remove any calls to this method."
565
- )
566
- _sample_data
567
- end
568
-
569
- # @see Appsignal::Helpers::Instrumentation#set_error
570
- def set_error(error)
488
+ # @see Appsignal::Helpers::Instrumentation#add_error
489
+ def add_error(error, &block)
571
490
  unless error.is_a?(Exception)
572
- Appsignal.internal_logger.error "Appsignal::Transaction#set_error: Cannot set error. " \
491
+ Appsignal.internal_logger.error "Appsignal::Transaction#add_error: Cannot add error. " \
573
492
  "The given value is not an exception: #{error.inspect}"
574
493
  return
575
494
  end
495
+
576
496
  return unless error
577
497
  return unless Appsignal.active?
578
498
 
579
- backtrace = cleaned_backtrace(error.backtrace)
580
- @ext.set_error(
581
- error.class.name,
582
- cleaned_error_message(error),
583
- backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new
584
- )
585
-
586
- root_cause_missing = false
587
-
588
- causes = []
589
- while error
590
- error = error.cause
591
-
592
- break unless error
499
+ _set_error(error) if @error_blocks.empty?
593
500
 
594
- if causes.length >= ERROR_CAUSES_LIMIT
595
- Appsignal.internal_logger.debug "Appsignal::Transaction#set_error: Error has more " \
596
- "than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
597
- "will be reported."
598
- root_cause_missing = true
599
- break
600
- end
601
-
602
- causes << error
501
+ if !@error_blocks.include?(error) && @error_blocks.length >= ERRORS_LIMIT
502
+ Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
503
+ "than #{ERRORS_LIMIT} distinct errors. Only the first " \
504
+ "#{ERRORS_LIMIT} distinct errors will be reported."
505
+ return
603
506
  end
604
507
 
605
- return if causes.empty?
606
-
607
- causes_sample_data = causes.map do |e|
608
- {
609
- :name => e.class.name,
610
- :message => cleaned_error_message(e)
611
- }
612
- end
508
+ @error_blocks[error] << block
509
+ @error_blocks[error].compact!
510
+ end
613
511
 
614
- causes_sample_data.last[:is_root_cause] = false if root_cause_missing
512
+ alias :set_error :add_error
615
513
 
616
- _set_sample_data(
617
- "error_causes",
618
- causes_sample_data
619
- )
620
- end
621
- alias_method :add_exception, :set_error
514
+ alias_method :add_exception, :add_error
622
515
 
623
516
  # @see Helpers::Instrumentation#instrument
624
517
  # @api private
@@ -671,37 +564,56 @@ module Appsignal
671
564
  end
672
565
  alias_method :to_hash, :to_h
673
566
 
674
- # @api private
675
- class InternalGenericRequest
676
- attr_reader :env
567
+ protected
677
568
 
678
- def initialize(env)
679
- @env = env
680
- end
569
+ attr_writer :is_duplicate, :tags, :custom_data, :breadcrumbs, :params, :session_data, :headers
570
+
571
+ private
572
+
573
+ def _set_error(error)
574
+ backtrace = cleaned_backtrace(error.backtrace)
575
+ @ext.set_error(
576
+ error.class.name,
577
+ cleaned_error_message(error),
578
+ backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new
579
+ )
580
+ @error_set = error
581
+
582
+ root_cause_missing = false
583
+
584
+ causes = []
585
+ while error
586
+ error = error.cause
587
+
588
+ break unless error
589
+
590
+ if causes.length >= ERROR_CAUSES_LIMIT
591
+ Appsignal.internal_logger.debug "Appsignal::Transaction#add_error: Error has more " \
592
+ "than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
593
+ "will be reported."
594
+ root_cause_missing = true
595
+ break
596
+ end
681
597
 
682
- def params
683
- env[:params]
598
+ causes << error
684
599
  end
685
- end
686
600
 
687
- # @deprecated Use the instrumentation helpers to set metadata on the
688
- # transaction, rather than rely on the GenericRequest automation. See the
689
- # {Helpers::Instrumentation} module for a list of helpers.
690
- # @api private
691
- class GenericRequest < InternalGenericRequest
692
- def initialize(_env)
693
- Appsignal::Utils::StdoutAndLoggerMessage.warning(
694
- "The use of Appsignal::Transaction::GenericRequest is deprecated. " \
695
- "Use the `Appsignal.set_*` helpers instead. " \
696
- "https://docs.appsignal.com/guides/custom-data/sample-data.html"
697
- )
698
- super
601
+ causes_sample_data = causes.map do |e|
602
+ {
603
+ :name => e.class.name,
604
+ :message => cleaned_error_message(e)
605
+ }
699
606
  end
700
- end
701
607
 
702
- private
608
+ causes_sample_data.last[:is_root_cause] = false if root_cause_missing
609
+
610
+ set_sample_data(
611
+ "error_causes",
612
+ causes_sample_data
613
+ )
614
+ end
703
615
 
704
- def _set_sample_data(key, data)
616
+ def set_sample_data(key, data)
705
617
  return unless key && data
706
618
 
707
619
  if !data.is_a?(Array) && !data.is_a?(Hash)
@@ -728,44 +640,43 @@ module Appsignal
728
640
  end
729
641
  end
730
642
 
731
- def _sample_data
643
+ def sample_data
732
644
  {
733
645
  :params => sanitized_params,
734
- :environment => sanitized_environment,
646
+ :environment => sanitized_request_headers,
735
647
  :session_data => sanitized_session_data,
736
- :metadata => sanitized_metadata,
737
648
  :tags => sanitized_tags,
738
649
  :breadcrumbs => breadcrumbs,
739
650
  :custom_data => custom_data
740
651
  }.each do |key, data|
741
- _set_sample_data(key, data)
652
+ set_sample_data(key, data)
742
653
  end
743
654
  end
744
655
 
745
- # Returns calculated background queue start time in milliseconds, based on
746
- # environment values.
747
- #
748
- # @return [nil] if no {#environment} is present.
749
- # @return [nil] if there is no `:queue_start` in the {#environment}.
750
- # @return [Integer] `:queue_start` time (in seconds) converted to milliseconds
751
- def background_queue_start
752
- env = environment
753
- return unless env
754
-
755
- queue_start = env[:queue_start]
756
- return unless queue_start
757
-
758
- (queue_start.to_f * 1000.0).to_i # Convert seconds to milliseconds
656
+ def duplicate
657
+ new_transaction_id = SecureRandom.uuid
658
+
659
+ self.class.new(
660
+ new_transaction_id,
661
+ namespace,
662
+ :ext => ext.duplicate(new_transaction_id)
663
+ ).tap do |transaction|
664
+ transaction.is_duplicate = true
665
+ transaction.tags = @tags.dup
666
+ transaction.custom_data = @custom_data.dup
667
+ transaction.breadcrumbs = @breadcrumbs.dup
668
+ transaction.params = @params.dup
669
+ transaction.session_data = @session_data.dup
670
+ transaction.headers = @headers.dup
671
+ end
759
672
  end
760
673
 
761
- # Returns HTTP queue start time in milliseconds.
762
- #
763
- # @return [nil] if no queue start time is found.
764
- # @return [nil] if begin time is too low to be plausible.
765
- # @return [Integer] queue start in milliseconds.
766
- def http_queue_start
767
- env = environment
768
- Appsignal::Rack::Utils.queue_start_from(env)
674
+ # @api private
675
+ def params
676
+ @params.value
677
+ rescue => e
678
+ Appsignal.internal_logger.error("Exception while fetching params: #{e.class}: #{e}")
679
+ nil
769
680
  end
770
681
 
771
682
  def sanitized_params
@@ -775,27 +686,8 @@ module Appsignal
775
686
  Appsignal::Utils::HashSanitizer.sanitize params, filter_keys
776
687
  end
777
688
 
778
- def request_params
779
- return unless request.respond_to?(options[:params_method])
780
-
781
- begin
782
- request.send options[:params_method]
783
- rescue => e
784
- Appsignal.internal_logger.warn "Exception while getting params: #{e}"
785
- nil
786
- end
787
- end
788
-
789
689
  def session_data
790
- if @session_data
791
- if @session_data.respond_to? :call
792
- @session_data.call
793
- else
794
- @session_data
795
- end
796
- elsif request.respond_to?(:session)
797
- request.session
798
- end
690
+ @session_data.value
799
691
  rescue => e
800
692
  Appsignal.internal_logger.error \
801
693
  "Exception while fetching session data: #{e.class}: #{e}"
@@ -814,35 +706,12 @@ module Appsignal
814
706
  return unless Appsignal.config[:send_session_data]
815
707
 
816
708
  Appsignal::Utils::HashSanitizer.sanitize(
817
- session_data&.to_hash, Appsignal.config[:filter_session_data]
709
+ session_data, Appsignal.config[:filter_session_data]
818
710
  )
819
711
  end
820
712
 
821
- # Returns sanitized metadata set on the request environment.
822
- #
823
- # @return [Hash<String, Object>]
824
- def sanitized_metadata
825
- env = environment
826
- return unless env
827
-
828
- metadata = env[:metadata]
829
- return unless metadata
830
-
831
- metadata
832
- .transform_keys(&:to_s)
833
- .reject { |key, _value| Appsignal.config[:filter_metadata].include?(key) }
834
- end
835
-
836
- def environment
837
- if @headers
838
- if @headers.respond_to? :call
839
- @headers.call
840
- else
841
- @headers
842
- end
843
- elsif request.respond_to?(:env)
844
- request.env
845
- end
713
+ def request_headers
714
+ @headers.value
846
715
  rescue => e
847
716
  Appsignal.internal_logger.error \
848
717
  "Exception while fetching headers: #{e.class}: #{e}"
@@ -856,15 +725,13 @@ module Appsignal
856
725
  #
857
726
  # @return [nil] if no environment is present.
858
727
  # @return [Hash<String, Object>]
859
- def sanitized_environment
860
- env = environment
861
- return unless env
862
- return unless env.respond_to?(:empty?)
863
- return if env.empty?
728
+ def sanitized_request_headers
729
+ headers = request_headers
730
+ return unless headers
864
731
 
865
732
  {}.tap do |out|
866
733
  Appsignal.config[:request_headers].each do |key|
867
- out[key] = env[key] if env[key]
734
+ out[key] = headers[key] if headers[key]
868
735
  end
869
736
  end
870
737
  end
@@ -890,6 +757,13 @@ module Appsignal
890
757
  end
891
758
  end
892
759
 
760
+ def custom_data
761
+ @custom_data.value
762
+ rescue => e
763
+ Appsignal.internal_logger.error("Exception while fetching custom data: #{e.class}: #{e}")
764
+ nil
765
+ end
766
+
893
767
  # Clean error messages that are known to potentially contain user data.
894
768
  # Returns an unchanged message otherwise.
895
769
  def cleaned_error_message(error)