appsignal 3.0.0.beta.1 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
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