appsignal 3.0.0.beta.1-java → 3.0.3-java

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/.rubocop_todo.yml +1 -1
  3. data/.semaphore/semaphore.yml +88 -88
  4. data/CHANGELOG.md +41 -1
  5. data/Rakefile +12 -4
  6. data/appsignal.gemspec +7 -5
  7. data/build_matrix.yml +11 -11
  8. data/ext/agent.yml +17 -17
  9. data/gemfiles/no_dependencies.gemfile +0 -7
  10. data/lib/appsignal.rb +1 -2
  11. data/lib/appsignal/config.rb +1 -1
  12. data/lib/appsignal/extension.rb +50 -0
  13. data/lib/appsignal/helpers/instrumentation.rb +69 -5
  14. data/lib/appsignal/hooks.rb +16 -0
  15. data/lib/appsignal/hooks/action_cable.rb +10 -2
  16. data/lib/appsignal/hooks/sidekiq.rb +9 -142
  17. data/lib/appsignal/integrations/object.rb +21 -43
  18. data/lib/appsignal/integrations/railtie.rb +0 -4
  19. data/lib/appsignal/integrations/sidekiq.rb +171 -0
  20. data/lib/appsignal/minutely.rb +6 -0
  21. data/lib/appsignal/transaction.rb +2 -2
  22. data/lib/appsignal/version.rb +1 -1
  23. data/spec/lib/appsignal/config_spec.rb +2 -0
  24. data/spec/lib/appsignal/extension_install_failure_spec.rb +0 -7
  25. data/spec/lib/appsignal/extension_spec.rb +43 -9
  26. data/spec/lib/appsignal/hooks/action_cable_spec.rb +88 -0
  27. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +60 -458
  28. data/spec/lib/appsignal/hooks_spec.rb +41 -0
  29. data/spec/lib/appsignal/integrations/object_spec.rb +91 -4
  30. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +524 -0
  31. data/spec/lib/appsignal/transaction_spec.rb +17 -0
  32. data/spec/lib/appsignal/utils/data_spec.rb +133 -87
  33. data/spec/lib/appsignal_spec.rb +162 -47
  34. data/spec/lib/puma/appsignal_spec.rb +28 -0
  35. data/spec/spec_helper.rb +22 -0
  36. data/spec/support/testing.rb +11 -1
  37. metadata +9 -8
  38. data/gemfiles/rails-4.0.gemfile +0 -6
  39. data/gemfiles/rails-4.1.gemfile +0 -6
@@ -78,6 +78,47 @@ describe Appsignal::Hooks do
78
78
  expect(Appsignal::Hooks.hooks[:mock_error_hook].installed?).to be_falsy
79
79
  Appsignal::Hooks.hooks.delete(:mock_error_hook)
80
80
  end
81
+
82
+ describe "missing constants" do
83
+ let(:err_stream) { std_stream }
84
+ let(:stderr) { err_stream.read }
85
+ let(:log_stream) { std_stream }
86
+ let(:log) { log_contents(log_stream) }
87
+ before do
88
+ Appsignal.logger = test_logger(log_stream)
89
+ end
90
+
91
+ def call_constant(&block)
92
+ capture_std_streams(std_stream, err_stream, &block)
93
+ end
94
+
95
+ describe "SidekiqPlugin" do
96
+ it "logs a deprecation message and returns the new constant" do
97
+ constant = call_constant { Appsignal::Hooks::SidekiqPlugin }
98
+
99
+ expect(constant).to eql(Appsignal::Integrations::SidekiqMiddleware)
100
+ expect(constant.name).to eql("Appsignal::Integrations::SidekiqMiddleware")
101
+
102
+ deprecation_message =
103
+ "The constant Appsignal::Hooks::SidekiqPlugin has been deprecated. " \
104
+ "Please update the constant name to Appsignal::Integrations::SidekiqMiddleware " \
105
+ "in the following file to remove this message.\n#{__FILE__}:"
106
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
107
+ expect(log).to contains_log :warn, deprecation_message
108
+ end
109
+ end
110
+
111
+ describe "other constant" do
112
+ it "raises a NameError like Ruby normally does" do
113
+ expect do
114
+ call_constant { Appsignal::Hooks::Unknown }
115
+ end.to raise_error(NameError)
116
+
117
+ expect(stderr).to be_empty
118
+ expect(log).to be_empty
119
+ end
120
+ end
121
+ end
81
122
  end
