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
@@ -123,6 +123,12 @@ module Appsignal
123
123
  def start
124
124
  stop
125
125
  @thread = Thread.new do
126
+ # Advise multi-threaded app servers to ignore this thread
127
+ # for the purposes of fork safety warnings
128
+ if Thread.current.respond_to?(:thread_variable_set)
129
+ Thread.current.thread_variable_set(:fork_safe, true)
130
+ end
131
+
126
132
  sleep initial_wait_time
127
133
  initialize_probes
128
134
  loop do
@@ -90,7 +90,7 @@ module Appsignal
90
90
  @transaction_id,
91
91
  @namespace,
92
92
  self.class.garbage_collection_profiler.total_time
93
- )
93
+ ) || Appsignal::Extension::MockTransaction.new
94
94
  end
95
95
 
96
96
  def nil_transaction?
@@ -228,7 +228,7 @@ module Appsignal
228
228
  # "Web" and "background_job" gets transformed to "Background".
229
229
  #
230
230
  # @example
231
- # transaction.set_action("admin")
231
+ # transaction.set_namespace("background")
232
232
  #
233
233
  # @param namespace [String] namespace name to use for this transaction.
234
234
  # @return [void]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.0.0.beta.1".freeze
4
+ VERSION = "3.0.3".freeze
5
5
  end
@@ -374,6 +374,7 @@ describe Appsignal::Config do
374
374
  :active => true,
375
375
  :name => "App name",
376
376
  :debug => true,
377
+ :dns_servers => ["8.8.8.8", "8.8.4.4"],
377
378
  :ignore_actions => %w[action1 action2],
378
379
  :ignore_errors => %w[ExampleStandardError AnotherError],
379
380
  :ignore_namespaces => %w[admin private_namespace],
@@ -393,6 +394,7 @@ describe Appsignal::Config do
393
394
  ENV["APPSIGNAL_ACTIVE"] = "true"
394
395
  ENV["APPSIGNAL_APP_NAME"] = "App name"
395
396
  ENV["APPSIGNAL_DEBUG"] = "true"
397
+ ENV["APPSIGNAL_DNS_SERVERS"] = "8.8.8.8,8.8.4.4"
396
398
  ENV["APPSIGNAL_IGNORE_ACTIONS"] = "action1,action2"
397
399
  ENV["APPSIGNAL_IGNORE_ERRORS"] = "ExampleStandardError,AnotherError"
398
400
  ENV["APPSIGNAL_IGNORE_NAMESPACES"] = "admin,private_namespace"
@@ -1,13 +1,6 @@
1
1
  describe Appsignal::Extension, :extension_installation_failure do
2
2
  context "when the extension library cannot be loaded" do
3
- # This test breaks the installation on purpose and is not run by default.
4
- # See `rake test:failure`. If this test was run, run `rake
5
- # extension:install` again to fix the extension installation.
6
3
  it "prints and logs an error" do
7
- # ENV var to make sure installation fails on purpurse
8
- ENV["_TEST_APPSIGNAL_EXTENSION_FAILURE"] = "true"
9
- `rake extension:install` # Run installation
10
-
11
4
  require "open3"
12
5
  _stdout, stderr, _status = Open3.capture3("bin/appsignal --version")
13
6
  expect(stderr).to include("ERROR: AppSignal failed to load extension")
@@ -119,22 +119,56 @@ describe Appsignal::Extension do
119
119
  end
120
120
  end
121
121
 
122
- context "when the extension library cannot be loaded" do
123
- subject { Appsignal::Extension }
122
+ context "when the extension library cannot be loaded", :extension_installation_failure do
123
+ let(:ext) { Appsignal::Extension }
124
124
 
125
- before do
126
- allow(Appsignal).to receive(:extension_loaded).and_return(false)
127
- allow(Appsignal).to receive(:testing?).and_return(false)
125
+ around do |example|
126
+ Appsignal::Testing.without_testing { example.run }
128
127
  end
129
128
 
130
129
  it "should indicate that the extension is not loaded" do
131
130
  expect(Appsignal.extension_loaded?).to be_falsy
132
131
  end
133
132
 
