appsignal 2.11.8-java → 3.0.1-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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/.rubocop_todo.yml +1 -1
  4. data/.semaphore/semaphore.yml +88 -111
  5. data/CHANGELOG.md +24 -0
  6. data/appsignal.gemspec +1 -1
  7. data/build_matrix.yml +11 -15
  8. data/lib/appsignal.rb +2 -29
  9. data/lib/appsignal/auth_check.rb +2 -8
  10. data/lib/appsignal/cli.rb +1 -23
  11. data/lib/appsignal/config.rb +1 -25
  12. data/lib/appsignal/event_formatter.rb +0 -25
  13. data/lib/appsignal/helpers/instrumentation.rb +69 -5
  14. data/lib/appsignal/hooks.rb +6 -13
  15. data/lib/appsignal/hooks/action_cable.rb +3 -34
  16. data/lib/appsignal/hooks/active_support_notifications.rb +7 -86
  17. data/lib/appsignal/hooks/celluloid.rb +5 -9
  18. data/lib/appsignal/hooks/net_http.rb +2 -12
  19. data/lib/appsignal/hooks/puma.rb +3 -5
  20. data/lib/appsignal/hooks/que.rb +1 -1
  21. data/lib/appsignal/hooks/rake.rb +2 -24
  22. data/lib/appsignal/hooks/redis.rb +2 -13
  23. data/lib/appsignal/hooks/resque.rb +2 -43
  24. data/lib/appsignal/hooks/sidekiq.rb +6 -143
  25. data/lib/appsignal/hooks/unicorn.rb +3 -24
  26. data/lib/appsignal/hooks/webmachine.rb +1 -7
  27. data/lib/appsignal/integrations/action_cable.rb +34 -0
  28. data/lib/appsignal/integrations/active_support_notifications.rb +77 -0
  29. data/lib/appsignal/integrations/net_http.rb +16 -0
  30. data/lib/appsignal/integrations/object.rb +39 -4
  31. data/lib/appsignal/integrations/padrino.rb +5 -7
  32. data/lib/appsignal/integrations/que.rb +26 -33
  33. data/lib/appsignal/integrations/railtie.rb +1 -4
  34. data/lib/appsignal/integrations/rake.rb +26 -2
  35. data/lib/appsignal/integrations/redis.rb +17 -0
  36. data/lib/appsignal/integrations/resque.rb +39 -10
  37. data/lib/appsignal/integrations/sidekiq.rb +171 -0
  38. data/lib/appsignal/integrations/unicorn.rb +28 -0
  39. data/lib/appsignal/integrations/webmachine.rb +22 -24
  40. data/lib/appsignal/minutely.rb +0 -12
  41. data/lib/appsignal/version.rb +1 -1
  42. data/spec/lib/appsignal/auth_check_spec.rb +1 -24
  43. data/spec/lib/appsignal/cli_spec.rb +1 -1
  44. data/spec/lib/appsignal/config_spec.rb +2 -66
  45. data/spec/lib/appsignal/event_formatter_spec.rb +0 -37
  46. data/spec/lib/appsignal/hooks/celluloid_spec.rb +6 -1
  47. data/spec/lib/appsignal/hooks/rake_spec.rb +1 -2
  48. data/spec/lib/appsignal/hooks/redis_spec.rb +50 -15
  49. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +12 -464
  50. data/spec/lib/appsignal/hooks/unicorn_spec.rb +14 -3
  51. data/spec/lib/appsignal/hooks/webmachine_spec.rb +2 -13
  52. data/spec/lib/appsignal/hooks_spec.rb +6 -22
  53. data/spec/lib/appsignal/integrations/object_spec.rb +91 -8
  54. data/spec/lib/appsignal/integrations/padrino_spec.rb +2 -3
  55. data/spec/lib/appsignal/integrations/railtie_spec.rb +0 -45
  56. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +524 -0
  57. data/spec/lib/appsignal/integrations/webmachine_spec.rb +26 -8
  58. data/spec/lib/appsignal/minutely_spec.rb +0 -19
  59. data/spec/lib/appsignal/transaction_spec.rb +1 -14
  60. data/spec/lib/appsignal/transmitter_spec.rb +1 -1
  61. data/spec/lib/appsignal_spec.rb +162 -116
  62. data/spec/spec_helper.rb +1 -15
  63. metadata +11 -21
  64. data/lib/appsignal/cli/notify_of_deploy.rb +0 -131
  65. data/lib/appsignal/integrations/object_ruby_19.rb +0 -37
  66. data/lib/appsignal/integrations/object_ruby_modern.rb +0 -64
  67. data/lib/appsignal/integrations/resque_active_job.rb +0 -19
  68. data/lib/appsignal/js_exception_transaction.rb +0 -56
  69. data/lib/appsignal/rack/js_exception_catcher.rb +0 -80
  70. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +0 -180
  71. data/spec/lib/appsignal/integrations/object_19_spec.rb +0 -266
  72. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +0 -28
  73. data/spec/lib/appsignal/integrations/resque_spec.rb +0 -28
  74. data/spec/lib/appsignal/js_exception_transaction_spec.rb +0 -128
  75. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +0 -170
