appsignal 3.9.3-java → 3.11.0-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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -19
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +180 -0
  5. data/Gemfile +1 -0
  6. data/README.md +0 -1
  7. data/Rakefile +1 -1
  8. data/benchmark.rake +99 -42
  9. data/build_matrix.yml +10 -12
  10. data/gemfiles/webmachine1.gemfile +5 -4
  11. data/lib/appsignal/cli/demo.rb +0 -1
  12. data/lib/appsignal/config.rb +57 -97
  13. data/lib/appsignal/demo.rb +15 -20
  14. data/lib/appsignal/environment.rb +6 -1
  15. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +1 -0
  16. data/lib/appsignal/event_formatter.rb +3 -2
  17. data/lib/appsignal/helpers/instrumentation.rb +490 -16
  18. data/lib/appsignal/hooks/action_cable.rb +21 -16
  19. data/lib/appsignal/hooks/active_job.rb +15 -14
  20. data/lib/appsignal/hooks/delayed_job.rb +1 -1
  21. data/lib/appsignal/hooks/shoryuken.rb +3 -63
  22. data/lib/appsignal/integrations/action_cable.rb +5 -7
  23. data/lib/appsignal/integrations/active_support_notifications.rb +1 -0
  24. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +36 -35
  25. data/lib/appsignal/integrations/data_mapper.rb +1 -0
  26. data/lib/appsignal/integrations/delayed_job_plugin.rb +27 -33
  27. data/lib/appsignal/integrations/dry_monitor.rb +1 -0
  28. data/lib/appsignal/integrations/excon.rb +1 -0
  29. data/lib/appsignal/integrations/http.rb +1 -0
  30. data/lib/appsignal/integrations/net_http.rb +1 -0
  31. data/lib/appsignal/integrations/object.rb +6 -0
  32. data/lib/appsignal/integrations/padrino.rb +21 -25
  33. data/lib/appsignal/integrations/que.rb +13 -20
  34. data/lib/appsignal/integrations/railtie.rb +1 -1
  35. data/lib/appsignal/integrations/rake.rb +45 -15
  36. data/lib/appsignal/integrations/redis.rb +1 -0
  37. data/lib/appsignal/integrations/redis_client.rb +1 -0
  38. data/lib/appsignal/integrations/resque.rb +2 -5
  39. data/lib/appsignal/integrations/shoryuken.rb +75 -0
  40. data/lib/appsignal/integrations/sidekiq.rb +7 -25
  41. data/lib/appsignal/integrations/unicorn.rb +1 -0
  42. data/lib/appsignal/integrations/webmachine.rb +12 -9
  43. data/lib/appsignal/logger.rb +7 -3
  44. data/lib/appsignal/probes/helpers.rb +1 -0
  45. data/lib/appsignal/probes/mri.rb +1 -0
  46. data/lib/appsignal/probes/sidekiq.rb +1 -0
  47. data/lib/appsignal/probes.rb +3 -0
  48. data/lib/appsignal/rack/abstract_middleware.rb +67 -24
  49. data/lib/appsignal/rack/body_wrapper.rb +143 -0
  50. data/lib/appsignal/rack/event_handler.rb +39 -8
  51. data/lib/appsignal/rack/generic_instrumentation.rb +6 -4
  52. data/lib/appsignal/rack/grape_middleware.rb +3 -2
  53. data/lib/appsignal/rack/hanami_middleware.rb +1 -1
  54. data/lib/appsignal/rack/instrumentation_middleware.rb +62 -0
  55. data/lib/appsignal/rack/rails_instrumentation.rb +1 -3
  56. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -3
  57. data/lib/appsignal/rack/streaming_listener.rb +14 -59
  58. data/lib/appsignal/rack.rb +60 -0
  59. data/lib/appsignal/span.rb +1 -0
  60. data/lib/appsignal/transaction.rb +353 -104
  61. data/lib/appsignal/utils/data.rb +0 -1
  62. data/lib/appsignal/utils/hash_sanitizer.rb +0 -1
  63. data/lib/appsignal/utils/integration_logger.rb +0 -13
  64. data/lib/appsignal/utils/integration_memory_logger.rb +0 -13
  65. data/lib/appsignal/utils/json.rb +0 -1
  66. data/lib/appsignal/utils/query_params_sanitizer.rb +0 -1
  67. data/lib/appsignal/utils/stdout_and_logger_message.rb +0 -1
  68. data/lib/appsignal/utils.rb +6 -0
  69. data/lib/appsignal/version.rb +1 -1
  70. data/lib/appsignal.rb +9 -6
  71. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  72. data/spec/lib/appsignal/config_spec.rb +139 -43
  73. data/spec/lib/appsignal/hooks/action_cable_spec.rb +43 -74
  74. data/spec/lib/appsignal/hooks/activejob_spec.rb +9 -0
  75. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +2 -443
  76. data/spec/lib/appsignal/hooks/rake_spec.rb +100 -17
  77. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +0 -171
  78. data/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb +459 -0
  79. data/spec/lib/appsignal/integrations/padrino_spec.rb +181 -131
  80. data/spec/lib/appsignal/integrations/que_spec.rb +3 -4
  81. data/spec/lib/appsignal/integrations/shoryuken_spec.rb +167 -0
  82. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +4 -4
  83. data/spec/lib/appsignal/integrations/sinatra_spec.rb +10 -2
  84. data/spec/lib/appsignal/integrations/webmachine_spec.rb +77 -17
  85. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +144 -11
  86. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +263 -0
  87. data/spec/lib/appsignal/rack/event_handler_spec.rb +81 -10
  88. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +70 -17
  89. data/spec/lib/appsignal/rack/grape_middleware_spec.rb +1 -1
  90. data/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb +38 -0
  91. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +4 -2
  92. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +43 -120
  93. data/spec/lib/appsignal/rack_spec.rb +63 -0
  94. data/spec/lib/appsignal/transaction_spec.rb +1675 -953
  95. data/spec/lib/appsignal/utils/integration_logger_spec.rb +12 -16
  96. data/spec/lib/appsignal/utils/integration_memory_logger_spec.rb +0 -10
  97. data/spec/lib/appsignal_spec.rb +517 -13
  98. data/spec/support/helpers/transaction_helpers.rb +44 -20
  99. data/spec/support/matchers/transaction.rb +15 -1
  100. data/spec/support/mocks/dummy_app.rb +1 -1
  101. data/spec/support/testing.rb +1 -1
  102. metadata +12 -4
  103. data/support/check_versions +0 -22