134
- it "should not raise errors when methods are called" do
135
- expect do
136
- subject.something
137
- end.not_to raise_error
133
+ it "does not raise errors when methods are called" do
134
+ ext.appsignal_start
135
+ ext.something
136
+ end
137
+
138
+ describe Appsignal::Extension::MockData do
139
+ it "does not error on missing data_map_new extension method calls" do
140
+ map = ext.data_map_new
141
+ expect(map).to be_kind_of(Appsignal::Extension::MockData)
142
+ # Does not raise errors any arbitrary method call that does not exist
143
+ map.set_string("key", "value")
144
+ map.set_int("key", 123)
145
+ map.something
146
+ end
147
+
148
+ it "does not error on missing data_array_new extension method calls" do
149
+ array = ext.data_array_new
150
+ expect(array).to be_kind_of(Appsignal::Extension::MockData)
151
+ # Does not raise errors any arbitrary method call that does not exist
152
+ array.append_string("value")
153
+ array.append_int(123)
154
+ array.something
155
+ end
156
+ end
157
+
158
+ describe Appsignal::Extension::MockTransaction do
159
+ it "does not error on missing transaction extension method calls" do
160
+ transaction = described_class.new
161
+
162
+ transaction.start_event(0)
163
+ transaction.finish_event(
164
+ "name",
165
+ "title",
166
+ "body",
167
+ Appsignal::EventFormatter::DEFAULT,
168
+ 0
169
+ )
170
+ transaction.something
171
+ end
138
172
  end
139
173
  end
140
174
  end
@@ -2,6 +2,8 @@ describe Appsignal::Hooks::ActionCableHook do
2
2
  if DependencyHelper.action_cable_present?
3
3
  context "with ActionCable" do
4
4
  require "action_cable/engine"
5
+ # Require test helper to test with ConnectionStub
6
+ require "action_cable/channel/test_case" if DependencyHelper.rails6_present?
5
7
 
6
8
  describe ".dependencies_present?" do
7
9
  subject { described_class.new.dependencies_present? }
@@ -262,6 +264,49 @@ describe Appsignal::Hooks::ActionCableHook do
262
264
  )
263
265
  end
264
266
  end
267
+
268
+ if DependencyHelper.rails6_present?
269
+ context "with ConnectionStub" do
270
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
271
+ let(:transaction_id) { "Stubbed transaction id" }
272
+ before do
273
+ # Stub future (private AppSignal) transaction id generated by the hook.
274
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
275
+ end
276
+
277
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
278
+ instance.subscribe_to_channel
279
+
280
+ expect(subject).to include(
281
+ "action" => "MyChannel#subscribed",
282
+ "error" => nil,
283
+ "id" => transaction_id,
284
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
285
+ "metadata" => {
286
+ "method" => "websocket",
287
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
288
+ }
289
+ )
290
+ expect(subject["events"].first).to include(
291
+ "allocation_count" => kind_of(Integer),
292
+ "body" => "",
293
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
294
+ "child_allocation_count" => kind_of(Integer),
295
+ "child_duration" => kind_of(Float),
296
+ "child_gc_duration" => kind_of(Float),
297
+ "count" => 1,
298
+ "gc_duration" => kind_of(Float),
299
+ "start" => kind_of(Float),
300
+ "duration" => kind_of(Float),
301
+ "name" => "subscribed.action_cable",
302
+ "title" => ""
303
+ )
304
+ expect(subject["sample_data"]).to include(
305
+ "params" => { "internal" => "true" }
306
+ )
307
+ end
308
+ end
309
+ end
265
310
  end
266
311
 
267
312
  describe "unsubscribe callback" do
@@ -349,6 +394,49 @@ describe Appsignal::Hooks::ActionCableHook do
349
394
  )
350
395
  end
351
396
  end
397
+
398
+ if DependencyHelper.rails6_present?
399
+ context "with ConnectionStub" do
400
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
401
+ let(:transaction_id) { "Stubbed transaction id" }
402
+ before do
403
+ # Stub future (private AppSignal) transaction id generated by the hook.
404
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
405
+ end
406
+
407
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
408
+ instance.unsubscribe_from_channel
409
+
410
+ expect(subject).to include(
411
+ "action" => "MyChannel#unsubscribed",
412
+ "error" => nil,
413
+ "id" => transaction_id,
414
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
415
+ "metadata" => {
416
+ "method" => "websocket",
417
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
418
+ }
419
+ )
420
+ expect(subject["events"].first).to include(
421
+ "allocation_count" => kind_of(Integer),
422
+ "body" => "",
423
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
424
+ "child_allocation_count" => kind_of(Integer),
425
+ "child_duration" => kind_of(Float),
426
+ "child_gc_duration" => kind_of(Float),
427
+ "count" => 1,
428
+ "gc_duration" => kind_of(Float),
429
+ "start" => kind_of(Float),
430
+ "duration" => kind_of(Float),
431
+ "name" => "unsubscribed.action_cable",
432
+ "title" => ""
433
+ )
434
+ expect(subject["sample_data"]).to include(
435
+ "params" => { "internal" => "true" }
436
+ )
437
+ end
438
+ end
439
+ end
352
440
  end
