appsignal 3.9.3 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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