appsignal 2.10.6 → 2.11.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -60,6 +60,9 @@ module Appsignal
60
60
  [:appsignal_string],
61
61
  :appsignal_string
62
62
  attach_function :appsignal_running_in_container, [], :bool
63
+ attach_function :appsignal_set_environment_metadata,
64
+ [:appsignal_string, :appsignal_string],
65
+ :void
63
66
 
64
67
  # Metrics methods
65
68
  attach_function :appsignal_set_gauge,
@@ -224,6 +227,13 @@ module Appsignal
224
227
  appsignal_running_in_container
225
228
  end
226
229
 
230
+ def set_environment_metadata(key, value)
231
+ appsignal_set_environment_metadata(
232
+ make_appsignal_string(key),
233
+ make_appsignal_string(value)
234
+ )
235
+ end
236
+
227
237
  def set_gauge(key, value, tags)
228
238
  appsignal_set_gauge(make_appsignal_string(key), value, tags.pointer)
229
239
  end
@@ -13,23 +13,20 @@ module Appsignal
13
13
  end
14
14
 
15
15
  def install
16
- if Appsignal::System.ruby_2_or_up?
17
- require "appsignal/integrations/net_http"
18
- Net::HTTP.send(:prepend, Appsignal::Integrations::NetHttpIntegration)
19
- else
20
- Net::HTTP.class_eval do
21
- alias request_without_appsignal request
16
+ Net::HTTP.class_eval do
17
+ alias request_without_appsignal request
22
18
 
23
- def request(request, body = nil, &block)
24
- Appsignal.instrument(
25
- "request.net_http",
26
- "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}"
27
- ) do
28
- request_without_appsignal(request, body, &block)
29
- end
19
+ def request(request, body = nil, &block)
20
+ Appsignal.instrument(
21
+ "request.net_http",
22
+ "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}"
23
+ ) do
24
+ request_without_appsignal(request, body, &block)
30
25
  end
31
26
  end
32
27
  end
28
+
29
+ Appsignal::Environment.report_enabled("net_http")
33
30
  end
34
31
  end
35
32
  end
@@ -26,6 +26,8 @@ module Appsignal
26
26
  end
27
27
  end
28
28
  end
29
+
30
+ Appsignal::Environment.report_enabled("redis")
29
31
  end
30
32
  end
31
33
  end
@@ -56,6 +56,8 @@ module Appsignal
56
56
 
57
57
  # ... and automatically add it to future instances.
58
58
  ::Sequel::Database.extension(:appsignal_integration)
59
+
60
+ Appsignal::Environment.report_enabled("sequel")
59
61
  end
60
62
  end
61
63
  end
@@ -27,9 +27,8 @@ module Appsignal
27
27
  method_name = "perform"
28
28
  else
29
29
  # Delayed Job
30
- args = extract_value(job.payload_object, :args, {})
31
- class_and_method_name = extract_value(job.payload_object, :appsignal_name, job.name)
32
- class_name, method_name = class_and_method_name.split("#")
30
+ args = extract_value(payload, :args, {})
31
+ class_name, method_name = class_and_method_name_from_object_or_hash(payload, job.name)
33
32
  end
34
33
 
35
34
  params = Appsignal::Utils::HashSanitizer.sanitize(
@@ -54,6 +53,20 @@ module Appsignal
54
53
  end
55
54
  end
56
55
 
56
+ def self.class_and_method_name_from_object_or_hash(payload, default_name)
57
+ # Attempt to find appsignal_name override
58
+ class_and_method_name = extract_value(payload, :appsignal_name, nil)
59
+ return class_and_method_name.split("#") if class_and_method_name.is_a?(String)
60
+
61
+ pound_split = default_name.split("#")
62
+ return pound_split if pound_split.length == 2
63
+
64
+ dot_split = default_name.split(".")
65
+ return default_name if dot_split.length == 2
66
+
67
+ ["unknown"]
68
+ end
69
+
57
70
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
58
71
  value = nil
59
72
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if defined?(Appsignal)
4
+ Appsignal::Environment.report_enabled("object_instrumentation")
5
+ end
6
+
3
7
  class Object
4
8
  def self.appsignal_instrument_class_method(method_name, options = {})
5
9
  singleton_class.send \
@@ -14,12 +14,18 @@ module Appsignal
14
14
  Appsignal.config[:filter_parameters]
15
15
  )
