appsignal 2.11.0.alpha.2 → 2.11.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +94 -10
  4. data/CHANGELOG.md +31 -1
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +7 -3
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/base.rb +7 -0
  12. data/ext/extconf.rb +2 -0
  13. data/gemfiles/rails-4.2.gemfile +9 -2
  14. data/gemfiles/rails-5.0.gemfile +1 -0
  15. data/gemfiles/rails-5.1.gemfile +1 -0
  16. data/gemfiles/rails-5.2.gemfile +1 -0
  17. data/gemfiles/rails-6.0.gemfile +1 -0
  18. data/gemfiles/resque-1.gemfile +7 -0
  19. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  20. data/lib/appsignal.rb +1 -0
  21. data/lib/appsignal/auth_check.rb +4 -2
  22. data/lib/appsignal/cli/diagnose.rb +1 -1
  23. data/lib/appsignal/config.rb +35 -2
  24. data/lib/appsignal/extension.rb +6 -5
  25. data/lib/appsignal/extension/jruby.rb +6 -5
  26. data/lib/appsignal/hooks.rb +25 -0
  27. data/lib/appsignal/hooks/active_job.rb +137 -0
  28. data/lib/appsignal/hooks/puma.rb +0 -1
  29. data/lib/appsignal/hooks/resque.rb +60 -0
  30. data/lib/appsignal/hooks/sidekiq.rb +17 -94
  31. data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
  32. data/lib/appsignal/integrations/que.rb +1 -1
  33. data/lib/appsignal/integrations/resque.rb +9 -12
  34. data/lib/appsignal/integrations/resque_active_job.rb +9 -32
  35. data/lib/appsignal/probes.rb +7 -0
  36. data/lib/appsignal/probes/puma.rb +1 -1
  37. data/lib/appsignal/probes/sidekiq.rb +3 -1
  38. data/lib/appsignal/transaction.rb +10 -0
  39. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  40. data/lib/appsignal/version.rb +1 -1
  41. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  42. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  43. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  44. data/spec/lib/appsignal/cli/diagnose_spec.rb +42 -0
  45. data/spec/lib/appsignal/config_spec.rb +21 -0
  46. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  47. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  48. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  49. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +3 -14
  50. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  51. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +222 -268
  52. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  53. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  54. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -179
  55. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  56. data/spec/lib/appsignal/marker_spec.rb +1 -1
  57. data/spec/lib/appsignal/probes/sidekiq_spec.rb +10 -7
  58. data/spec/lib/appsignal/transaction_spec.rb +5 -7
  59. data/spec/spec_helper.rb +5 -0
  60. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  61. data/spec/support/helpers/config_helpers.rb +3 -2
  62. data/spec/support/helpers/dependency_helper.rb +9 -2
  63. data/spec/support/helpers/transaction_helpers.rb +6 -0
  64. data/spec/support/stubs/sidekiq/api.rb +1 -1
  65. data/spec/support/testing.rb +19 -19
  66. metadata +16 -4
@@ -227,25 +227,14 @@ describe Appsignal::Hooks::DelayedJobHook do
227
227
  end
228
228
  end
229
229
 
230
- context "without job name" do
231
- let(:job_data) do
232
- { :name => "", :payload_object => payload_object }
233
- end
234
-
235
- it "wraps it in a transaction using the class method job name" do
236
- perform
237
- expect(last_transaction.to_h["action"]).to eql("unknown")
238
- end
239
- end
240
-
241
- context "with invalid job name" do
230
+ context "with only job class name" do
242
231
  let(:job_data) do
243
232
  { :name => "Banana", :payload_object => payload_object }
244
233
  end
245
234
 
246
- it "wraps it in a transaction using the class method job name" do
235
+ it "appends #perform to the class name" do
247
236
  perform
248
- expect(last_transaction.to_h["action"]).to eql("unknown")
237
+ expect(last_transaction.to_h["action"]).to eql("Banana#perform")
249
238
  end
250
239
  end
251
240
 