82
123
 
83
124
  describe Appsignal::Hooks::Helpers do
@@ -26,12 +26,57 @@ describe Object do
26
26
  before do
27
27
  Appsignal.config = project_fixture_config
28
28
  expect(Appsignal::Transaction).to receive(:current).at_least(:once).and_return(transaction)
29
+ expect(Appsignal.active?).to be_truthy
29
30
  end
30
31
  after { Appsignal.config = nil }
31
32
 
33
+ context "with different kind of arguments" do
34
+ let(:klass) do
35
+ Class.new do
36
+ def positional_arguments(param1, param2)
37
+ [param1, param2]
38
+ end
39
+ appsignal_instrument_method :positional_arguments
40
+
41
+ def positional_arguments_splat(*params)
42
+ params
43
+ end
44
+ appsignal_instrument_method :positional_arguments_splat
45
+
46
+ def keyword_arguments(a: nil, b: nil)
47
+ [a, b]
48
+ end
49
+ appsignal_instrument_method :keyword_arguments
50
+
51
+ def keyword_arguments_splat(**kwargs)
52
+ kwargs
53
+ end
54
+ appsignal_instrument_method :keyword_arguments_splat
55
+
56
+ def splat(*args, **kwargs)
57
+ [args, kwargs]
58
+ end
59
+ appsignal_instrument_method :splat
60
+ end
61
+ end
62
+
63
+ it "instruments the method and calls it" do
64
+ expect(instance.positional_arguments("abc", "def")).to eq(["abc", "def"])
65
+ expect(instance.positional_arguments_splat("abc", "def")).to eq(["abc", "def"])
66
+ expect(instance.keyword_arguments(:a => "a", :b => "b")).to eq(["a", "b"])
67
+ expect(instance.keyword_arguments_splat(:a => "a", :b => "b"))
68
+ .to eq(:a => "a", :b => "b")
69
+
70
+ expect(instance.splat).to eq([[], {}])
71
+ expect(instance.splat(:a => "a", :b => "b")).to eq([[], { :a => "a", :b => "b" }])
72
+ expect(instance.splat("abc", "def")).to eq([["abc", "def"], {}])
73
+ expect(instance.splat("abc", "def", :a => "a", :b => "b"))
74
+ .to eq([["abc", "def"], { :a => "a", :b => "b" }])
75
+ end
76
+ end
77
+
32
78
  context "with anonymous class" do
33
79
  it "instruments the method and calls it" do
34
- expect(Appsignal.active?).to be_truthy
35
80
  expect(transaction).to receive(:start_event)
36
81
  expect(transaction).to receive(:finish_event).with \
37
82
  "foo.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -52,7 +97,6 @@ describe Object do
52
97
  let(:klass) { NamedClass }
53
98
 
54
99
  it "instruments the method and calls it" do
55
- expect(Appsignal.active?).to be_truthy
56
100
  expect(transaction).to receive(:start_event)
57
101
  expect(transaction).to receive(:finish_event).with \
58
102
  "foo.NamedClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -77,7 +121,6 @@ describe Object do
77
121
  let(:klass) { MyModule::NestedModule::NamedClass }
78
122
 
79
123
  it "instruments the method and calls it" do
80
- expect(Appsignal.active?).to be_truthy
81
124
  expect(transaction).to receive(:start_event)
82
125
  expect(transaction).to receive(:finish_event).with \
