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
@@ -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