@@ -3,12 +3,22 @@ describe Appsignal::Hooks::UnicornHook do
3
3
  before :context do
4
4
  module Unicorn
5
5
  class HttpServer
6
- def worker_loop(worker)
6
+ def worker_loop(_worker)
7
+ @worker_loop = true
8
+ end
9
+
10
+ def worker_loop?
11
+ @worker_loop == true
7
12
  end
8
13
  end
9
14
 
10
15
  class Worker
11
16
  def close
17
+ @close = true
18
+ end
19
+
20
+ def close?
21
+ @close == true
12
22
  end
13
23
  end
14
24
  end
@@ -27,18 +37,19 @@ describe Appsignal::Hooks::UnicornHook do
27
37
  worker = double
28
38
 
29
39
  expect(Appsignal).to receive(:forked)
30
- expect(server).to receive(:worker_loop_without_appsignal).with(worker)
31
40
 
32
41
  server.worker_loop(worker)
42
+
43
+ expect(server.worker_loop?).to be true
33
44
  end
34
45
 
35
46
  it "adds behavior to Unicorn::Worker#close" do
36
47
  worker = Unicorn::Worker.new
37
48
 
38
49
  expect(Appsignal).to receive(:stop)
39
- expect(worker).to receive(:close_without_appsignal)
40
50
 
41
51
  worker.close
52
+ expect(worker.close?).to be true
42
53
  end
43
54
  end
44
55
 
@@ -10,19 +10,8 @@ describe Appsignal::Hooks::WebmachineHook do
10
10
  it { is_expected.to be_truthy }
11
11
  end
12
12
 
13
- it "should include the run alias methods" do
14
- expect(fsm).to respond_to(:run_with_appsignal)
15
- expect(fsm).to respond_to(:run_without_appsignal)
16
- end
17
-
18
- it "should include the handle_exceptions alias methods" do
19
- expect(
20
- fsm.respond_to?(:handle_exceptions_with_appsignal, true)
21
- ).to be_truthy
22
-
23
- expect(
24
- fsm.respond_to?(:handle_exceptions_without_appsignal, true)
25
- ).to be_truthy
13
+ it "adds behavior to Webmachine::Decision::FSM" do
14
+ expect(fsm.class.ancestors.first).to eq(Appsignal::Integrations::WebmachineIntegration)
26
15
  end
27
16
  end
28
17
  else
@@ -92,32 +92,16 @@ describe Appsignal::Hooks do
92
92
  capture_std_streams(std_stream, err_stream, &block)
93
93
  end
94
94
 
95
- describe "SidekiqProbe" do
95
+ describe "SidekiqPlugin" do
96
96
  it "logs a deprecation message and returns the new constant" do
97
- constant = call_constant { Appsignal::Hooks::SidekiqProbe }
97
+ constant = call_constant { Appsignal::Hooks::SidekiqPlugin }
98
98
 
99
- expect(constant).to eql(Appsignal::Probes::SidekiqProbe)
100
- expect(constant.name).to eql("Appsignal::Probes::SidekiqProbe")
99
+ expect(constant).to eql(Appsignal::Integrations::SidekiqMiddleware)
100
+ expect(constant.name).to eql("Appsignal::Integrations::SidekiqMiddleware")
101
101
 
102
102
  deprecation_message =
103
- "The constant Appsignal::Hooks::SidekiqProbe has been deprecated. " \
104
- "Please update the constant name to Appsignal::Probes::SidekiqProbe " \
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 "PumaProbe" do
112
- it "logs a deprecation message and returns the new constant" do
113
- constant = call_constant { Appsignal::Hooks::PumaProbe }
114
-
115
- expect(constant).to eql(Appsignal::Probes::PumaProbe)
116
- expect(constant.name).to eql("Appsignal::Probes::PumaProbe")
117
-
118
- deprecation_message =
119
- "The constant Appsignal::Hooks::PumaProbe has been deprecated. " \
120
- "Please update the constant name to Appsignal::Probes::PumaProbe " \
103
+ "The constant Appsignal::Hooks::SidekiqPlugin has been deprecated. " \
104
+ "Please update the constant name to Appsignal::Integrations::SidekiqMiddleware " \
121
105
  "in the following file to remove this message.\n#{__FILE__}:"
122
106
  expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
123
107
  expect(log).to contains_log :warn, deprecation_message
@@ -1,9 +1,5 @@
1
1
  require "appsignal/integrations/object"
2
2
 
3
- def is_ruby_19
4
- RUBY_VERSION < "2.0"
5
- end
6
-
7
3
  describe Object do