@@ -0,0 +1,185 @@
1
+ describe Appsignal::Hooks::ResqueHook do
2
+ describe "#dependency_present?" do
3
+ subject { described_class.new.dependencies_present? }
4
+
5
+ context "when Resque is loaded" do
6
+ before { stub_const "Resque", 1 }
7
+
8
+ it { is_expected.to be_truthy }
9
+ end
10
+
11
+ context "when Resque is not loaded" do
12
+ before { hide_const "Resque" }
13
+
14
+ it { is_expected.to be_falsy }
15
+ end
16
+ end
17
+
18
+ if DependencyHelper.resque_present?
19
+ describe "#install" do
20
+ def perform_job(klass, options = {})
21
+ payload = { "class" => klass.to_s }.merge(options)
22
+ job = ::Resque::Job.new(queue, payload)
23
+ keep_transactions { job.perform }
24
+ end
25
+
26
+ let(:queue) { "default" }
27
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
28
+ before do
29
+ start_agent
30
+
31
+ class ResqueTestJob
32
+ def self.perform(*_args)
33
+ end
34
+ end
35
+
36
+ class ResqueErrorTestJob
37
+ def self.perform
38
+ raise "resque job error"
39
+ end
40
+ end
41
+
42
+ expect(Appsignal).to receive(:stop) # Resque calls stop after every job
43
+ end
44
+ around do |example|
45
+ keep_transactions { example.run }
46
+ end
47
+ after do
48
+ Object.send(:remove_const, :ResqueTestJob)
49
+ Object.send(:remove_const, :ResqueErrorTestJob)
50
+ end
51
+
52
+ it "tracks a transaction on perform" do
53
+ perform_job(ResqueTestJob)
54
+
55
+ transaction = last_transaction
56
+ transaction_hash = transaction.to_h
57
+ expect(transaction_hash).to include(
58
+ "id" => kind_of(String),
59
+ "action" => "ResqueTestJob#perform",
60
+ "error" => nil,
61
+ "namespace" => namespace,
62
+ "metadata" => {},
63
+ "sample_data" => { "tags" => { "queue" => queue } }
64
+ )
65
+ expect(transaction_hash["events"].map { |e| e["name"] })
66
+ .to eql(["perform.resque"])
67
+ end
68
+
69
+ context "with error" do
70
+ it "tracks the error on the transaction" do
71
+ expect do
72
+ perform_job(ResqueErrorTestJob)
73
+ end.to raise_error(RuntimeError, "resque job error")
74
+
75
+ transaction = last_transaction
76
+ transaction_hash = transaction.to_h
77
+ expect(transaction_hash).to include(
78
+ "id" => kind_of(String),
79
+ "action" => "ResqueErrorTestJob#perform",
80
+ "error" => {
81
+ "name" => "RuntimeError",
82
+ "message" => "resque job error",
83
+ "backtrace" => kind_of(String)
84
+ },
85
+ "namespace" => namespace,
86
+ "metadata" => {},
87
+ "sample_data" => { "tags" => { "queue" => queue } }
88
+ )
89
+ end
90
+ end
91
+
92
+ context "with arguments" do
93
+ before do
94
+ Appsignal.config = project_fixture_config("production")
95
+ Appsignal.config[:filter_parameters] = ["foo"]
96
+ end
97
+
98
+ it "filters out configured arguments" do
99
+ perform_job(
100
+ ResqueTestJob,
101
+ "args" => [
102
+ "foo",
103
+ {
104
+ "foo" => "secret",
105
+ "bar" => "Bar",
106
+ "baz" => { "1" => "foo" }
107
+ }
108
+ ]
109
+ )
110
+
111
+ transaction = last_transaction
112
+ transaction_hash = transaction.to_h
113
+ expect(transaction_hash).to include(
114
+ "id" => kind_of(String),
115
+ "action" => "ResqueTestJob#perform",
116
+ "error" => nil,
117
+ "namespace" => namespace,
118
+ "metadata" => {},
119
+ "sample_data" => {
120
+ "tags" => { "queue" => queue },
121
+ "params" => [
122
+ "foo",
123
+ {
124
+ "foo" => "[FILTERED]",
125
+ "bar" => "Bar",
126
+ "baz" => { "1" => "foo" }
127
+ }
128
+ ]
129
+ }
130
+ )
131
+ end
132
+ end
133
+
134
+ context "with active job" do
135
+ before do
136
+ module ActiveJobMock
137
+ module QueueAdapters
138
+ module ResqueAdapter
139
+ module JobWrapper
140
+ class << self
141
+ def perform(job_data)
142
+ # Basic ActiveJob stub for this test.
143
+ # I haven't found a way to run Resque in a testing mode.
144
+ Appsignal.set_action(job_data["job_class"])
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ stub_const "ActiveJob", ActiveJobMock
153
+ end
154
+ after { Object.send(:remove_const, :ActiveJobMock) }
155
+
156
+ it "does not set arguments but lets the ActiveJob intergration handle it" do
157
+ perform_job(
158
+ ResqueTestJob,
159
+ "class" => "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper",
160
+ "args" => [
161
+ {
162
+ "job_class" => "ResqueTestJobByActiveJob#perform",
163
+ "arguments" => ["an argument", "second argument"]
164
+ }
165
+ ]
166
+ )
167
+
168
+ transaction = last_transaction
169
+ transaction_hash = transaction.to_h
170
+ expect(transaction_hash).to include(
171
+ "id" => kind_of(String),
172
+ "action" => "ResqueTestJobByActiveJob#perform",
173
+ "error" => nil,
174
+ "namespace" => namespace,
175
+ "metadata" => {},
176
+ "sample_data" => {
177
+ "tags" => { "queue" => queue }
178
+ # Params will be set by the ActiveJob integration
179
+ }
180
+ )
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -1,3 +1,54 @@
1
+ describe Appsignal::Hooks::SidekiqHook do
2
+ describe "#dependencies_present?" do
3
+ subject { described_class.new.dependencies_present? }
4
+
5
+ context "when Sidekiq constant is found" do
6
+ before { stub_const "Sidekiq", Class.new }
7
+
8
+ it { is_expected.to be_truthy }
9
+ end
10
+
11
+ context "when Sidekiq constant is not found" do
12
+ before { hide_const "Sidekiq" }
13
+
14
+ it { is_expected.to be_falsy }
15
+ end
16
+ end
17
+
18
+ describe "#install" do
19
+ class SidekiqMiddlewareMock < Set
20
+ def exists?(middleware)
21
+ include?(middleware)
22
+ end
23
+ end
24
+ module SidekiqMock
25
+ def self.middlewares
26
+ @middlewares ||= SidekiqMiddlewareMock.new
27
+ end
28
+
29
+ def self.configure_server
30
+ yield self
31
+ end
32
+
33
+ def self.server_middleware
34
+ yield middlewares if block_given?
35
+ middlewares
36
+ end
37
+ end
38
+
39
+ before do
40
+ Appsignal.config = project_fixture_config
41
+ stub_const "Sidekiq", SidekiqMock
42
+ end
43
+
44
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
45
+ described_class.new.install
46
+
47
+ expect(Sidekiq.server_middleware.exists?(Appsignal::Hooks::SidekiqPlugin)).to be(true)
48
+ end
49
+ end
50
+ end
51
+
1
52
  describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