353
441
  end
354
442
  end
@@ -16,14 +16,31 @@ describe Appsignal::Hooks::SidekiqHook do
16
16
  end
17
17
 
18
18
  describe "#install" do
19
- class SidekiqMiddlewareMock < Set
20
- def exists?(middleware)
21
- include?(middleware)
19
+ class SidekiqMiddlewareMockWithPrepend < Array
20
+ alias add <<
21
+ alias exists? include?
22
+
23
+ unless method_defined? :prepend
24
+ def prepend(middleware) # For Ruby < 2.5
25
+ insert(0, middleware)
26
+ end
22
27
  end
23
28
  end
29
+
30
+ class SidekiqMiddlewareMockWithoutPrepend < Array
31
+ alias add <<
32
+ alias exists? include?
33
+
34
+ undef_method :prepend if method_defined? :prepend # For Ruby >= 2.5
35
+ end
36
+
24
37
  module SidekiqMock
38
+ def self.middleware_mock=(mock)
39
+ @middlewares = mock.new
40
+ end
41
+
25
42
  def self.middlewares
26
- @middlewares ||= SidekiqMiddlewareMock.new
43
+ @middlewares
27
44
  end
28
45
 
29
46
  def self.configure_server
@@ -34,479 +51,64 @@ describe Appsignal::Hooks::SidekiqHook do
34
51
  yield middlewares if block_given?
35
52
  middlewares
36
53
  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
-
52
- describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
53
- class DelayedTestClass; end
54
-
55
- let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
56
- let(:worker) { anything }
57
- let(:queue) { anything }
58
- let(:given_args) do
59
- [
60
- "foo",
61
- {
62
- :foo => "Foo",
63
- :bar => "Bar",
64
- "baz" => { 1 => :foo }
65
- }
66
- ]
67
- end
68
- let(:expected_args) do
69
- [
70
- "foo",
71
- {
72
- "foo" => "Foo",
73
- "bar" => "Bar",
74
- "baz" => { "1" => "foo" }
75
- }
76
- ]
77
- end
78
- let(:job_class) { "TestClass" }
79
- let(:jid) { "b4a577edbccf1d805744efa9" }
80
- let(:item) do
81
- {
82
- "jid" => jid,
83
- "class" => job_class,
84
- "retry_count" => 0,
85
- "queue" => "default",
86
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
87
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
88
- "args" => given_args,
89
- "extra" => "data"
90
- }
91
- end
92
- let(:plugin) { Appsignal::Hooks::SidekiqPlugin.new }
93
- let(:log) { StringIO.new }
94
- before do
95
- start_agent
96
- Appsignal.logger = test_logger(log)
97
- end
98
- around { |example| keep_transactions { example.run } }
99
- after :with_yaml_parse_error => false do
100
- expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
101
- end
102
-
103
- describe "internal Sidekiq job values" do
104
- it "does not save internal Sidekiq values as metadata on transaction" do
105
- perform_job
106
-
107
- transaction_hash = transaction.to_h
108
- expect(transaction_hash["metadata"].keys)
109
- .to_not include(*Appsignal::Hooks::SidekiqPlugin::EXCLUDED_JOB_KEYS)
110
- end
111
- end
112
-
113
- context "with parameter filtering" do
114
- before do
115
- Appsignal.config = project_fixture_config("production")
116
- Appsignal.config[:filter_parameters] = ["foo"]
117
- end
118
-
119
- it "filters selected arguments" do
120
- perform_job
121
54
 