8
4
  describe "#instrument_method" do
9
5
  context "with instance method" do
@@ -30,12 +26,57 @@ describe Object do
30
26
  before do
31
27
  Appsignal.config = project_fixture_config
32
28
  expect(Appsignal::Transaction).to receive(:current).at_least(:once).and_return(transaction)
29
+ expect(Appsignal.active?).to be_truthy
33
30
  end
34
31
  after { Appsignal.config = nil }
35
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
+
36
78
  context "with anonymous class" do
37
79
  it "instruments the method and calls it" do
38
- expect(Appsignal.active?).to be_truthy
39
80
  expect(transaction).to receive(:start_event)
40
81
  expect(transaction).to receive(:finish_event).with \
41
82
  "foo.AnonymousClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -56,7 +97,6 @@ describe Object do
56
97
  let(:klass) { NamedClass }
57
98
 
58
99
  it "instruments the method and calls it" do
59
- expect(Appsignal.active?).to be_truthy
60
100
  expect(transaction).to receive(:start_event)
61
101
  expect(transaction).to receive(:finish_event).with \
62
102
  "foo.NamedClass.other", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -81,7 +121,6 @@ describe Object do
81
121
  let(:klass) { MyModule::NestedModule::NamedClass }
82
122
 
83
123
  it "instruments the method and calls it" do
84
- expect(Appsignal.active?).to be_truthy
85
124
  expect(transaction).to receive(:start_event)
86
125
  expect(transaction).to receive(:finish_event).with \
87
126
  "bar.NamedClass.NestedModule.MyModule.other", nil, nil,
@@ -101,7 +140,6 @@ describe Object do
101
140
  end
102
141
 
103
142
  it "instruments with custom name" do
104
- expect(Appsignal.active?).to be_truthy
105
143
  expect(transaction).to receive(:start_event)
106
144
  expect(transaction).to receive(:finish_event).with \
107
145
  "my_method.group", nil, nil, Appsignal::EventFormatter::DEFAULT
@@ -162,6 +200,51 @@ describe Object do
162
200
  end
163
201
  after { Appsignal.config = nil }
164
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
+
165
248
  context "with anonymous class" do
166
249
  it "instruments the method and calls it" do
167
250
  expect(Appsignal.active?).to be_truthy
@@ -150,10 +150,9 @@ if DependencyHelper.padrino_present?
150
150
  let(:path) { "/static" }
151
151
  before do
152
152
  env["sinatra.static_file"] = true
153
- expect_any_instance_of(app)
154
- .to receive(:route_without_appsignal).and_return([200, {}, ["foo"]])
153
+ app.controllers { get(:static) { "Static!" } }
155
154
  end
156
- after { expect(response).to match_response(200, "foo") }
155
+ after { expect(response).to match_response(200, "Static!") }
157
156
 
158
157
  it "does not instrument the request" do
159
158
  expect_no_transaction_to_be_created
@@ -79,51 +79,6 @@ if DependencyHelper.rails_present?
79
79
 
80
80
  after { Appsignal::Integrations::Railtie.initialize_appsignal(app) }
81
81
  end
82
-
83
- describe "frontend_error_catching middleware" do
84
- let(:config) do
85
- Appsignal::Config.new(
86
- project_fixture_path,
87
- "test",
88
- :name => "MyApp",
89
- :enable_frontend_error_catching => enable_frontend_error_catching
90
- )
91
- end
92
- before { allow(Appsignal::Config).to receive(:new).and_return(config) }
93
- after { Appsignal::Integrations::Railtie.initialize_appsignal(app) }
94
-
95
- context "when enabled" do
96
- let(:enable_frontend_error_catching) { true }
97
-
98
- it "adds the Rails and JSExceptionCatcher middleware" do
99
- expect(app.middleware).to receive(:insert_after).with(
100
- ActionDispatch::DebugExceptions,
101
- Appsignal::Rack::RailsInstrumentation
102
- )
103
-
104
- expect(app.middleware).to receive(:insert_before).with(
105
- Appsignal::Rack::RailsInstrumentation,
106
- Appsignal::Rack::JSExceptionCatcher
107
- )
108
- end
109
- end
110
-
111
- context "when not enabled" do
112
- let(:enable_frontend_error_catching) { false }
113
-
114
- it "adds the Rails middleware, but not the JSExceptionCatcher middleware" do
115
- expect(app.middleware).to receive(:insert_after).with(
116
- ActionDispatch::DebugExceptions,
117
- Appsignal::Rack::RailsInstrumentation
118
- )
119
-
120
- expect(app.middleware).to_not receive(:insert_before).with(
121
- Appsignal::Rack::RailsInstrumentation,
122
- Appsignal::Rack::JSExceptionCatcher
123
- )
124
- end
125
- end
126
- end
127
82
  end
128
83
  end
129
84
  end
@@ -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