appsignal 2.10.6 → 2.11.0.alpha.1

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