122
- transaction_hash = transaction.to_h
123
- expect(transaction_hash["sample_data"]).to include(
124
- "params" => [
125
- "foo",
126
- {
127
- "foo" => "[FILTERED]",
128
- "bar" => "Bar",
129
- "baz" => { "1" => "foo" }
130
- }
131
- ]
132
- )
133
- end
134
- end
135
-
136
- context "with encrypted arguments" do
137
- before do
138
- item["encrypt"] = true
139
- item["args"] << "super secret value" # Last argument will be replaced
140
- end
141
-
142
- it "replaces the last argument (the secret bag) with an [encrypted data] string" do
143
- perform_job
144
-
145
- transaction_hash = transaction.to_h
146
- expect(transaction_hash["sample_data"]).to include(
147
- "params" => expected_args << "[encrypted data]"
148
- )
149
- end
150
- end
151
-
152
- context "when using the Sidekiq delayed extension" do
153
- let(:item) do
154
- {
155
- "jid" => jid,
156
- "class" => "Sidekiq::Extensions::DelayedClass",
157
- "queue" => "default",
158
- "args" => [
159
- "---\n- !ruby/class 'DelayedTestClass'\n- :foo_method\n- - :bar: baz\n"
160
- ],
161
- "retry" => true,
162
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
163
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
164
- "extra" => "data"
165
- }
166
- end
167
-
168
- it "uses the delayed class and method name for the action" do
169
- perform_job
170
-
171
- transaction_hash = transaction.to_h
172
- expect(transaction_hash["action"]).to eq("DelayedTestClass.foo_method")
173
- expect(transaction_hash["sample_data"]).to include(
174
- "params" => ["bar" => "baz"]
175
- )
176
- end
177
-
178
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
179
- before { item["args"] = [] }
180
-
181
- it "logs a warning and uses the default argument" do
182
- perform_job
183
-
184
- transaction_hash = transaction.to_h
185
- expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedClass#perform")
186
- expect(transaction_hash["sample_data"]).to include("params" => [])
187
- expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
55
+ def self.error_handlers
56
+ @error_handlers ||= []
188
57
  end
189
58
  end
190
- end
191
-
192
- context "when using the Sidekiq ActiveRecord instance delayed extension" do
193
- let(:item) do
194
- {
195
- "jid" => jid,
196
- "class" => "Sidekiq::Extensions::DelayedModel",
197
- "queue" => "default",
198
- "args" => [
199
- "---\n- !ruby/object:DelayedTestClass {}\n- :foo_method\n- - :bar: :baz\n"
200
- ],
201
- "retry" => true,
202
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
203
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
204
- "extra" => "data"
205
- }
206
- end
207
-
208
- it "uses the delayed class and method name for the action" do
209
- perform_job
210
-
211
- transaction_hash = transaction.to_h
212
- expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
213
- expect(transaction_hash["sample_data"]).to include(
214
- "params" => ["bar" => "baz"]
215
- )
216
- end
217
59
 
218
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
219
- before { item["args"] = [] }
220
-
221
- it "logs a warning and uses the default argument" do
222
- perform_job
223
-
224
- transaction_hash = transaction.to_h
225
- expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedModel#perform")
226
- expect(transaction_hash["sample_data"]).to include("params" => [])
227
- expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
60
+ def add_middleware(middleware)
61
+ Sidekiq.configure_server do |sidekiq_config|
62
+ sidekiq_config.middlewares.add(middleware)
228
63
  end
229
64
  end
230
- end
231
-
232
- context "with an error" do
233
- let(:error) { ExampleException }
234
65
 