2
53
  class DelayedTestClass; end
3
54
 
@@ -25,9 +76,10 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
25
76
  ]
26
77
  end
27
78
  let(:job_class) { "TestClass" }
79
+ let(:jid) { "b4a577edbccf1d805744efa9" }
28
80
  let(:item) do
29
81
  {
30
- "jid" => "b4a577edbccf1d805744efa9",
82
+ "jid" => jid,
31
83
  "class" => job_class,
32
84
  "retry_count" => 0,
33
85
  "queue" => "default",
@@ -48,37 +100,13 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
48
100
  expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
49
101
  end
50
102
 
51
- shared_examples "unknown job action name" do
52
- it "sets the action name to unknown" do
53
- transaction_hash = transaction.to_h
54
- expect(transaction_hash).to include("action" => "unknown")
55
- end
56
-
57
- it "stores no sample data" do
58
- transaction_hash = transaction.to_h
59
- expect(transaction_hash).to include(
60
- "sample_data" => {
61
- "environment" => {},
62
- "params" => [],
63
- "tags" => {}
64
- }
65
- )
66
- end
67
-
68
- it "logs a debug message" do
69
- expect(log_contents(log)).to contains_log(
70
- :debug, "Unable to determine an action name from Sidekiq payload: #{item}"
71
- )
72
- end
73
- end
74
-
75
103
  describe "internal Sidekiq job values" do
76
104
  it "does not save internal Sidekiq values as metadata on transaction" do
77
105
  perform_job
78
106
 
79
107
  transaction_hash = transaction.to_h
80
108
  expect(transaction_hash["metadata"].keys)
81
- .to_not include(*Appsignal::Hooks::SidekiqPlugin::JOB_KEYS)
109
+ .to_not include(*Appsignal::Hooks::SidekiqPlugin::EXCLUDED_JOB_KEYS)
82
110
  end
83
111
  end
84
112
 
@@ -124,7 +152,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
124
152
  context "when using the Sidekiq delayed extension" do
125
153
  let(:item) do
126
154
  {
127
- "jid" => "efb140489485999d32b5504c",
155
+ "jid" => jid,
128
156
  "class" => "Sidekiq::Extensions::DelayedClass",
129
157
  "queue" => "default",
130
158
  "args" => [
@@ -164,7 +192,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
164
192
  context "when using the Sidekiq ActiveRecord instance delayed extension" do
165
193
  let(:item) do
166
194
  {
167
- "jid" => "efb140489485999d32b5504c",
195
+ "jid" => jid,
168
196
  "class" => "Sidekiq::Extensions::DelayedModel",
169
197
  "queue" => "default",
170
198
  "args" => [
@@ -201,217 +229,6 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
201
229
  end
202
230
  end
203
231
 
204
- context "when using ActiveJob" do
205
- let(:item) do
206
- {
207
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
208
- "wrapped" => "ActiveJobTestClass",
209
- "queue" => "default",
210
- "args" => [{
211
- "job_class" => "ActiveJobTestJob",
212
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
213
- "queue_name" => "default",
214
- "arguments" => [
215
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
216
- ]
217
- }],
218
- "retry" => true,
219
- "jid" => "efb140489485999d32b5504c",
220
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
221
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
222
- }
223
- end
224
-
225
- it "creates a transaction with events" do
226
- perform_job
227
-
228
- transaction_hash = transaction.to_h
229
- expect(transaction_hash).to include(
230
- "id" => kind_of(String),
231
- "action" => "ActiveJobTestClass#perform",
232
- "error" => nil,
233
- "namespace" => namespace,
234
- "metadata" => {
235
- "queue" => "default"
236
- },
237
- "sample_data" => {
238
- "environment" => {},
239
- "params" => [
240
- "foo",
241
- {
242
- "foo" => "Foo",
243
- "bar" => "Bar",
244
- "baz" => { "1" => "bar" }
245
- }
246
- ],
247
- "tags" => {}
248
- }
249
- )
250
- # TODO: Not available in transaction.to_h yet.
251
- # https://github.com/appsignal/appsignal-agent/issues/293
252
- expect(transaction.request.env).to eq(
253
- :queue_start => Time.parse("2001-01-01 10:00:00UTC").to_f
254
- )
255
- expect_transaction_to_have_sidekiq_event(transaction_hash)
256
- end
257
-
258
- context "with ActionMailer job" do
259
- let(:item) do
260
- {
261
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
262
- "wrapped" => "ActionMailer::DeliveryJob",
263
- "queue" => "default",
264
- "args" => [{
265
- "job_class" => "ActiveMailerTestJob",
266
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
267
- "queue_name" => "default",
268
- "arguments" => [
269
- "MailerClass", "mailer_method", "deliver_now",
270
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
271
- ]
272
- }],
273
- "retry" => true,
274
- "jid" => "efb140489485999d32b5504c",
275
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
276
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
277
- }
278
- end
279
-
280
- it "creates a transaction for the ActionMailer class" do
281
- perform_job
282
-
283
- transaction_hash = transaction.to_h
284
- expect(transaction_hash).to include(
285
- "id" => kind_of(String),
286
- "action" => "MailerClass#mailer_method",
287
- "error" => nil,
288
- "namespace" => namespace,
289
- "metadata" => {
290
- "queue" => "default"
291
- },
292
- "sample_data" => {
293
- "environment" => {},
294
- "params" => [
295
- "foo",
296
- {
297
- "foo" => "Foo",
298
- "bar" => "Bar",
299
- "baz" => { "1" => "bar" }
300
- }
301
- ],
302
- "tags" => {}
303
- }
304
- )
305
- end
306
- end
307
-
308
- context "with parameter filtering" do
309
- before do
310
- Appsignal.config = project_fixture_config("production")
311
- Appsignal.config[:filter_parameters] = ["foo"]
312
- end
313
-
314
- it "filters selected arguments" do
315
- perform_job
316
-
317
- transaction_hash = transaction.to_h
318
- expect(transaction_hash["sample_data"]).to include(
319
- "params" => [
320
- "foo",
321
- {
322
- "foo" => "[FILTERED]",
323
- "bar" => "Bar",
324
- "baz" => { "1" => "bar" }
325
- }
326
- ]
327
- )
328
- end
329
- end
330
-
331
- context "when Sidekiq job payload is missing the 'wrapped' value" do
332
- let(:item) do
333
- {
334
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
335
- "queue" => "default",
336
- "args" => [first_argument],
337
- "retry" => true,
338
- "jid" => "efb140489485999d32b5504c",
339
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
340
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
341
- }
342
- end
343
- before { perform_job }
344
-
345
- context "when the first argument is not a Hash object" do
346
- let(:first_argument) { "foo" }
347
-
348
- include_examples "unknown job action name"
349
- end
350
-
351
- context "when the first argument is a Hash object not containing a job payload" do
352
- let(:first_argument) { { "foo" => "bar" } }
353
-
354
- include_examples "unknown job action name"
355
-
356
- context "when the argument contains an invalid job_class value" do
357
- let(:first_argument) { { "job_class" => :foo } }
358
-
359
- include_examples "unknown job action name"
360
- end
361
- end
362
-
363
- context "when the first argument is a Hash object containing a job payload" do
364
- let(:first_argument) do
365
- {
366
- "job_class" => "ActiveMailerTestJob",
367
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
368
- "queue_name" => "default",
369
- "arguments" => [
370
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
371
- ]
372
- }
373
- end
374
-
375
- it "sets the action name to the job class in the first argument" do
376
- transaction_hash = transaction.to_h
377
- expect(transaction_hash).to include(
378
- "action" => "ActiveMailerTestJob#perform"
379
- )
380
- end
381
-
382
- it "stores the job metadata on the transaction" do
383
- transaction_hash = transaction.to_h
384
- expect(transaction_hash).to include(
385
- "id" => kind_of(String),
386
- "error" => nil,
387
- "namespace" => namespace,
388
- "metadata" => {
389
- "queue" => "default"
390
- },
391
- "sample_data" => {
392
- "environment" => {},
393
- "params" => [
394
- "foo",
395
- {
396
- "foo" => "Foo",
397
- "bar" => "Bar",
398
- "baz" => { "1" => "bar" }
399
- }
400
- ],
401
- "tags" => {}
402
- }
403
- )
404
- end
405
-
406
- it "does not log a debug message" do
407
- expect(log_contents(log)).to_not contains_log(
408
- :debug, "Unable to determine an action name from Sidekiq payload"
409
- )
410
- end
411
- end
412
- end
413
- end
414
-
415
232
  context "with an error" do
416
233
  let(:error) { ExampleException }
417
234
 
@@ -427,7 +244,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
427
244
 
428
245
  transaction_hash = transaction.to_h
429
246
  expect(transaction_hash).to include(
430
- "id" => kind_of(String),
247
+ "id" => jid,
431
248
  "action" => "TestClass#perform",
432
249
  "error" => {
433
250
  "name" => "ExampleException",
@@ -461,7 +278,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
461
278
 
462
279
  transaction_hash = transaction.to_h
463
280
  expect(transaction_hash).to include(
464
- "id" => kind_of(String),
281
+ "id" => jid,
465
282
  "action" => "TestClass#perform",
466
283
  "error" => nil,
467
284
  "metadata" => {
@@ -510,47 +327,184 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
510
327
  end
511
328
  end
512
329
 
513
- describe Appsignal::Hooks::SidekiqHook do
514
- describe "#dependencies_present?" do
515
- subject { described_class.new.dependencies_present? }
330
+ if DependencyHelper.active_job_present?
331
+ require "active_job"
332
+ require "action_mailer"
333
+ require "sidekiq/testing"
334
+
335
+ describe "Sidekiq ActiveJob integration" do
336
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
337
+ let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
338
+ let(:log) { StringIO.new }
339
+ let(:given_args) do
340
+ [
341
+ "foo",
342
+ {
343
+ :foo => "Foo",
344
+ "bar" => "Bar",
345
+ "baz" => { "1" => "foo" }
346
+ }
347
+ ]
348
+ end
349
+ let(:expected_args) do
350
+ [
351
+ "foo",
352
+ {
353
+ "_aj_symbol_keys" => ["foo"],
354
+ "foo" => "Foo",
355
+ "bar" => "Bar",
356
+ "baz" => {
357
+ "_aj_symbol_keys" => [],
358
+ "1" => "foo"
359
+ }
360
+ }
361
+ ]
362
+ end
363
+ let(:expected_tags) do
364
+ {}.tap do |hash|
365
+ hash["active_job_id"] = kind_of(String)
366
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
367
+ hash["provider_job_id"] = kind_of(String)
368
+ end
369
+ end
370
+ end
371
+ before do
372
+ start_agent
373
+ Appsignal.logger = test_logger(log)
374
+ ActiveJob::Base.queue_adapter = :sidekiq
516
375
 
517
- context "when Sidekiq constant is found" do
518
- before { Object.const_set("Sidekiq", 1) }
519
- after { Object.send(:remove_const, "Sidekiq") }
376
+ class ActiveJobSidekiqTestJob < ActiveJob::Base
377
+ self.queue_adapter = :sidekiq
520
378
 
521
- it { is_expected.to be_truthy }
379
+ def perform(*_args)
380
+ end
381
+ end
382
+
383
+ class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
384
+ self.queue_adapter = :sidekiq
385
+
386
+ def perform(*_args)
387
+ raise "uh oh"
388
+ end
389
+ end
390
+ # Manually add the AppSignal middleware for the Testing environment.
391
+ # It doesn't use configured middlewares by default looks like.
392
+ # We test somewhere else if the middleware is installed properly.
393
+ Sidekiq::Testing.server_middleware do |chain|
394
+ chain.add Appsignal::Hooks::SidekiqPlugin
395
+ end
396
+ end
397
+ around do |example|
398
+ keep_transactions do
399
+ Sidekiq::Testing.fake! do
400
+ example.run
401
+ end
402
+ end
403
+ end
404
+ after do
405
+ Object.send(:remove_const, :ActiveJobSidekiqTestJob)
406
+ Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
522
407
  end
523
408
 
524
- context "when Sidekiq constant is not found" do
525
- before { Object.send(:remove_const, "Sidekiq") if defined?(Sidekiq) }
409
+ it "reports the transaction from the ActiveJob integration" do
410
+ perform_job(ActiveJobSidekiqTestJob, given_args)
526
411
 
527
- it { is_expected.to be_falsy }
412
+ transaction = last_transaction
413
+ transaction_hash = transaction.to_h
414
+ expect(transaction_hash).to include(
415
+ "action" => "ActiveJobSidekiqTestJob#perform",
416
+ "error" => nil,
417
+ "namespace" => namespace,
418
+ "metadata" => hash_including(
419
+ "queue" => "default"
420
+ ),
421
+ "sample_data" => hash_including(
422
+ "environment" => {},
423
+ "params" => [expected_args],
424
+ "tags" => expected_tags.merge("queue" => "default")
425
+ )
426
+ )
427
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
428
+ events = transaction_hash["events"]
429
+ .sort_by { |e| e["start"] }
430
+ .map { |event| event["name"] }
431
+ expect(events)
432
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
528
433
  end
529
- end
530
434
 
531
- describe "#install" do
532
- before do
533
- Appsignal.config = project_fixture_config
534
- module Sidekiq
535
- def self.middlewares
536
- @middlewares ||= Set.new
537
- end
435
+ context "with error" do
436
+ it "reports the error on the transaction from the ActiveRecord integration" do
437
+ expect do
438
+ perform_job(ActiveJobSidekiqErrorTestJob, given_args)
439
+ end.to raise_error(RuntimeError, "uh oh")
538
440
 
539
- def self.configure_server
540
- yield self
541
- end
441
+ transaction = last_transaction
442
+ transaction_hash = transaction.to_h
443
+ expect(transaction_hash).to include(
444
+ "action" => "ActiveJobSidekiqErrorTestJob#perform",
445
+ "error" => {
446
+ "name" => "RuntimeError",
447
+ "message" => "uh oh",
448
+ "backtrace" => kind_of(String)
449
+ },
450
+ "namespace" => namespace,
451
+ "metadata" => hash_including(
452
+ "queue" => "default"
453
+ ),
454
+ "sample_data" => hash_including(
455
+ "environment" => {},
456
+ "params" => [expected_args],
457
+ "tags" => expected_tags.merge("queue" => "default")
458
+ )
459
+ )
460
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
461
+ events = transaction_hash["events"]
462
+ .sort_by { |e| e["start"] }
463
+ .map { |event| event["name"] }
464
+ expect(events)
465
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
466
+ end
467
+ end
468
+
469
+ context "with ActionMailer" do
470
+ include ActionMailerHelpers
542
471
 
543
- def self.server_middleware
544
- yield middlewares
472
+ before do
473
+ class ActionMailerSidekiqTestJob < ActionMailer::Base
474
+ def welcome(*args)
475
+ end
545
476
  end
546
477
  end
478
+
479
+ it "reports ActionMailer data on the transaction" do
480
+ perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
481
+
482
+ transaction = last_transaction
483
+ transaction_hash = transaction.to_h
484
+ expect(transaction_hash).to include(
485
+ "action" => "ActionMailerSidekiqTestJob#welcome",
486
+ "sample_data" => hash_including(
487
+ "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
488
+ )
489
+ )
490
+ end
547
491
  end
548
- after { Object.send(:remove_const, "Sidekiq") }
549
492
 
550
- it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
551
- described_class.new.install
493
+ def perform_sidekiq
494
+ Timecop.freeze(time) do
495
+ yield
496
+ # Combined with Sidekiq::Testing.fake! and drain_all we get a
497
+ # enqueue_at in the job data.
498
+ Sidekiq::Worker.drain_all
499
+ end
500
+ end
501
+
502
+ def perform_job(job_class, args)
503
+ perform_sidekiq { job_class.perform_later(args) }
504
+ end
552
505
 
553
- expect(Sidekiq.middlewares).to include(Appsignal::Hooks::SidekiqPlugin)
506
+ def perform_mailer(mailer, method, args = nil)
507
+ perform_sidekiq { perform_action_mailer(mailer, method, args) }
554
508
  end
555
509
  end
556
510
  end