@@ -1,174 +1,3 @@
1
- describe Appsignal::Hooks::ShoryukenMiddleware do
2
- class DemoShoryukenWorker
3
- end
4
-
5
- let(:time) { "2010-01-01 10:01:00UTC" }
6
- let(:worker_instance) { DemoShoryukenWorker.new }
7
- let(:queue) { "some-funky-queue-name" }
8
- let(:sqs_msg) { double(:message_id => "msg1", :attributes => {}) }
9
- let(:body) { {} }
10
- before(:context) { start_agent }
11
- around { |example| keep_transactions { example.run } }
12
-
13
- def perform_shoryuken_job(&block)
14
- block ||= lambda {}
15
- Timecop.freeze(Time.parse(time)) do
16
- Appsignal::Hooks::ShoryukenMiddleware.new.call(
17
- worker_instance,
18
- queue,
19
- sqs_msg,
20
- body,
21
- &block
22
- )
23
- end
24
- end
25
-
26
- context "with a performance call" do
27
- let(:sent_timestamp) { Time.parse("1976-11-18 0:00:00UTC").to_i * 1000 }
28
- let(:sqs_msg) do
29
- double(:message_id => "msg1", :attributes => { "SentTimestamp" => sent_timestamp })
30
- end
31
-
32
- context "with complex argument" do
33
- let(:body) { { :foo => "Foo", :bar => "Bar" } }
34
-
35
- it "wraps the job in a transaction with the correct params" do
36
- allow_any_instance_of(Appsignal::Transaction).to receive(:set_queue_start).and_call_original
37
- expect { perform_shoryuken_job }.to change { created_transactions.length }.by(1)
38
-
39
- transaction = last_transaction
40
- expect(transaction).to have_id
41
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
42
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
43
- expect(transaction).to_not have_error
44
- expect(transaction).to include_event(
45
- "body" => "",
46
- "body_format" => Appsignal::EventFormatter::DEFAULT,
47
- "count" => 1,
48
- "name" => "perform_job.shoryuken",
49
- "title" => ""
50
- )
51
- expect(transaction).to include_params("foo" => "Foo", "bar" => "Bar")
52
- expect(transaction).to include_sample_metadata(
53
- "message_id" => "msg1",
54
- "queue" => queue,
55
- "SentTimestamp" => sent_timestamp
56
- )
57
- expect(transaction).to have_queue_start(sent_timestamp)
58
- expect(transaction).to be_completed
59
- end
60
-
61
- context "with parameter filtering" do
62
- before do
63
- Appsignal.config = project_fixture_config("production")
64
- Appsignal.config[:filter_parameters] = ["foo"]
65
- end
66
- after do
67
- Appsignal.config[:filter_parameters] = []
68
- end
69
-
70
- it "filters selected arguments" do
71
- perform_shoryuken_job
72
-
73
- expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
74
- end
75
- end
76
- end
77
-
78
- context "with a string as an argument" do
79
- let(:body) { "foo bar" }
80
-
81
- it "handles string arguments" do
82
- perform_shoryuken_job
83
-
84
- expect(last_transaction).to include_params("params" => body)
85
- end
86
- end
87
-
88
- context "with primitive type as argument" do
89
- let(:body) { 1 }
90
-
91
- it "handles primitive types as arguments" do
92
- perform_shoryuken_job
93
-
94
- expect(last_transaction).to include_params("params" => body)
95
- end
96
- end
97
- end
98
-
99
- context "with exception" do
100
- it "sets the exception on the transaction" do
101
- expect do
102
- expect do
103
- perform_shoryuken_job { raise ExampleException, "error message" }
104
- end.to raise_error(ExampleException)
105
- end.to change { created_transactions.length }.by(1)
106
-
107
- transaction = last_transaction
108
- expect(transaction).to have_id
109
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
110
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
111
- expect(transaction).to have_error("ExampleException", "error message")
112
- expect(transaction).to be_completed
113
- end
114
- end
115
-
116
- context "with batched jobs" do
117
- let(:sqs_msg) do
118
- [
119
- double(
120
- :message_id => "msg2",
121
- :attributes => {
122
- "SentTimestamp" => (Time.parse("1976-11-18 01:00:00UTC").to_i * 1000).to_s
123
- }
124
- ),
125
- double(
126
- :message_id => "msg1",
127
- :attributes => { "SentTimestamp" => sent_timestamp.to_s }
128
- )
129
- ]
130
- end
131
- let(:body) do
132
- [
133
- "foo bar",
134
- { :id => "123", :foo => "Foo", :bar => "Bar" }
135
- ]
136
- end
137
- let(:sent_timestamp) { Time.parse("1976-11-18 01:00:00UTC").to_i * 1000 }
138
-
139
- it "creates a transaction for the batch" do
140
- allow_any_instance_of(Appsignal::Transaction).to receive(:set_queue_start).and_call_original
141
- expect do
142
- perform_shoryuken_job {} # rubocop:disable Lint/EmptyBlock
143
- end.to change { created_transactions.length }.by(1)
144
-
145
- transaction = last_transaction
146
- expect(transaction).to have_id
147
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
148
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
149
- expect(transaction).to_not have_error
150
- expect(transaction).to include_event(
151
- "body" => "",
152
- "body_format" => Appsignal::EventFormatter::DEFAULT,
153
- "count" => 1,
154
- "name" => "perform_job.shoryuken",
155
- "title" => ""
156
- )
157
- expect(transaction).to include_params(
158
- "msg2" => "foo bar",
159
- "msg1" => { "id" => "123", "foo" => "Foo", "bar" => "Bar" }
160
- )
161
- expect(transaction).to include_sample_metadata(
162
- "batch" => true,
163
- "queue" => "some-funky-queue-name",
164
- "SentTimestamp" => sent_timestamp.to_s # Earliest/oldest timestamp from messages
165
- )
166
- # Queue time based on earliest/oldest timestamp from messages
167
- expect(transaction).to have_queue_start(sent_timestamp)
168
- end
169
- end
170
- end
171
-
172
1
  describe Appsignal::Hooks::ShoryukenHook do