235
- it "creates a transaction and adds the error" do
236
- expect(Appsignal).to receive(:increment_counter)
237
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :failed)
238
- expect(Appsignal).to receive(:increment_counter)
239
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
240
-
241
- expect do
242
- perform_job { raise error, "uh oh" }
243
- end.to raise_error(error)
244
-
245
- transaction_hash = transaction.to_h
246
- expect(transaction_hash).to include(
247
- "id" => jid,
248
- "action" => "TestClass#perform",
249
- "error" => {
250
- "name" => "ExampleException",
251
- "message" => "uh oh",
252
- # TODO: backtrace should be an Array of Strings
253
- # https://github.com/appsignal/appsignal-agent/issues/294
254
- "backtrace" => kind_of(String)
255
- },
256
- "metadata" => {
257
- "extra" => "data",
258
- "queue" => "default",
259
- "retry_count" => "0"
260
- },
261
- "namespace" => namespace,
262
- "sample_data" => {
263
- "environment" => {},
264
- "params" => expected_args,
265
- "tags" => {},
266
- "breadcrumbs" => []
267
- }
268
- )
269
- expect_transaction_to_have_sidekiq_event(transaction_hash)
270
- end
271
- end
272
-
273
- context "without an error" do
274
- it "creates a transaction with events" do
275
- expect(Appsignal).to receive(:increment_counter)
276
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
277
-
278
- perform_job
279
-
280
- transaction_hash = transaction.to_h
281
- expect(transaction_hash).to include(
282
- "id" => jid,
283
- "action" => "TestClass#perform",
284
- "error" => nil,
285
- "metadata" => {
286
- "extra" => "data",
287
- "queue" => "default",
288
- "retry_count" => "0"
289
- },
290
- "namespace" => namespace,
291
- "sample_data" => {
292
- "environment" => {},
293
- "params" => expected_args,
294
- "tags" => {},
295
- "breadcrumbs" => []
296
- }
297
- )
298
- # TODO: Not available in transaction.to_h yet.
299
- # https://github.com/appsignal/appsignal-agent/issues/293
300
- expect(transaction.request.env).to eq(
301
- :queue_start => Time.parse("2001-01-01 10:00:00UTC").to_f
302
- )
303
- expect_transaction_to_have_sidekiq_event(transaction_hash)
304
- end
305
- end
306
-
307
- def perform_job
308
- Timecop.freeze(Time.parse("2001-01-01 10:01:00UTC")) do
309
- plugin.call(worker, item, queue) do
310
- yield if block_given?
311
- end
312
- end
313
- end
314
-
315
- def transaction
316
- last_transaction
317
- end
318
-
319
- def expect_transaction_to_have_sidekiq_event(transaction_hash)
320
- events = transaction_hash["events"]
321
- expect(events.count).to eq(1)
322
- expect(events.first).to include(
323
- "name" => "perform_job.sidekiq",
324
- "title" => "",
325
- "count" => 1,
326
- "body" => "",
327
- "body_format" => Appsignal::EventFormatter::DEFAULT
328
- )
329
- end
330
- end
331
-
332
- if DependencyHelper.active_job_present?
333
- require "active_job"
334
- require "action_mailer"
335
- require "sidekiq/testing"
336
-
337
- describe "Sidekiq ActiveJob integration" do
338
- let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
339
- let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
340
- let(:log) { StringIO.new }
341
- let(:given_args) do
342
- [
343
- "foo",
344
- {
345
- :foo => "Foo",
346
- "bar" => "Bar",
347
- "baz" => { "1" => "foo" }
348
- }
349
- ]
350
- end
351
- let(:expected_args) do
352
- [
353
- "foo",
354
- {
355
- "_aj_symbol_keys" => ["foo"],
356
- "foo" => "Foo",
357
- "bar" => "Bar",
358
- "baz" => {
359
- "_aj_symbol_keys" => [],
360
- "1" => "foo"
361
- }
362
- }
363
- ]
364
- end
365
- let(:expected_tags) do
366
- {}.tap do |hash|
367
- hash["active_job_id"] = kind_of(String)
368
- if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
369
- hash["provider_job_id"] = kind_of(String)
370
- end
371
- end
372
- end
373
66
  before do
374
- start_agent
375
- Appsignal.logger = test_logger(log)
376
- ActiveJob::Base.queue_adapter = :sidekiq
377
-
378
- class ActiveJobSidekiqTestJob < ActiveJob::Base
379
- self.queue_adapter = :sidekiq
380
-
381
- def perform(*_args)
382
- end
383
- end
384
-
385
- class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
386
- self.queue_adapter = :sidekiq
387
-
388
- def perform(*_args)
389
- raise "uh oh"
390
- end
391
- end
392
- # Manually add the AppSignal middleware for the Testing environment.
393
- # It doesn't use configured middlewares by default looks like.
394
- # We test somewhere else if the middleware is installed properly.
395
- Sidekiq::Testing.server_middleware do |chain|
396
- chain.add Appsignal::Hooks::SidekiqPlugin
397
- end
398
- end
399
- around do |example|
400
- keep_transactions do
401
- Sidekiq::Testing.fake! do
402
- example.run
403
- end
404
- end
405
- end
406
- after do
407
- Object.send(:remove_const, :ActiveJobSidekiqTestJob)
408
- Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
409
- end
410
-
411
- it "reports the transaction from the ActiveJob integration" do
412
- perform_job(ActiveJobSidekiqTestJob, given_args)
413
-
414
- transaction = last_transaction
415
- transaction_hash = transaction.to_h
416
- expect(transaction_hash).to include(
417
- "action" => "ActiveJobSidekiqTestJob#perform",
418
- "error" => nil,
419
- "namespace" => namespace,
420
- "metadata" => hash_including(
421
- "queue" => "default"
422
- ),
423
- "sample_data" => hash_including(
424
- "environment" => {},
425
- "params" => [expected_args],
426
- "tags" => expected_tags.merge("queue" => "default")
427
- )
428
- )
429
- expect(transaction.request.env).to eq(:queue_start => time.to_f)
430
- events = transaction_hash["events"]
431
- .sort_by { |e| e["start"] }
432
- .map { |event| event["name"] }
433
- expect(events)
434
- .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
67
+ Appsignal.config = project_fixture_config
68
+ stub_const "Sidekiq", SidekiqMock
435
69
  end