16
16
 
17
+ queue_start =
18
+ if job.respond_to?(:enqueued_at) && job.enqueued_at
19
+ Time.parse(job.enqueued_at).utc
20
+ end
21
+
17
22
  Appsignal.monitor_single_transaction(
18
23
  "perform_job.resque",
19
- :class => job.class.to_s,
20
- :method => "perform",
21
- :params => params,
22
- :metadata => {
24
+ :class => job.class.to_s,
25
+ :method => "perform",
26
+ :params => params,
27
+ :queue_start => queue_start,
28
+ :metadata => {
23
29
  :id => job.job_id,
24
30
  :queue => job.queue_name
25
31
  }
@@ -28,6 +34,8 @@ module Appsignal
28
34
  end
29
35
  end
30
36
  end
37
+
38
+ Appsignal::Environment.report("ruby_active_job_resque_enabled") { true }
31
39
  end
32
40
  end
33
41
  end
@@ -76,14 +76,8 @@ module Appsignal
76
76
  ldd_version && ldd_version[0]
77
77
  end
78
78
 
79
- # @api private
80
79
  def self.jruby?
81
80
  RUBY_PLATFORM == "java"
82
81
  end
83
-
84
- # @api private
85
- def self.ruby_2_or_up?
86
- versionify(RUBY_VERSION) >= versionify("2.0")
87
- end
88
82
  end
89
83
  end
@@ -228,12 +228,27 @@ module Appsignal
228
228
  Appsignal.logger.warn("Queue start value #{start} is too big")
229
229
  end
230
230
 
231
+ # Set the queue time based on the HTTP header or `:queue_start` env key
232
+ # value.
233
+ #
234
+ # This method will first try to read the queue time from the HTTP headers
235
+ # `X-Request-Start` or `X-Queue-Start`. Which are parsed by Rack as
236
+ # `HTTP_X_QUEUE_START` and `HTTP_X_REQUEST_START`.
237
+ # The header value is parsed by AppSignal as either milliseconds or
238
+ # microseconds.
239
+ #
240
+ # If no headers are found, or the value could not be parsed, it falls back
241
+ # on the `:queue_start` env key on this Transaction's {request} environment
242
+ # (called like `request.env[:queue_start]`). This value is parsed by
243
+ # AppSignal as seconds.
244
+ #
245
+ # @see https://docs.appsignal.com/ruby/instrumentation/request-queue-time.html
246
+ # @return [void]
231
247
  def set_http_or_background_queue_start
232
- if namespace == HTTP_REQUEST
233
- set_queue_start(http_queue_start)
234
- elsif namespace == BACKGROUND_JOB
235
- set_queue_start(background_queue_start)
236
- end
248
+ start = http_queue_start || background_queue_start
249
+ return unless start
250
+
251
+ set_queue_start(start)
237
252
  end
238
253
 
239
254
  def set_metadata(key, value)
@@ -346,14 +361,14 @@ module Appsignal
346
361
  #
347
362
  # @return [nil] if no {#environment} is present.
348
363
  # @return [nil] if there is no `:queue_start` in the {#environment}.
349
- # @return [Integer]
364
+ # @return [Integer] `:queue_start` time (in seconds) converted to milliseconds
350
365
  def background_queue_start
351
366
  env = environment
352
367
  return unless env
353
368
  queue_start = env[:queue_start]
354
369
  return unless queue_start
355
370
 
356
- (queue_start.to_f * 1000.0).to_i
371
+ (queue_start.to_f * 1000.0).to_i # Convert seconds to milliseconds
357
372
  end
358
373
 
359
374
  # Returns HTTP queue start time in milliseconds.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.10.6".freeze
4
+ VERSION = "2.11.0.alpha.1".freeze
5
5
  end
@@ -263,7 +263,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
263
263
  },
264
264
  "download" => {
265
265
  "download_url" => kind_of(String),
266
- "checksum" => "verified"
266
+ "checksum" => "verified",
267
+ "http_proxy" => nil
267
268
  },
268
269
  "build" => {
269
270
  "time" => kind_of(String),
@@ -148,6 +148,7 @@ describe Appsignal::Config do
148
148
  :instrument_redis => true,
149
149
  :instrument_sequel => true,
150
150
  :skip_session_data => false,
151
+ :send_environment_metadata => true,
151
152
  :send_params => true,
152
153
  :endpoint => "https://push.appsignal.com",
153
154
  :push_api_key => "abc",
@@ -411,7 +412,8 @@ describe Appsignal::Config do
411
412
  :instrument_sequel => false,
412
413
  :files_world_accessible => false,
413
414
  :request_headers => %w[accept accept-charset],
414
- :revision => "v2.5.1"
415
+ :revision => "v2.5.1",
416
+ :send_environment_metadata => false
415
417
  }
416
418
  end
417
419
  before do
@@ -428,6 +430,7 @@ describe Appsignal::Config do
428
430
  ENV["APPSIGNAL_INSTRUMENT_SEQUEL"] = "false"
429
431
  ENV["APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = "false"
430
432
  ENV["APPSIGNAL_REQUEST_HEADERS"] = "accept,accept-charset"
433
+ ENV["APPSIGNAL_SEND_ENVIRONMENT_METADATA"] = "false"
431
434
  ENV["APP_REVISION"] = "v2.5.1"
432
435
  end
433
436
 
@@ -527,6 +530,7 @@ describe Appsignal::Config do
527
530
  config[:running_in_container] = false
528
531
  config[:dns_servers] = ["8.8.8.8", "8.8.4.4"]
529
532
  config[:transaction_debug_mode] = true
533
+ config[:send_environment_metadata] = false
530
534
  config[:revision] = "v2.5.1"
531
535
  config.write_to_environment
532
536
  end
@@ -555,6 +559,7 @@ describe Appsignal::Config do
555
559
  expect(ENV["_APPSIGNAL_DNS_SERVERS"]).to eq "8.8.8.8,8.8.4.4"
556
560
  expect(ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"]).to eq "true"
557
561
  expect(ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"]).to eq "true"
562
+ expect(ENV["_APPSIGNAL_SEND_ENVIRONMENT_METADATA"]).to eq "false"
558
563
  expect(ENV["_APP_REVISION"]).to eq "v2.5.1"
559
564
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIR_PATH")
560
565
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIRECTORY_PATH")
@@ -0,0 +1,167 @@
1
+ describe Appsignal::Environment do
2
+ include EnvironmentMetadataHelper
3
+
4
+ before(:context) { start_agent }
5
+ before { capture_environment_metadata_report_calls }
6
+
7
+ def report(key, &value_block)
8
+ described_class.report(key, &value_block)
9
+ end
10
+
11
+ describe ".report" do
12
+ it "sends environment metadata to the extension" do
13
+ logs =
14
+ capture_logs do
15
+ report("_test_ruby_version") { "1.0.0" }
16
+ expect_environment_metadata("_test_ruby_version", "1.0.0")
17
+ end
18
+ expect(logs).to be_empty
19
+ end
20
+
21
+ context "when the key is a non String type" do
22
+ it "does not set the value" do
23
+ logs =
24
+ capture_logs do
25
+ report(:_test_symbol) { "1.0.0" }
26
+ expect_not_environment_metadata(:_test_symbol)
27
+ expect_not_environment_metadata("_test_symbol")
28
+ end
29
+ expect(logs).to contains_log(
30
+ :error,
31
+ "Unable to report on environment metadata: Unsupported value type for :_test_symbol"
32
+ )
33
+ end
34
+ end
35
+
36
+ context "when the key is nil" do
37
+ it "does not set the value" do
38
+ logs =
39
+ capture_logs do
40
+ report(nil) { "1" }
41
+ expect_not_environment_metadata(nil)
42
+ end
43
+ expect(logs).to contains_log(
44
+ :error,
45
+ "Unable to report on environment metadata: Unsupported value type for nil"
46
+ )
47
+ end
48
+ end
49
+
50
+ context "when the value is true or false" do
51
+ it "reports true or false as Strings" do
52
+ logs =
53
+ capture_logs do
54
+ report("_test_true") { true }
55
+ report("_test_false") { false }
56
+ expect_environment_metadata("_test_true", "true")
57
+ expect_environment_metadata("_test_false", "false")
58
+ end
59
+ expect(logs).to be_empty
60
+ end
61
+ end
62
+
63
+ context "when the value is nil" do
64
+ it "does not set the value" do
65
+ logs =
66
+ capture_logs do
67
+ report("_test_ruby_version") { nil }
68
+ expect_not_environment_metadata("_test_ruby_version")
69
+ end
70
+ expect(logs).to contains_log(
71
+ :error,
72
+ "Unable to report on environment metadata \"_test_ruby_version\": " \
73
+ "Unsupported value type for nil"
74
+ )
75
+ end
76
+ end
77
+
78
+ context "when the value block raises an error" do
79
+ it "does not re-raise the error and writes it to the log" do
80
+ logs =
81
+ capture_logs do
82
+ report("_test_error") { raise "uh oh" }
83
+ expect_not_environment_metadata("_test_error")
84
+ end
85
+ expect(logs).to contains_log(
86
+ :error,
87
+ "Unable to report on environment metadata \"_test_error\":\n" \
88
+ "RuntimeError: uh oh"
89
+ )
90
+ end
91
+ end
92
+
93
+ context "when something unforseen errors" do
94
+ it "does not re-raise the error and writes it to the log" do
95
+ klass = Class.new do
96
+ def inspect
97
+ raise "inspect error"
98
+ end
99
+ end
100
+
101
+ logs =
102
+ capture_logs do
103
+ report(klass.new) { raise "value error" }
104
+ expect(Appsignal::Extension).to_not have_received(:set_environment_metadata)
105
+ end
106
+ expect(logs).to contains_log(
107
+ :error,
108
+ "Unable to report on environment metadata:\n" \
109
+ "RuntimeError: inspect error"
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ".report_supported_gems" do
116
+ it "reports about all AppSignal supported gems in the bundle" do
117
+ logs = capture_logs { described_class.report_supported_gems }
118
+
119
+ expect(logs).to be_empty
120
+
121
+ bundle_gem_specs = ::Bundler.rubygems.all_specs
122
+ rack_spec = bundle_gem_specs.find { |s| s.name == "rack" }
123
+ rake_spec = bundle_gem_specs.find { |s| s.name == "rake" }
124
+ expect_environment_metadata("ruby_rack_version", rack_spec.version.to_s)
125
+ expect_environment_metadata("ruby_rake_version", rake_spec.version.to_s)
126
+ expect(rack_spec.version.to_s).to_not be_empty
127
+ expect(rake_spec.version.to_s).to_not be_empty
128
+ end
129
+
130
+ context "when something unforseen errors" do
131
+ it "does not re-raise the error and writes it to the log" do
132
+ expect(Bundler).to receive(:rubygems).and_raise(RuntimeError, "bundler error")
133
+
134
+ logs = capture_logs { described_class.report_supported_gems }
135
+ expect(logs).to contains_log(
136
+ :error,
137
+ "Unable to report supported gems:\nRuntimeError: bundler error"
138
+ )
139
+ end
140
+ end
141
+ end
142
+
143
+ describe ".report_enabled" do
144
+ it "reports a feature being enabled" do
145
+ logs = capture_logs { described_class.report_enabled("a_test") }
146
+
147
+ expect(logs).to be_empty
148
+ expect_environment_metadata("ruby_a_test_enabled", "true")
149
+ end
150
+
151
+ context "when something unforseen errors" do
152
+ it "does not re-raise the error and writes it to the log" do
153
+ klass = Class.new do
154
+ def to_s
155
+ raise "to_s error"
156
+ end
157
+ end
158
+
159
+ logs = capture_logs { described_class.report_enabled(klass.new) }
160
+ expect(logs).to contains_log(
161
+ :error,
162
+ "Unable to report integration enabled:\nRuntimeError: to_s error"
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
@@ -36,6 +36,7 @@ describe Appsignal::Hooks::DelayedJobHook do
36
36
  let(:time) { Time.parse("01-01-2001 10:01:00UTC") }
37
37
  let(:created_at) { time - 3600 }
38
38
  let(:run_at) { time - 3600 }
39
+ let(:payload_object) { double(:args => args) }
39
40
  let(:job_data) do
40
41
  {
41
42
  :id => 123,
@@ -45,38 +46,40 @@ describe Appsignal::Hooks::DelayedJobHook do
45
46
  :queue => "default",
46
47
  :created_at => created_at,
47
48
  :run_at => run_at,
48
- :payload_object => double(:args => args)
49
+ :payload_object => payload_object
49
50
  }
50
51
  end
51
52
  let(:args) { ["argument"] }
52
53
  let(:job) { double(job_data) }
53
54
  let(:invoked_block) { proc {} }
54
55
 
55
- context "with a normal call" do
56
- let(:default_params) do
57
- {
58
- :class => "TestClass",
59
- :method => "perform",
60
- :metadata => {
61
- :priority => 1,
62
- :attempts => 1,
63
- :queue => "default",
64
- :id => "123"
65
- },
66
- :params => args,
67
- :queue_start => run_at
68
- }
69
- end
70
- after do
71
- Timecop.freeze(time) do
56
+ def perform
57
+ Timecop.freeze(time) do
58
+ keep_transactions do
72
59
  plugin.invoke_with_instrumentation(job, invoked_block)
73
60
  end
74
61
  end
62
+ end
75
63
 
76
- it "wraps it in a transaction with the correct params" do
77
- expect(Appsignal).to receive(:monitor_transaction).with(
78
- "perform_job.delayed_job",
79
- default_params.merge(:params => ["argument"])
64
+ context "with a normal call" do
65
+ it "wraps it in a transaction" do
66
+ perform
67
+ transaction_data = last_transaction.to_h
68
+ expect(transaction_data).to include(
69
+ "action" => "TestClass#perform",
70
+ "namespace" => "background_job",
71
+ "error" => nil
72
+ )
73
+ expect(transaction_data["events"].map { |e| e["name"] })
74
+ .to eql(["perform_job.delayed_job"])
75
+ expect(transaction_data["sample_data"]).to include(
76
+ "metadata" => {
77
+ "priority" => 1,
78
+ "attempts" => 1,
79
+ "queue" => "default",
80
+ "id" => "123"
81
+ },
82
+ "params" => ["argument"]
80
83
  )
81
84
  end
82
85
 
@@ -89,14 +92,13 @@ describe Appsignal::Hooks::DelayedJobHook do
89
92
  end
90
93
 
91
94
  it "adds the more complex arguments" do
92
- expect(Appsignal).to receive(:monitor_transaction).with(
93
- "perform_job.delayed_job",
94
- default_params.merge(
95
- :params => {
96
- :foo => "Foo",
97
- :bar => "Bar"
98
- }
99
- )
95
+ perform
96
+ transaction_data = last_transaction.to_h
97
+ expect(transaction_data["sample_data"]).to include(
98
+ "params" => {
99
+ "foo" => "Foo",
100
+ "bar" => "Bar"
101
+ }
100
102
  )
101
103
  end
102
104
 
@@ -107,14 +109,13 @@ describe Appsignal::Hooks::DelayedJobHook do
107
109
  end
108
110
 
109
111
  it "filters selected arguments" do
110
- expect(Appsignal).to receive(:monitor_transaction).with(
111
- "perform_job.delayed_job",
112
- default_params.merge(
113
- :params => {
114
- :foo => "[FILTERED]",
115
- :bar => "Bar"
116
- }
117
- )
112
+ perform
113
+ transaction_data = last_transaction.to_h
114
+ expect(transaction_data["sample_data"]).to include(
115
+ "params" => {
116
+ "foo" => "[FILTERED]",
117
+ "bar" => "Bar"
118
+ }
118
119
  )
119
120
  end
120
121
  end
@@ -124,98 +125,135 @@ describe Appsignal::Hooks::DelayedJobHook do
124
125
  let(:run_at) { Time.parse("2017-01-01 10:01:00UTC") }
125
126
 
126
127
  it "reports queue_start with run_at time" do
128
+ # TODO: Not available in transaction.to_h yet.
129
+ # https://github.com/appsignal/appsignal-agent/issues/293
127
130
  expect(Appsignal).to receive(:monitor_transaction).with(
128
131
  "perform_job.delayed_job",
129
- default_params.merge(:queue_start => run_at)
130
- )
132
+ a_hash_including(:queue_start => run_at)
133
+ ).and_call_original
134
+ perform
131
135
  end
132
136
  end
133
137
 
134
- context "with custom name call" do
138
+ context "with class method job" do
135
139
  let(:job_data) do
136
- {
137
- :payload_object => double(
138
- :appsignal_name => "CustomClass#perform",
139
- :args => args
140
- ),
141
- :id => "123",
142
- :name => "TestClass#perform",
143
- :priority => 1,
144
- :attempts => 1,
145
- :queue => "default",
146
- :created_at => created_at,
147
- :run_at => run_at
148
- }
149
- end
150
- let(:default_params) do
151
- {
152
- :class => "CustomClass",
153
- :method => "perform",
154
- :metadata => {
155
- :priority => 1,
156
- :attempts => 1,
157
- :queue => "default",
158
- :id => "123"
159
- },
160
- :queue_start => run_at
161
- }
140
+ { :name => "CustomClassMethod.perform", :payload_object => payload_object }
162
141
  end
163
142
 
164
- it "wraps it in a transaction with the correct params" do
165
- expect(Appsignal).to receive(:monitor_transaction).with(
166
- "perform_job.delayed_job",
167
- default_params.merge(
168
- :params => ["argument"]
169
- )
170
- )
143
+ it "wraps it in a transaction using the class method job name" do
144
+ perform
145
+ expect(last_transaction.to_h["action"]).to eql("CustomClassMethod.perform")
171
146
  end
147
+ end
172
148
 
173
- context "with more complex params" do
174
- let(:args) do
175
- {
176
- :foo => "Foo",
177
- :bar => "Bar"
178
- }
179
- end
149
+ context "with custom name call" do
150
+ before { perform }
180
151
 
181
- it "adds the more complex arguments" do
182
- expect(Appsignal).to receive(:monitor_transaction).with(
183
- "perform_job.delayed_job",
184
- default_params.merge(
185
- :params => {
186
- :foo => "Foo",
187
- :bar => "Bar"
188
- }
189
- )
190
- )
152
+ context "with appsignal_name defined" do
153
+ context "with payload_object being an object" do
154
+ context "with value" do
155
+ let(:payload_object) { double(:appsignal_name => "CustomClass#perform") }
156
+
157
+ it "wraps it in a transaction using the custom name" do
158
+ expect(last_transaction.to_h["action"]).to eql("CustomClass#perform")
159
+ end
160
+ end
161
+
162
+ context "with non-String value" do
163
+ let(:payload_object) { double(:appsignal_name => Object.new) }
164
+
165
+ it "wraps it in a transaction using the original job name" do
166
+ expect(last_transaction.to_h["action"]).to eql("TestClass#perform")
167
+ end
168
+ end
169
+
170
+ context "with class method name as job" do
171
+ let(:payload_object) { double(:appsignal_name => "CustomClassMethod.perform") }
172
+
173
+ it "wraps it in a transaction using the custom name" do
174
+ perform
175
+ expect(last_transaction.to_h["action"]).to eql("CustomClassMethod.perform")
176
+ end
177
+ end
191
178
  end
192
179
 
193
- context "with parameter filtering" do
194
- before do
195
- Appsignal.config = project_fixture_config("production")
196
- Appsignal.config[:filter_parameters] = ["foo"]
180
+ context "with payload_object being a Hash" do
181
+ context "with value" do
182
+ let(:payload_object) { double(:appsignal_name => "CustomClassHash#perform") }
183
+
184
+ it "wraps it in a transaction using the custom name" do
185
+ expect(last_transaction.to_h["action"]).to eql("CustomClassHash#perform")
186
+ end
197
187
  end
198
188
 
199
- it "filters selected arguments" do
200
- expect(Appsignal).to receive(:monitor_transaction).with(
201
- "perform_job.delayed_job",
202
- default_params.merge(
203
- :params => {
204
- :foo => "[FILTERED]",
205
- :bar => "Bar"
206
- }
207
- )
208
- )
189
+ context "with non-String value" do
190
+ let(:payload_object) { double(:appsignal_name => Object.new) }
191
+
192
+ it "wraps it in a transaction using the original job name" do
193
+ expect(last_transaction.to_h["action"]).to eql("TestClass#perform")
194
+ end
195
+ end
196
+
197
+ context "with class method name as job" do
198
+ let(:payload_object) { { :appsignal_name => "CustomClassMethod.perform" } }
199
+
200
+ it "wraps it in a transaction using the custom name" do
201
+ perform
202
+ expect(last_transaction.to_h["action"]).to eql("CustomClassMethod.perform")
203
+ end
204
+ end
205
+ end
206
+
207
+ context "with payload_object being acting like a Hash and returning a non-String value" do
208
+ class ClassActingAsHash
209
+ def self.[](_key)
210
+ Object.new
211
+ end
212
+
213
+ def self.appsignal_name
214
+ "ClassActingAsHash#perform"
215
+ end
216
+ end
217
+ let(:payload_object) { ClassActingAsHash }
218
+
219
+ # We check for hash values before object values
220
+ # this means ClassActingAsHash returns `Object.new` instead
221
+ # of `self.appsignal_name`. Since this isn't a valid `String`
222
+ # we return the default job name as action name.
223
+ it "wraps it in a transaction using the original job name" do
224
+ expect(last_transaction.to_h["action"]).to eql("TestClass#perform")
209
225
  end
210
226
  end
211
227
  end
212
228
  end
213
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
242
+ let(:job_data) do
243
+ { :name => "Banana", :payload_object => payload_object }
244
+ end
245
+
246
+ it "wraps it in a transaction using the class method job name" do
247
+ perform
248
+ expect(last_transaction.to_h["action"]).to eql("unknown")
249
+ end
250
+ end
251
+
214
252
  if active_job_present?
215
253
  require "active_job"
216
254
 
217
255
  context "when wrapped by ActiveJob" do
218
- let(:wrapped_job) do
256
+ let(:payload_object) do
219
257
  ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(
220
258
  "arguments" => args,
221
259
  "job_class" => "TestClass",
@@ -233,32 +271,30 @@ describe Appsignal::Hooks::DelayedJobHook do
233
271
  :queue => "default",
234
272
  :created_at => created_at,
235
273
  :run_at => run_at,
236
- :payload_object => wrapped_job
274
+ :payload_object => payload_object
237
275
  )
238
276
  end
239
- let(:default_params) do
240
- {
241
- :class => "TestClass",
242
- :method => "perform",
243
- :metadata => {
244
- :priority => 1,
245
- :attempts => 1,
246
- :queue => "default",
247
- :id => "123"
248
- },
249
- :queue_start => run_at,
250
- :params => args
251
- }
252
- end
253
277
  let(:args) { ["activejob_argument"] }
254
278
 
255
- context "with simple params" do
256
- it "wraps it in a transaction with the correct params" do
257
- expect(Appsignal).to receive(:monitor_transaction).with(
258
- "perform_job.delayed_job",
259
- default_params.merge(:params => ["activejob_argument"])
260
- )
261
- end
279
+ it "wraps it in a transaction with the correct params" do
280
+ perform
281
+ transaction_data = last_transaction.to_h
282
+ expect(transaction_data).to include(
283
+ "action" => "TestClass#perform",
284
+ "namespace" => "background_job",
285
+ "error" => nil
286
+ )
287
+ expect(transaction_data["events"].map { |e| e["name"] })
288
+ .to eql(["perform_job.delayed_job"])
289
+ expect(transaction_data["sample_data"]).to include(
290
+ "metadata" => {
291
+ "priority" => 1,
292
+ "attempts" => 1,
293
+ "queue" => "default",
294
+ "id" => "123"
295
+ },
296
+ "params" => ["activejob_argument"]
297
+ )
262
298
  end
263
299
 
264
300
  context "with more complex params" do
@@ -270,14 +306,14 @@ describe Appsignal::Hooks::DelayedJobHook do
270
306
  end
271
307
 
272
308
  it "adds the more complex arguments" do
273
- expect(Appsignal).to receive(:monitor_transaction).with(
274
- "perform_job.delayed_job",
275
- default_params.merge(
276
- :params => {
277
- :foo => "Foo",
278
- :bar => "Bar"
279
- }
280
- )
309
+ perform
310
+ transaction_data = last_transaction.to_h
311
+ expect(transaction_data).to include("action" => "TestClass#perform")
312
+ expect(transaction_data["sample_data"]).to include(
313
+ "params" => {
314
+ "foo" => "Foo",
315
+ "bar" => "Bar"
316
+ }
281
317
  )
282
318
  end
283
319
 
@@ -288,14 +324,14 @@ describe Appsignal::Hooks::DelayedJobHook do
288
324
  end
289
325
 
290
326
  it "filters selected arguments" do
291
- expect(Appsignal).to receive(:monitor_transaction).with(
292
- "perform_job.delayed_job",
293
- default_params.merge(
294
- :params => {
295
- :foo => "[FILTERED]",
296
- :bar => "Bar"
297
- }
298
- )
327
+ perform
328
+ transaction_data = last_transaction.to_h
329
+ expect(transaction_data).to include("action" => "TestClass#perform")
330
+ expect(transaction_data["sample_data"]).to include(
331
+ "params" => {
332
+ "foo" => "[FILTERED]",
333
+ "bar" => "Bar"
334
+ }
299
335
  )
300
336
  end
301
337
  end
@@ -307,8 +343,9 @@ describe Appsignal::Hooks::DelayedJobHook do
307
343
  it "reports queue_start with run_at time" do
308
344
  expect(Appsignal).to receive(:monitor_transaction).with(
309
345
  "perform_job.delayed_job",
310
- default_params.merge(:queue_start => run_at)
311
- )
346
+ a_hash_including(:queue_start => run_at)
347
+ ).and_call_original
348
+ perform
312
349
  end
313
350
  end
314
351
  end
@@ -316,33 +353,28 @@ describe Appsignal::Hooks::DelayedJobHook do
316
353
  end
317
354
 
318
355
  context "with an erroring call" do
319
- let(:error) { ExampleException }
320
- let(:transaction) do
321
- Appsignal::Transaction.new(
322
- SecureRandom.uuid,
323
- Appsignal::Transaction::BACKGROUND_JOB,
324
- Appsignal::Transaction::GenericRequest.new({})
325
- )
326
- end
356
+ let(:error) { ExampleException.new("uh oh") }
327
357
  before do
328
358
  expect(invoked_block).to receive(:call).and_raise(error)
329
-
330
- allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
331
- expect(Appsignal::Transaction).to receive(:create)
332
- .with(
333
- kind_of(String),
334
- Appsignal::Transaction::BACKGROUND_JOB,
335
- kind_of(Appsignal::Transaction::GenericRequest)
336
- ).and_return(transaction)
337
359
  end
338
360
 
339
361
  it "adds the error to the transaction" do
340
- expect(transaction).to receive(:set_error).with(error)
341
- expect(transaction).to receive(:complete)
342
-
343
362
  expect do
344
- plugin.invoke_with_instrumentation(job, invoked_block)
363
+ perform
345
364
  end.to raise_error(error)
365
+
366
+ transaction_data = last_transaction.to_h
367
+ expect(transaction_data).to include(
368
+ "action" => "TestClass#perform",
369
+ "namespace" => "background_job",
370
+ "error" => {
371
+ "name" => "ExampleException",
372
+ "message" => "uh oh",
373
+ # TODO: backtrace should be an Array of Strings
374
+ # https://github.com/appsignal/appsignal-agent/issues/294
375
+ "backtrace" => kind_of(String)
376
+ }
377
+ )
346
378
  end
347
379
  end
348
380
  end