173
2
  context "with shoryuken" do
174
3
  before(:context) do
@@ -0,0 +1,459 @@
1
+ describe "Appsignal::Integrations::DelayedJobHook" do
2
+ before(:context) do
3
+ module Delayed
4
+ class Plugin
5
+ def self.callbacks
6
+ end
7
+ end
8
+
9
+ class Worker
10
+ def self.plugins
11
+ @plugins ||= []
12
+ end
13
+ end
14
+ end
15
+ require "appsignal/integrations/delayed_job_plugin"
16
+ end
17
+ after(:context) { Object.send(:remove_const, :Delayed) }
18
+ before { start_agent }
19
+
20
+ # We haven't found a way to test the hooks, we'll have to do that manually
21
+
22
+ describe ".invoke_with_instrumentation" do
23
+ let(:plugin) { Appsignal::Integrations::DelayedJobPlugin }
24
+ let(:time) { Time.parse("01-01-2001 10:01:00UTC") }
25
+ let(:created_at) { time - 3600 }
26
+ let(:run_at) { time - 3600 }
27
+ let(:payload_object) { double(:args => args) }
28
+ let(:job_data) do
29
+ {
30
+ :id => 123,
31
+ :name => "TestClass#perform",
32
+ :priority => 1,
33
+ :attempts => 1,
34
+ :queue => "default",
35
+ :created_at => created_at,
36
+ :run_at => run_at,
37
+ :payload_object => payload_object
38
+ }
39
+ end
40
+ let(:args) { ["argument"] }
41
+ let(:job) { double(job_data) }
42
+ let(:invoked_block) { proc {} }
43
+
44
+ def perform
45
+ Timecop.freeze(time) do
46
+ keep_transactions do
47
+ plugin.invoke_with_instrumentation(job, invoked_block)
48
+ end
49
+ end
50
+ end
51
+
52
+ context "with a normal call" do
53
+ it "wraps it in a transaction" do
54
+ perform
55
+
56
+ transaction = last_transaction
57
+ expect(transaction).to have_namespace("background_job")
58
+ expect(transaction).to have_action("TestClass#perform")
59
+ expect(transaction).to_not have_error
60
+ expect(transaction).to include_event(:name => "perform_job.delayed_job")
61
+ expect(transaction).to include_tags(
62
+ "priority" => 1,
63
+ "attempts" => 1,
64
+ "queue" => "default",
65
+ "id" => "123"
66
+ )
67
+ expect(transaction).to include_params(["argument"])
68
+ end
69
+
70
+ context "with more complex params" do
71
+ let(:args) do
72
+ {
73
+ :foo => "Foo",
74
+ :bar => "Bar"
75
+ }
76
+ end
77
+
78
+ it "adds the more complex arguments" do
79
+ perform
80
+
81
+ expect(last_transaction).to include_params("foo" => "Foo", "bar" => "Bar")
82
+ end
83
+
84
+ context "with parameter filtering" do
85
+ before do
86
+ Appsignal.config = project_fixture_config("production")
87
+ Appsignal.config[:filter_parameters] = ["foo"]
88
+ end
89
+
90
+ it "filters selected arguments" do
91
+ perform
92
+
93
+ expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
94
+ end
95
+ end
96
+ end
97
+
98
+ context "with run_at in the future" do
99
+ let(:run_at) { Time.parse("2017-01-01 10:01:00UTC") }
100
+
101
+ it "reports queue_start with run_at time" do
102
+ perform
103
+
104
+ expect(last_transaction).to have_queue_start(run_at.to_i * 1000)
105
+ end
106
+ end
107
+
108
+ context "with class method job" do
109
+ let(:job_data) do
110
+ { :name => "CustomClassMethod.perform", :payload_object => payload_object }
111
+ end
112
+
113
+ it "wraps it in a transaction using the class method job name" do
114
+ perform
115
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
116
+ end
117
+ end
118
+
119
+ context "with custom name call" do
120
+ before { perform }
121
+
122
+ context "with appsignal_name defined" do
123
+ context "with payload_object being an object" do
124
+ context "with value" do
125
+ let(:payload_object) { double(:appsignal_name => "CustomClass#perform") }
126
+
127
+ it "wraps it in a transaction using the custom name" do
128
+ expect(last_transaction).to have_action("CustomClass#perform")
129
+ end
130
+ end
131
+
132
+ context "with non-String value" do
133
+ let(:payload_object) { double(:appsignal_name => Object.new) }
134
+
135
+ it "wraps it in a transaction using the original job name" do
136
+ expect(last_transaction).to have_action("TestClass#perform")
137
+ end
138
+ end
139
+
140
+ context "with class method name as job" do
141
+ let(:payload_object) { double(:appsignal_name => "CustomClassMethod.perform") }
142
+
143
+ it "wraps it in a transaction using the custom name" do
144
+ perform
145
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
146
+ end
147
+ end
148
+ end
149
+
150
+ context "with payload_object being a Hash" do
151
+ context "with value" do
152
+ let(:payload_object) { double(:appsignal_name => "CustomClassHash#perform") }
153
+
154
+ it "wraps it in a transaction using the custom name" do
155
+ expect(last_transaction).to have_action("CustomClassHash#perform")
156
+ end
157
+ end
158
+
159
+ context "with non-String value" do
160
+ let(:payload_object) { double(:appsignal_name => Object.new) }
161
+
162
+ it "wraps it in a transaction using the original job name" do
163
+ expect(last_transaction).to have_action("TestClass#perform")
164
+ end
165
+ end
166
+
167
+ context "with class method name as job" do
168
+ let(:payload_object) { { :appsignal_name => "CustomClassMethod.perform" } }
169
+
170
+ it "wraps it in a transaction using the custom name" do
171
+ perform
172
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
173
+ end
174
+ end
175
+ end
176
+
177
+ context "with payload_object acting like a Hash and returning a non-String value" do
178
+ class ClassActingAsHash
179
+ def self.[](_key)
180
+ Object.new
181
+ end
182
+
183
+ def self.appsignal_name
184
+ "ClassActingAsHash#perform"
185
+ end
186
+ end
187
+ let(:payload_object) { ClassActingAsHash }
188
+
189
+ # We check for hash values before object values
190
+ # this means ClassActingAsHash returns `Object.new` instead
191
+ # of `self.appsignal_name`. Since this isn't a valid `String`
192
+ # we return the default job name as action name.
193
+ it "wraps it in a transaction using the original job name" do
194
+ expect(last_transaction).to have_action("TestClass#perform")
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ context "with only job class name" do
201
+ let(:job_data) do
202
+ { :name => "Banana", :payload_object => payload_object }
203
+ end
204
+
205
+ it "appends #perform to the class name" do
206
+ perform
207
+ expect(last_transaction).to have_action("Banana#perform")
208
+ end
209
+ end
210
+
211
+ if active_job_present?
212
+ require "active_job"
213
+
214
+ context "when wrapped by ActiveJob" do
215
+ let(:payload_object) do
216
+ ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(
217
+ "arguments" => args,
218
+ "job_class" => "TestClass",
219
+ "job_id" => 123,
220
+ "locale" => :en,
221
+ "queue_name" => "default"
222
+ )
223
+ end
224
+ let(:job) do
225
+ double(
226
+ :id => 123,
227
+ :name => "ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper",
228
+ :priority => 1,
229
+ :attempts => 1,
230
+ :queue => "default",
231
+ :created_at => created_at,
232
+ :run_at => run_at,
233
+ :payload_object => payload_object
234
+ )
235
+ end
236
+ let(:args) { ["activejob_argument"] }
237
+
238
+ it "wraps it in a transaction with the correct params" do
239
+ perform
240
+
241
+ transaction = last_transaction
242
+ expect(transaction).to have_namespace("background_job")
243
+ expect(transaction).to have_action("TestClass#perform")
244
+ expect(transaction).to_not have_error
245
+ expect(transaction).to include_event("name" => "perform_job.delayed_job")
246
+ expect(transaction).to include_tags(
247
+ "priority" => 1,
248
+ "attempts" => 1,
249
+ "queue" => "default",
250
+ "id" => "123"
251
+ )
252
+ expect(transaction).to include_params(["activejob_argument"])
253
+ end
254
+
255
+ context "with more complex params" do
256
+ let(:args) do
257
+ {
258
+ :foo => "Foo",
259
+ :bar => "Bar"
260
+ }
261
+ end
262
+
263
+ it "adds the more complex arguments" do
264
+ perform
265
+ transaction = last_transaction
266
+ expect(transaction).to have_action("TestClass#perform")
267
+ expect(transaction).to include_params(
268
+ "foo" => "Foo",
269
+ "bar" => "Bar"
270
+ )
271
+ end
272
+
273
+ context "with parameter filtering" do
274
+ before do
275
+ Appsignal.config = project_fixture_config("production")
276
+ Appsignal.config[:filter_parameters] = ["foo"]
277
+ end
278
+
279
+ it "filters selected arguments" do
280
+ perform
281
+ transaction = last_transaction
282
+ expect(transaction).to have_action("TestClass#perform")
283
+ expect(transaction).to include_params(
284
+ "foo" => "[FILTERED]",
285
+ "bar" => "Bar"
286
+ )
287
+ end
288
+ end
289
+ end
290
+
291
+ context "with run_at in the future" do
292
+ let(:run_at) { Time.parse("2017-01-01 10:01:00UTC") }
293
+
294
+ it "reports queue_start with run_at time" do
295
+ perform
296
+
297
+ expect(last_transaction).to have_queue_start(run_at.to_i * 1000)
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ context "with an erroring call" do
305
+ let(:error) { ExampleException.new("uh oh") }
306
+ before do
307
+ expect(invoked_block).to receive(:call).and_raise(error)
308
+ end
309
+
310
+ it "adds the error to the transaction" do
311
+ expect do
312
+ perform
313
+ end.to raise_error(error)
314
+
315
+ transaction = last_transaction
316
+ expect(transaction).to have_namespace("background_job")
317
+ expect(transaction).to have_action("TestClass#perform")
318
+ expect(transaction).to have_error("ExampleException", "uh oh")
319
+ end
320
+ end
321
+ end
322
+
323
+ describe ".extract_value" do
324
+ let(:plugin) { Appsignal::Integrations::DelayedJobPlugin }
325
+
326
+ context "for a hash" do
327
+ let(:hash) { { :key => "value", :bool_false => false } }
328
+
329
+ context "when the key exists" do
330
+ subject { plugin.extract_value(hash, :key) }
331
+
332
+ it { is_expected.to eq "value" }
333
+
334
+ context "when the value is false" do
335
+ subject { plugin.extract_value(hash, :bool_false) }
336
+
337
+ it { is_expected.to be false }
338
+ end
339
+ end
340
+
341
+ context "when the key does not exist" do
342
+ subject { plugin.extract_value(hash, :nonexistent_key) }
343
+
344
+ it { is_expected.to be_nil }
345
+
346
+ context "with a default value" do
347
+ subject { plugin.extract_value(hash, :nonexistent_key, 1) }
348
+
349
+ it { is_expected.to eq 1 }
350
+ end
351
+ end
352
+ end
353
+
354
+ context "for a struct" do
355
+ before :context do
356
+ TestStruct = Struct.new(:key)
357
+ end
358
+ let(:struct) { TestStruct.new("value") }
359
+
360
+ context "when the key exists" do
361
+ subject { plugin.extract_value(struct, :key) }
362
+
363
+ it { is_expected.to eq "value" }
364
+ end
365
+
366
+ context "when the key does not exist" do
367
+ subject { plugin.extract_value(struct, :nonexistent_key) }
368
+
369
+ it { is_expected.to be_nil }
370
+
371
+ context "with a default value" do
372
+ subject { plugin.extract_value(struct, :nonexistent_key, 1) }
373
+
374
+ it { is_expected.to eq 1 }
375
+ end
376
+ end
377
+ end
378
+
379
+ context "for a struct with a method" do
380
+ before :context do
381
+ class TestStructClass < Struct.new(:id) # rubocop:disable Style/StructInheritance
382
+ def appsignal_name
383
+ "TestStruct#perform"
384
+ end
385
+
386
+ def bool_false
387
+ false
388
+ end
389
+ end
390
+ end
391
+ let(:struct) { TestStructClass.new("id") }
392
+
393
+ context "when the Struct responds to a method" do
394
+ subject { plugin.extract_value(struct, :appsignal_name) }
395
+
396
+ it "returns the method value" do
397
+ is_expected.to eq "TestStruct#perform"
398
+ end
399
+
400
+ context "when the value is false" do
401
+ subject { plugin.extract_value(struct, :bool_false) }
402
+
403
+ it "returns the method value" do
404
+ is_expected.to be false
405
+ end
406
+ end
407
+ end
408
+
409
+ context "when the key does not exist" do
410
+ subject { plugin.extract_value(struct, :nonexistent_key) }
411
+
412
+ context "without a method with the same name" do
413
+ it "returns nil" do
414
+ is_expected.to be_nil
415
+ end
416
+ end
417
+
418
+ context "with a default value" do
419
+ let(:default_value) { :my_default_value }
420
+ subject { plugin.extract_value(struct, :nonexistent_key, default_value) }
421
+
422
+ it "returns the default value" do
423
+ is_expected.to eq default_value
424
+ end
425
+ end
426
+ end
427
+ end
428
+
429
+ context "for an object" do
430
+ let(:object) { double(:existing_method => "value") }
431
+
432
+ context "when the method exists" do
433
+ subject { plugin.extract_value(object, :existing_method) }
434
+
435
+ it { is_expected.to eq "value" }
436
+ end
437
+
438
+ context "when the method does not exist" do
439
+ subject { plugin.extract_value(object, :nonexistent_method) }
440
+
441
+ it { is_expected.to be_nil }
442
+
443
+ context "and there is a default value" do
444
+ subject { plugin.extract_value(object, :nonexistent_method, 1) }
445
+
446
+ it { is_expected.to eq 1 }
447
+ end
448
+ end
449
+ end
450
+
451
+ context "when we need to call to_s on the value" do
452
+ let(:object) { double(:existing_method => 1) }
453
+
454
+ subject { plugin.extract_value(object, :existing_method, nil, true) }
455
+
456
+ it { is_expected.to eq "1" }
457
+ end
458
+ end
459
+ end