436
70
 
437
- context "with error" do
438
- it "reports the error on the transaction from the ActiveRecord integration" do
439
- expect do
440
- perform_job(ActiveJobSidekiqErrorTestJob, given_args)
441
- end.to raise_error(RuntimeError, "uh oh")
442
-
443
- transaction = last_transaction
444
- transaction_hash = transaction.to_h
445
- expect(transaction_hash).to include(
446
- "action" => "ActiveJobSidekiqErrorTestJob#perform",
447
- "error" => {
448
- "name" => "RuntimeError",
449
- "message" => "uh oh",
450
- "backtrace" => kind_of(String)
451
- },
452
- "namespace" => namespace,
453
- "metadata" => hash_including(
454
- "queue" => "default"
455
- ),
456
- "sample_data" => hash_including(
457
- "environment" => {},
458
- "params" => [expected_args],
459
- "tags" => expected_tags.merge("queue" => "default")
460
- )
461
- )
462
- expect(transaction.request.env).to eq(:queue_start => time.to_f)
463
- events = transaction_hash["events"]
464
- .sort_by { |e| e["start"] }
465
- .map { |event| event["name"] }
466
- expect(events)
467
- .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
468
- end
71
+ it "adds error handler" do
72
+ Sidekiq.middleware_mock = SidekiqMiddlewareMockWithPrepend
73
+ described_class.new.install
74
+ expect(Sidekiq.error_handlers).to include(Appsignal::Integrations::SidekiqErrorHandler)
469
75
  end
470
76
 
471
- context "with ActionMailer" do
472
- include ActionMailerHelpers
77
+ context "when Sidekiq middleware responds to prepend method" do # Sidekiq 3.3.0 and newer
78
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithPrepend }
473
79
 
474
- before do
475
- class ActionMailerSidekiqTestJob < ActionMailer::Base
476
- def welcome(*args)
477
- end
478
- end
479
- end
80
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain in the first position" do
81
+ user_middleware1 = proc {}
82
+ add_middleware(user_middleware1)
83
+ described_class.new.install
84
+ user_middleware2 = proc {}
85
+ add_middleware(user_middleware2)
480
86
 
481
- it "reports ActionMailer data on the transaction" do
482
- perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
483
-
484
- transaction = last_transaction
485
- transaction_hash = transaction.to_h
486
- expect(transaction_hash).to include(
487
- "action" => "ActionMailerSidekiqTestJob#welcome",
488
- "sample_data" => hash_including(
489
- "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
490
- )
491
- )
87
+ expect(Sidekiq.server_middleware).to eql([
88
+ Appsignal::Integrations::SidekiqMiddleware, # Prepend makes it the first entry
89
+ user_middleware1,
90
+ user_middleware2
91
+ ])
492
92
  end
493
93
  end
494
94
 
495
- def perform_sidekiq
496
- Timecop.freeze(time) do
497
- yield
498
- # Combined with Sidekiq::Testing.fake! and drain_all we get a
499
- # enqueue_at in the job data.
500
- Sidekiq::Worker.drain_all
501
- end
502
- end
95
+ context "when Sidekiq middleware does not respond to prepend method" do
96
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithoutPrepend }
503
97
 
504
- def perform_job(job_class, args)
505
- perform_sidekiq { job_class.perform_later(args) }
506
- end
98
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
99
+ user_middleware1 = proc {}
100
+ add_middleware(user_middleware1)
101
+ described_class.new.install
102
+ user_middleware2 = proc {}
103
+ add_middleware(user_middleware2)
507
104
 
508
- def perform_mailer(mailer, method, args = nil)
509
- perform_sidekiq { perform_action_mailer(mailer, method, args) }
105
+ # Add middlewares in whatever order they were added
106
+ expect(Sidekiq.server_middleware).to eql([
107
+ user_middleware1,
108
+ Appsignal::Integrations::SidekiqMiddleware,
109
+ user_middleware2
110
+ ])
111
+ end
510
112
  end
511
113
  end
512
114
  end