83
126
  "bar.NamedClass.NestedModule.MyModule.other", nil, nil,
@@ -97,7 +140,6 @@ describe Object do
97
140
  end
98
141
 
99
142
  it "instruments with custom name" do
100
- expect(Appsignal.active?).to be_truthy
101
143
  expect(transaction).to receive(:start_event)
102
144
  expect(transaction).to receive(:finish_event).with \
103
145
  "my_method.group", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -158,6 +200,51 @@ describe Object do
158
200
  end
159
201
  after { Appsignal.config = nil }
160
202
 
203
+ context "with different kind of arguments" do
204
+ let(:klass) do
205
+ Class.new do
206
+ def self.positional_arguments(param1, param2)
207
+ [param1, param2]
208
+ end
209
+ appsignal_instrument_class_method :positional_arguments
210
+
211
+ def self.positional_arguments_splat(*params)
212
+ params
213
+ end
214
+ appsignal_instrument_class_method :positional_arguments_splat
215
+
216
+ def self.keyword_arguments(a: nil, b: nil)
217
+ [a, b]
218
+ end
219
+ appsignal_instrument_class_method :keyword_arguments
220
+
221
+ def self.keyword_arguments_splat(**kwargs)
222
+ kwargs
223
+ end
224
+ appsignal_instrument_class_method :keyword_arguments_splat
225
+
226
+ def self.splat(*args, **kwargs)
227
+ [args, kwargs]
228
+ end
229
+ appsignal_instrument_class_method :splat
230
+ end
231
+ end
232
+
233
+ it "instruments the method and calls it" do
234
+ expect(klass.positional_arguments("abc", "def")).to eq(["abc", "def"])
235
+ expect(klass.positional_arguments_splat("abc", "def")).to eq(["abc", "def"])
236
+ expect(klass.keyword_arguments(:a => "a", :b => "b")).to eq(["a", "b"])
237
+ expect(klass.keyword_arguments_splat(:a => "a", :b => "b"))
238
+ .to eq(:a => "a", :b => "b")
239
+
240
+ expect(klass.splat).to eq([[], {}])
241
+ expect(klass.splat(:a => "a", :b => "b")).to eq([[], { :a => "a", :b => "b" }])
242
+ expect(klass.splat("abc", "def")).to eq([["abc", "def"], {}])
243
+ expect(klass.splat("abc", "def", :a => "a", :b => "b"))
244
+ .to eq([["abc", "def"], { :a => "a", :b => "b" }])
245
+ end
246
+ end
247
+
161
248
  context "with anonymous class" do
162
249
  it "instruments the method and calls it" do
163
250
  expect(Appsignal.active?).to be_truthy
@@ -0,0 +1,524 @@
1
+ require "appsignal/integrations/sidekiq"
2
+
3
+ describe Appsignal::Integrations::SidekiqErrorHandler do
4
+ let(:log) { StringIO.new }
5
+ before do
6
+ start_agent
7
+ Appsignal.logger = test_logger(log)
8
+ end
9
+ around { |example| keep_transactions { example.run } }
10
+
11
+ context "without a current transction" do
12
+ let(:exception) do
13
+ begin
14
+ raise ExampleStandardError, "uh oh"
15
+ rescue => error
16
+ error
17
+ end
18
+ end
19
+ let(:job_context) do
20
+ {
21
+ :context => "Sidekiq internal error!",
22
+ :jobstr => "{ bad json }"
23
+ }
24
+ end
25
+
26
+ it "tracks error on a new transaction" do
27
+ described_class.new.call(exception, job_context)
28
+
29
+ transaction_hash = last_transaction.to_h
30
+ expect(transaction_hash["error"]).to include(
31
+ "name" => "ExampleStandardError",
32
+ "message" => "uh oh",
33
+ "backtrace" => kind_of(String)
34
+ )
35
+ expect(transaction_hash["sample_data"]).to include(
36
+ "params" => {
37
+ "jobstr" => "{ bad json }"
38
+ }
39
+ )
40
+ expect(transaction_hash["metadata"]).to include(
41
+ "sidekiq_error" => "Sidekiq internal error!"
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ describe Appsignal::Integrations::SidekiqMiddleware, :with_yaml_parse_error => false do
48
+ class DelayedTestClass; end
49
+
50
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
51
+ let(:worker) { anything }
52
+ let(:queue) { anything }
53
+ let(:given_args) do
54
+ [
55
+ "foo",
56
+ {
57
+ :foo => "Foo",
58
+ :bar => "Bar",
59
+ "baz" => { 1 => :foo }
60
+ }
61
+ ]
62
+ end
63
+ let(:expected_args) do
64
+ [
65
+ "foo",
66
+ {
67
+ "foo" => "Foo",
68
+ "bar" => "Bar",
69
+ "baz" => { "1" => "foo" }
70
+ }
71
+ ]
72
+ end
73
+ let(:job_class) { "TestClass" }
74
+ let(:jid) { "b4a577edbccf1d805744efa9" }
75
+ let(:item) do
76
+ {
77
+ "jid" => jid,
78
+ "class" => job_class,
79
+ "retry_count" => 0,
80
+ "queue" => "default",
81
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
82
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
83
+ "args" => given_args,
84
+ "extra" => "data"
85
+ }
86
+ end
87
+ let(:plugin) { Appsignal::Integrations::SidekiqMiddleware.new }
88
+ let(:log) { StringIO.new }
89
+ before do
90
+ start_agent
91
+ Appsignal.logger = test_logger(log)
92
+ end
93
+ around { |example| keep_transactions { example.run } }
94
+ after :with_yaml_parse_error => false do
95
+ expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
96
+ end
97
+
98
+ describe "internal Sidekiq job values" do
99
+ it "does not save internal Sidekiq values as metadata on transaction" do
100
+ perform_job
101
+
102
+ transaction_hash = transaction.to_h
103
+ expect(transaction_hash["metadata"].keys)
104
+ .to_not include(*Appsignal::Integrations::SidekiqMiddleware::EXCLUDED_JOB_KEYS)
105
+ end
106
+ end
107
+
108
+ context "with parameter filtering" do
109
+ before do
110
+ Appsignal.config = project_fixture_config("production")
111
+ Appsignal.config[:filter_parameters] = ["foo"]
112
+ end
113
+
114
+ it "filters selected arguments" do
115
+ perform_job
116
+
117
+ transaction_hash = transaction.to_h
118
+ expect(transaction_hash["sample_data"]).to include(
119
+ "params" => [
120
+ "foo",
121
+ {
122
+ "foo" => "[FILTERED]",
123
+ "bar" => "Bar",
124
+ "baz" => { "1" => "foo" }
125
+ }
126
+ ]
127
+ )
128
+ end
129
+ end
130
+
131
+ context "with encrypted arguments" do
132
+ before do
133
+ item["encrypt"] = true
134
+ item["args"] << "super secret value" # Last argument will be replaced
135
+ end
136
+
137
+ it "replaces the last argument (the secret bag) with an [encrypted data] string" do
138
+ perform_job
139
+
140
+ transaction_hash = transaction.to_h
141
+ expect(transaction_hash["sample_data"]).to include(
142
+ "params" => expected_args << "[encrypted data]"
143
+ )
144
+ end
145
+ end
146
+
147
+ context "when using the Sidekiq delayed extension" do
148
+ let(:item) do
149
+ {
150
+ "jid" => jid,
151
+ "class" => "Sidekiq::Extensions::DelayedClass",
152
+ "queue" => "default",
153
+ "args" => [
154
+ "---\n- !ruby/class 'DelayedTestClass'\n- :foo_method\n- - :bar: baz\n"
155
+ ],
156
+ "retry" => true,
157
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
158
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
159
+ "extra" => "data"
160
+ }
161
+ end
162
+
163
+ it "uses the delayed class and method name for the action" do
164
+ perform_job
165
+
166
+ transaction_hash = transaction.to_h
167
+ expect(transaction_hash["action"]).to eq("DelayedTestClass.foo_method")
168
+ expect(transaction_hash["sample_data"]).to include(
169
+ "params" => ["bar" => "baz"]
170
+ )
171
+ end
172
+
173
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
174
+ before { item["args"] = [] }
175
+
176
+ it "logs a warning and uses the default argument" do
177
+ perform_job
178
+
179
+ transaction_hash = transaction.to_h
180
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedClass#perform")
181
+ expect(transaction_hash["sample_data"]).to include("params" => [])
182
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
183
+ end
184
+ end
185
+ end
186
+
187
+ context "when using the Sidekiq ActiveRecord instance delayed extension" do
188
+ let(:item) do
189
+ {
190
+ "jid" => jid,
191
+ "class" => "Sidekiq::Extensions::DelayedModel",
192
+ "queue" => "default",
193
+ "args" => [
194
+ "---\n- !ruby/object:DelayedTestClass {}\n- :foo_method\n- - :bar: :baz\n"
195
+ ],
196
+ "retry" => true,
197
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
198
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
199
+ "extra" => "data"
200
+ }
201
+ end
202
+
203
+ it "uses the delayed class and method name for the action" do
204
+ perform_job
205
+
206
+ transaction_hash = transaction.to_h
207
+ expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
208
+ expect(transaction_hash["sample_data"]).to include(
209
+ "params" => ["bar" => "baz"]
210
+ )
211
+ end
212
+
213
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
214
+ before { item["args"] = [] }
215
+
216
+ it "logs a warning and uses the default argument" do
217
+ perform_job
218
+
219
+ transaction_hash = transaction.to_h
220
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedModel#perform")
221
+ expect(transaction_hash["sample_data"]).to include("params" => [])
222
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
223
+ end
224
+ end
225
+ end
226
+
227
+ context "with an error" do
228
+ let(:error) { ExampleException }
229
+
230
+ it "creates a transaction and adds the error" do
231
+ expect(Appsignal).to receive(:increment_counter)
232
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :failed)
233
+ expect(Appsignal).to receive(:increment_counter)
234
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
235
+
236
+ expect do
237
+ perform_job { raise error, "uh oh" }
238
+ end.to raise_error(error)
239
+
240
+ transaction_hash = transaction.to_h
241
+ expect(transaction_hash).to include(
242
+ "id" => jid,
243
+ "action" => "TestClass#perform",
244
+ "error" => {
245
+ "name" => "ExampleException",
246
+ "message" => "uh oh",
247
+ # TODO: backtrace should be an Array of Strings
248
+ # https://github.com/appsignal/appsignal-agent/issues/294
249
+ "backtrace" => kind_of(String)
250
+ },
251
+ "metadata" => {
252
+ "extra" => "data",
253
+ "queue" => "default",
254
+ "retry_count" => "0"
255
+ },
256
+ "namespace" => namespace,
257
+ "sample_data" => {
258
+ "environment" => {},
259
+ "params" => expected_args,
260
+ "tags" => {},
261
+ "breadcrumbs" => []
262
+ }
263
+ )
264
+ expect_transaction_to_have_sidekiq_event(transaction_hash)
265
+ end
266
+ end
267
+
268
+ context "without an error" do
269
+ it "creates a transaction with events" do
270
+ expect(Appsignal).to receive(:increment_counter)
271
+ .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
272
+
273
+ perform_job
274
+
275
+ transaction_hash = transaction.to_h
276
+ expect(transaction_hash).to include(
277
+ "id" => jid,
278
+ "action" => "TestClass#perform",
279
+ "error" => nil,
280
+ "metadata" => {
281
+ "extra" => "data",
282
+ "queue" => "default",
283
+ "retry_count" => "0"
284
+ },
285
+ "namespace" => namespace,
286
+ "sample_data" => {
287
+ "environment" => {},
288
+ "params" => expected_args,
289
+ "tags" => {},
290
+ "breadcrumbs" => []
291
+ }
292
+ )
293
+ # TODO: Not available in transaction.to_h yet.
294
+ # https://github.com/appsignal/appsignal-agent/issues/293
295
+ expect(transaction.request.env).to eq(
296
+ :queue_start => Time.parse("2001-01-01 10:00:00UTC").to_f
297
+ )
298
+ expect_transaction_to_have_sidekiq_event(transaction_hash)
299
+ end
300
+ end
301
+
302
+ def perform_job
303
+ Timecop.freeze(Time.parse("2001-01-01 10:01:00UTC")) do
304
+ begin
305
+ exception = nil
306
+ plugin.call(worker, item, queue) do
307
+ yield if block_given?
308
+ end
309
+ rescue Exception => exception # rubocop:disable Lint/RescueException
310
+ raise exception
311
+ ensure
312
+ if exception
313
+ Appsignal::Integrations::SidekiqErrorHandler.new.call(exception, :job => item)
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def transaction
320
+ last_transaction
321
+ end
322
+
323
+ def expect_transaction_to_have_sidekiq_event(transaction_hash)
324
+ events = transaction_hash["events"]
325
+ expect(events.count).to eq(1)
326
+ expect(events.first).to include(
327
+ "name" => "perform_job.sidekiq",
328
+ "title" => "",
329
+ "count" => 1,
330
+ "body" => "",
331
+ "body_format" => Appsignal::EventFormatter::DEFAULT
332
+ )
333
+ end
334
+ end
335
+
336
+ if DependencyHelper.active_job_present?
337
+ require "active_job"
338
+ require "action_mailer"
339
+ require "sidekiq/testing"
340
+
341
+ describe "Sidekiq ActiveJob integration" do
342
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
343
+ let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
344
+ let(:log) { StringIO.new }
345
+ let(:given_args) do
346
+ [
347
+ "foo",
348
+ {
349
+ :foo => "Foo",
350
+ "bar" => "Bar",
351
+ "baz" => { "1" => "foo" }
352
+ }
353
+ ]
354
+ end
355
+ let(:expected_args) do
356
+ [
357
+ "foo",
358
+ {
359
+ "_aj_symbol_keys" => ["foo"],
360
+ "foo" => "Foo",
361
+ "bar" => "Bar",
362
+ "baz" => {
363
+ "_aj_symbol_keys" => [],
364
+ "1" => "foo"
365
+ }
366
+ }
367
+ ]
368
+ end
369
+ let(:expected_tags) do
370
+ {}.tap do |hash|
371
+ hash["active_job_id"] = kind_of(String)
372
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
373
+ hash["provider_job_id"] = kind_of(String)
374
+ end
375
+ end
376
+ end
377
+ before do
378
+ start_agent
379
+ Appsignal.logger = test_logger(log)
380
+ ActiveJob::Base.queue_adapter = :sidekiq
381
+
382
+ class ActiveJobSidekiqTestJob < ActiveJob::Base
383
+ self.queue_adapter = :sidekiq
384
+
385
+ def perform(*_args)
386
+ end
387
+ end
388
+
389
+ class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
390
+ self.queue_adapter = :sidekiq
391
+
392
+ def perform(*_args)
393
+ raise "uh oh"
394
+ end
395
+ end
396
+ # Manually add the AppSignal middleware for the Testing environment.
397
+ # It doesn't use configured middlewares by default looks like.
398
+ # We test somewhere else if the middleware is installed properly.
399
+ Sidekiq::Testing.server_middleware do |chain|
400
+ chain.add Appsignal::Integrations::SidekiqMiddleware
401
+ end
402
+ end
403
+ around do |example|
404
+ keep_transactions do
405
+ Sidekiq::Testing.fake! do
406
+ example.run
407
+ end
408
+ end
409
+ end
410
+ after do
411
+ Object.send(:remove_const, :ActiveJobSidekiqTestJob)
412
+ Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
413
+ end
414
+
415
+ it "reports the transaction from the ActiveJob integration" do
416
+ perform_job(ActiveJobSidekiqTestJob, given_args)
417
+
418
+ transaction = last_transaction
419
+ transaction_hash = transaction.to_h
420
+ expect(transaction_hash).to include(
421
+ "action" => "ActiveJobSidekiqTestJob#perform",
422
+ "error" => nil,
423
+ "namespace" => namespace,
424
+ "metadata" => hash_including(
425
+ "queue" => "default"
426
+ ),
427
+ "sample_data" => hash_including(
428
+ "environment" => {},
429
+ "params" => [expected_args],
430
+ "tags" => expected_tags.merge("queue" => "default")
431
+ )
432
+ )
433
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
434
+ events = transaction_hash["events"]
435
+ .sort_by { |e| e["start"] }
436
+ .map { |event| event["name"] }
437
+ expect(events)
438
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
439
+ end
440
+
441
+ context "with error" do
442
+ it "reports the error on the transaction from the ActiveRecord integration" do
443
+ expect do
444
+ perform_job(ActiveJobSidekiqErrorTestJob, given_args)
445
+ end.to raise_error(RuntimeError, "uh oh")
446
+
447
+ transaction = last_transaction
448
+ transaction_hash = transaction.to_h
449
+ expect(transaction_hash).to include(
450
+ "action" => "ActiveJobSidekiqErrorTestJob#perform",
451
+ "error" => {
452
+ "name" => "RuntimeError",
453
+ "message" => "uh oh",
454
+ "backtrace" => kind_of(String)
455
+ },
456
+ "namespace" => namespace,
457
+ "metadata" => hash_including(
458
+ "queue" => "default"
459
+ ),
460
+ "sample_data" => hash_including(
461
+ "environment" => {},
462
+ "params" => [expected_args],
463
+ "tags" => expected_tags.merge("queue" => "default")
464
+ )
465
+ )
466
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
467
+ events = transaction_hash["events"]
468
+ .sort_by { |e| e["start"] }
469
+ .map { |event| event["name"] }
470
+ expect(events)
471
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
472
+ end
473
+ end
474
+
475
+ context "with ActionMailer" do
476
+ include ActionMailerHelpers
477
+
478
+ before do
479
+ class ActionMailerSidekiqTestJob < ActionMailer::Base
480
+ def welcome(*args)
481
+ end
482
+ end
483
+ end
484
+
485
+ it "reports ActionMailer data on the transaction" do
486
+ perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
487
+
488
+ transaction = last_transaction
489
+ transaction_hash = transaction.to_h
490
+ expect(transaction_hash).to include(
491
+ "action" => "ActionMailerSidekiqTestJob#welcome",
492
+ "sample_data" => hash_including(
493
+ "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
494
+ )
495
+ )
496
+ end
497
+ end
498
+
499
+ def perform_sidekiq
500
+ Timecop.freeze(time) do
501
+ begin
502
+ yield
503
+ # Combined with Sidekiq::Testing.fake! and drain_all we get a
504
+ # enqueue_at in the job data.
505
+ Sidekiq::Worker.drain_all
506
+ rescue Exception => exception # rubocop:disable Lint/RescueException
507
+ raise exception
508
+ ensure
509
+ if exception
510
+ Appsignal::Integrations::SidekiqErrorHandler.new.call(exception, {})
511
+ end
512
+ end
513
+ end
514
+ end
515
+
516
+ def perform_job(job_class, args)
517
+ perform_sidekiq { job_class.perform_later(args) }
518
+ end
519
+
520
+ def perform_mailer(mailer, method, args = nil)
521
+ perform_sidekiq { perform_action_mailer(mailer, method, args) }
522
+ end
523
+ end
524
+ end