appsignal 2.2.1 → 2.3.0.beta.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/appsignal.gemspec +1 -2
  4. data/ext/agent.yml +11 -11
  5. data/ext/appsignal_extension.c +17 -1
  6. data/lib/appsignal/config.rb +7 -4
  7. data/lib/appsignal/hooks.rb +1 -6
  8. data/lib/appsignal/hooks/action_cable.rb +113 -0
  9. data/lib/appsignal/hooks/active_support_notifications.rb +12 -4
  10. data/lib/appsignal/hooks/shoryuken.rb +11 -11
  11. data/lib/appsignal/hooks/sidekiq.rb +5 -3
  12. data/lib/appsignal/integrations/delayed_job_plugin.rb +5 -1
  13. data/lib/appsignal/integrations/resque_active_job.rb +4 -1
  14. data/lib/appsignal/transaction.rb +40 -13
  15. data/lib/appsignal/version.rb +1 -1
  16. data/spec/lib/appsignal/config_spec.rb +8 -1
  17. data/spec/lib/appsignal/hooks/action_cable_spec.rb +370 -0
  18. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +39 -7
  19. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +179 -34
  20. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +125 -30
  21. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +140 -21
  22. data/spec/lib/appsignal/hooks_spec.rb +0 -21
  23. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +62 -17
  24. data/spec/lib/appsignal/integrations/resque_spec.rb +24 -12
  25. data/spec/lib/appsignal/transaction_spec.rb +230 -91
  26. data/spec/spec_helper.rb +8 -2
  27. data/spec/support/helpers/dependency_helper.rb +4 -0
  28. data/spec/support/helpers/env_helpers.rb +1 -1
  29. data/spec/support/helpers/log_helpers.rb +17 -0
  30. data/spec/support/matchers/contains_log.rb +7 -0
  31. data/spec/support/shared_examples/instrument.rb +2 -2
  32. metadata +13 -20
@@ -1,5 +1,5 @@
1
1
  require "yaml"
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.2.1".freeze
4
+ VERSION = "2.3.0.beta.1".freeze
5
5
  end
@@ -84,8 +84,9 @@ describe Appsignal::Config do
84
84
  expect(config.config_hash).to eq(
85
85
  :debug => false,
86
86
  :log => "file",
87
- :ignore_errors => [],
88
87
  :ignore_actions => [],
88
+ :ignore_errors => [],
89
+ :ignore_namespaces => [],
89
90
  :filter_parameters => [],
90
91
  :instrument_net_http => true,
91
92
  :instrument_redis => true,
@@ -279,6 +280,8 @@ describe Appsignal::Config do
279
280
  ENV["APPSIGNAL_APP_NAME"] = "App name"
280
281
  ENV["APPSIGNAL_DEBUG"] = "true"
281
282
  ENV["APPSIGNAL_IGNORE_ACTIONS"] = "action1,action2"
283
+ ENV["APPSIGNAL_IGNORE_ERRORS"] = "VerySpecificError,AnotherError"
284
+ ENV["APPSIGNAL_IGNORE_NAMESPACES"] = "admin,private_namespace"
282
285
  ENV["APPSIGNAL_INSTRUMENT_NET_HTTP"] = "false"
283
286
  ENV["APPSIGNAL_INSTRUMENT_REDIS"] = "false"
284
287
  ENV["APPSIGNAL_INSTRUMENT_SEQUEL"] = "false"
@@ -294,6 +297,8 @@ describe Appsignal::Config do
294
297
  expect(config[:name]).to eq "App name"
295
298
  expect(config[:debug]).to be_truthy
296
299
  expect(config[:ignore_actions]).to eq %w(action1 action2)
300
+ expect(config[:ignore_errors]).to eq %w(VerySpecificError AnotherError)
301
+ expect(config[:ignore_namespaces]).to eq %w(admin private_namespace)
297
302
  expect(config[:instrument_net_http]).to be_falsey
298
303
  expect(config[:instrument_redis]).to be_falsey
299
304
  expect(config[:instrument_sequel]).to be_falsey
@@ -366,6 +371,7 @@ describe Appsignal::Config do
366
371
  config[:http_proxy] = "http://localhost"
367
372
  config[:ignore_actions] = %w(action1 action2)
368
373
  config[:ignore_errors] = %w(VerySpecificError AnotherError)
374
+ config[:ignore_namespaces] = %w(admin private_namespace)
369
375
  config[:log] = "stdout"
370
376
  config[:log_path] = "/tmp"
371
377
  config[:hostname] = "app1.local"
@@ -391,6 +397,7 @@ describe Appsignal::Config do
391
397
  expect(ENV["_APPSIGNAL_HTTP_PROXY"]).to eq "http://localhost"
392
398
  expect(ENV["_APPSIGNAL_IGNORE_ACTIONS"]).to eq "action1,action2"
393
399
  expect(ENV["_APPSIGNAL_IGNORE_ERRORS"]).to eq "VerySpecificError,AnotherError"
400
+ expect(ENV["_APPSIGNAL_IGNORE_NAMESPACES"]).to eq "admin,private_namespace"
394
401
  expect(ENV["_APPSIGNAL_FILTER_PARAMETERS"]).to eq "password,confirm_password"
395
402
  expect(ENV["_APPSIGNAL_SEND_PARAMS"]).to eq "true"
396
403
  expect(ENV["_APPSIGNAL_RUNNING_IN_CONTAINER"]).to eq "false"
@@ -0,0 +1,370 @@
1
+ describe Appsignal::Hooks::ActionCableHook do
2
+ if DependencyHelper.action_cable_present?
3
+ context "with ActionCable" do
4
+ require "action_cable/engine"
5
+
6
+ describe ".dependencies_present?" do
7
+ subject { described_class.new.dependencies_present? }
8
+
9
+ it "returns true" do
10
+ is_expected.to be_truthy
11
+ end
12
+ end
13
+
14
+ describe ActionCable::Channel::Base do
15
+ let(:transaction) do
16
+ Appsignal::Transaction.new(
17
+ transaction_id,
18
+ Appsignal::Transaction::ACTION_CABLE,
19
+ ActionDispatch::Request.new(env)
20
+ )
21
+ end
22
+ let(:channel) do
23
+ Class.new(ActionCable::Channel::Base) do
24
+ def speak(_data)
25
+ end
26
+
27
+ def self.to_s
28
+ "MyChannel"
29
+ end
30
+ end
31
+ end
32
+ let(:log) { StringIO.new }
33
+ let(:server) do
34
+ ActionCable::Server::Base.new.tap do |s|
35
+ s.config.logger = ActiveSupport::Logger.new(log)
36
+ end
37
+ end
38
+ let(:connection) { ActionCable::Connection::Base.new(server, env) }
39
+ let(:identifier) { { :channel => "MyChannel" }.to_json }
40
+ let(:params) { {} }
41
+ let(:request_id) { SecureRandom.uuid }
42
+ let(:transaction_id) { request_id }
43
+ let(:env) do
44
+ http_request_env_with_data("action_dispatch.request_id" => request_id, :params => params)
45
+ end
46
+ let(:instance) { channel.new(connection, identifier, params) }
47
+ subject { transaction.to_h }
48
+ before do
49
+ start_agent
50
+ expect(Appsignal.active?).to be_truthy
51
+ transaction
52
+
53
+ expect(Appsignal::Transaction).to receive(:create)
54
+ .with(transaction_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request))
55
+ .and_return(transaction)
56
+ allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
57
+ # Make sure sample data is added
58
+ expect(transaction.ext).to receive(:finish).and_return(true)
59
+ # Stub complete call, stops it from being cleared in the extension
60
+ # And allows us to call `#to_h` on it after it's been completed.
61
+ expect(transaction.ext).to receive(:complete)
62
+
63
+ # Stub transmit call for subscribe/unsubscribe tests
64
+ allow(connection).to receive(:websocket)
65
+ .and_return(instance_double("ActionCable::Connection::WebSocket", :transmit => nil))
66
+ end
67
+
68
+ describe "#perform_action" do
69
+ it "creates a transaction for an action" do
70
+ instance.perform_action("message" => "foo", "action" => "speak")
71
+
72
+ expect(subject).to include(
73
+ "action" => "MyChannel#speak",
74
+ "error" => nil,
75
+ "id" => transaction_id,
76
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
77
+ "metadata" => {
78
+ "method" => "websocket",
79
+ "path" => "/blog"
80
+ }
81
+ )
82
+ expect(subject["events"].first).to include(
83
+ "allocation_count" => kind_of(Integer),
84
+ "body" => "",
85
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
86
+ "child_allocation_count" => kind_of(Integer),
87
+ "child_duration" => kind_of(Float),
88
+ "child_gc_duration" => kind_of(Float),
89
+ "count" => 1,
90
+ "gc_duration" => kind_of(Float),
91
+ "start" => kind_of(Float),
92
+ "duration" => kind_of(Float),
93
+ "name" => "perform_action.action_cable",
94
+ "title" => ""
95
+ )
96
+ expect(subject["sample_data"]).to include(
97
+ "params" => {
98
+ "action" => "speak",
99
+ "message" => "foo"
100
+ }
101
+ )
102
+ end
103
+
104
+ context "without request_id (standalone server)" do
105
+ let(:request_id) { nil }
106
+ let(:transaction_id) { SecureRandom.uuid }
107
+ let(:action_transaction) do
108
+ Appsignal::Transaction.new(
109
+ transaction_id,
110
+ Appsignal::Transaction::ACTION_CABLE,
111
+ ActionDispatch::Request.new(env)
112
+ )
113
+ end
114
+ before do
115
+ # Stub future (private AppSignal) transaction id generated by the hook.
116
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
117
+ end
118
+
119
+ it "uses its own internal request_id set by the subscribed callback" do
120
+ # Subscribe action, sets the request_id
121
+ instance.subscribe_to_channel
122
+ expect(transaction.to_h["id"]).to eq(transaction_id)
123
+
124
+ # Expect another transaction for the action.
125
+ # This transaction will use the same request_id as the
126
+ # transaction id used to subscribe to the channel.
127
+ expect(Appsignal::Transaction).to receive(:create).with(
128
+ transaction_id,
129
+ Appsignal::Transaction::ACTION_CABLE,
130
+ kind_of(ActionDispatch::Request)
131
+ ).and_return(action_transaction)
132
+ allow(Appsignal::Transaction).to receive(:current).and_return(action_transaction)
133
+ # Stub complete call, stops it from being cleared in the extension
134
+ # And allows us to call `#to_h` on it after it's been completed.
135
+ expect(action_transaction.ext).to receive(:complete)
136
+
137
+ instance.perform_action("message" => "foo", "action" => "speak")
138
+ expect(action_transaction.to_h["id"]).to eq(transaction_id)
139
+ end
140
+ end
141
+
142
+ context "with an error in the action" do
143
+ let(:channel) do
144
+ Class.new(ActionCable::Channel::Base) do
145
+ def speak(_data)
146
+ raise VerySpecificError, "oh no!"
147
+ end
148
+
149
+ def self.to_s
150
+ "MyChannel"
151
+ end
152
+ end
153
+ end
154
+
155
+ it "registers an error on the transaction" do
156
+ expect do
157
+ instance.perform_action("message" => "foo", "action" => "speak")
158
+ end.to raise_error(VerySpecificError)
159
+
160
+ expect(subject).to include(
161
+ "action" => "MyChannel#speak",
162
+ "id" => transaction_id,
163
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
164
+ "metadata" => {
165
+ "method" => "websocket",
166
+ "path" => "/blog"
167
+ }
168
+ )
169
+ expect(subject["error"]).to include(
170
+ "backtrace" => kind_of(String),
171
+ "name" => "VerySpecificError",
172
+ "message" => "oh no!"
173
+ )
174
+ expect(subject["sample_data"]).to include(
175
+ "params" => {
176
+ "action" => "speak",
177
+ "message" => "foo"
178
+ }
179
+ )
180
+ end
181
+ end
182
+ end
183
+
184
+ describe "subscribe callback" do
185
+ let(:params) { { "internal" => true } }
186
+
187
+ it "creates a transaction for a subscription" do
188
+ instance.subscribe_to_channel
189
+
190
+ expect(subject).to include(
191
+ "action" => "MyChannel#subscribed",
192
+ "error" => nil,
193
+ "id" => transaction_id,
194
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
195
+ "metadata" => {
196
+ "method" => "websocket",
197
+ "path" => "/blog"
198
+ }
199
+ )
200
+ expect(subject["events"].first).to include(
201
+ "allocation_count" => kind_of(Integer),
202
+ "body" => "",
203
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
204
+ "child_allocation_count" => kind_of(Integer),
205
+ "child_duration" => kind_of(Float),
206
+ "child_gc_duration" => kind_of(Float),
207
+ "count" => 1,
208
+ "gc_duration" => kind_of(Float),
209
+ "start" => kind_of(Float),
210
+ "duration" => kind_of(Float),
211
+ "name" => "subscribed.action_cable",
212
+ "title" => ""
213
+ )
214
+ expect(subject["sample_data"]).to include(
215
+ "params" => { "internal" => "true" }
216
+ )
217
+ end
218
+
219
+ context "without request_id (standalone server)" do
220
+ let(:request_id) { nil }
221
+ let(:transaction_id) { SecureRandom.uuid }
222
+ before do
223
+ allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
224
+ instance.subscribe_to_channel
225
+ end
226
+
227
+ it "uses its own internal request_id" do
228
+ expect(subject["id"]).to eq(transaction_id)
229
+ end
230
+ end
231
+
232
+ context "with an error in the callback" do
233
+ let(:channel) do
234
+ Class.new(ActionCable::Channel::Base) do
235
+ def subscribed
236
+ raise VerySpecificError, "oh no!"
237
+ end
238
+
239
+ def self.to_s
240
+ "MyChannel"
241
+ end
242
+ end
243
+ end
244
+
245
+ it "registers an error on the transaction" do
246
+ expect do
247
+ instance.subscribe_to_channel
248
+ end.to raise_error(VerySpecificError)
249
+
250
+ expect(subject).to include(
251
+ "action" => "MyChannel#subscribed",
252
+ "id" => transaction_id,
253
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
254
+ "metadata" => {
255
+ "method" => "websocket",
256
+ "path" => "/blog"
257
+ }
258
+ )
259
+ expect(subject["error"]).to include(
260
+ "backtrace" => kind_of(String),
261
+ "name" => "VerySpecificError",
262
+ "message" => "oh no!"
263
+ )
264
+ expect(subject["sample_data"]).to include(
265
+ "params" => { "internal" => "true" }
266
+ )
267
+ end
268
+ end
269
+ end
270
+
271
+ describe "unsubscribe callback" do
272
+ let(:params) { { "internal" => true } }
273
+
274
+ it "creates a transaction for a subscription" do
275
+ instance.unsubscribe_from_channel
276
+
277
+ expect(subject).to include(
278
+ "action" => "MyChannel#unsubscribed",
279
+ "error" => nil,
280
+ "id" => transaction_id,
281
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
282
+ "metadata" => {
283
+ "method" => "websocket",
284
+ "path" => "/blog"
285
+ }
286
+ )
287
+ expect(subject["events"].first).to include(
288
+ "allocation_count" => kind_of(Integer),
289
+ "body" => "",
290
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
291
+ "child_allocation_count" => kind_of(Integer),
292
+ "child_duration" => kind_of(Float),
293
+ "child_gc_duration" => kind_of(Float),
294
+ "count" => 1,
295
+ "gc_duration" => kind_of(Float),
296
+ "start" => kind_of(Float),
297
+ "duration" => kind_of(Float),
298
+ "name" => "unsubscribed.action_cable",
299
+ "title" => ""
300
+ )
301
+ expect(subject["sample_data"]).to include(
302
+ "params" => { "internal" => "true" }
303
+ )
304
+ end
305
+
306
+ context "without request_id (standalone server)" do
307
+ let(:request_id) { nil }
308
+ let(:transaction_id) { SecureRandom.uuid }
309
+ before do
310
+ allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
311
+ instance.unsubscribe_from_channel
312
+ end
313
+
314
+ it "uses its own internal request_id" do
315
+ expect(subject["id"]).to eq(transaction_id)
316
+ end
317
+ end
318
+
319
+ context "with an error in the callback" do
320
+ let(:channel) do
321
+ Class.new(ActionCable::Channel::Base) do
322
+ def unsubscribed
323
+ raise VerySpecificError, "oh no!"
324
+ end
325
+
326
+ def self.to_s
327
+ "MyChannel"
328
+ end
329
+ end
330
+ end
331
+
332
+ it "registers an error on the transaction" do
333
+ expect do
334
+ instance.unsubscribe_from_channel
335
+ end.to raise_error(VerySpecificError)
336
+
337
+ expect(subject).to include(
338
+ "action" => "MyChannel#unsubscribed",
339
+ "id" => transaction_id,
340
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
341
+ "metadata" => {
342
+ "method" => "websocket",
343
+ "path" => "/blog"
344
+ }
345
+ )
346
+ expect(subject["error"]).to include(
347
+ "backtrace" => kind_of(String),
348
+ "name" => "VerySpecificError",
349
+ "message" => "oh no!"
350
+ )
351
+ expect(subject["sample_data"]).to include(
352
+ "params" => { "internal" => "true" }
353
+ )
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
359
+ else
360
+ context "without ActionCable" do
361
+ describe ".dependencies_present?" do
362
+ subject { described_class.new.dependencies_present? }
363
+
364
+ it "returns false" do
365
+ is_expected.to be_falsy
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -1,45 +1,77 @@
1
1
  describe Appsignal::Hooks::ActiveSupportNotificationsHook do
2
2
  if active_support_present?
3
+ let(:notifier) { ActiveSupport::Notifications::Fanout.new }
4
+ let(:as) { ActiveSupport::Notifications }
3
5
  before :context do
4
6
  start_agent
5
7
  end
6
8
  before do
9
+ as.notifier = notifier
7
10
  Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
8
11
  end
9
12
 
10
- let(:notifier) { ActiveSupport::Notifications::Fanout.new }
11
- let(:instrumenter) { ActiveSupport::Notifications::Instrumenter.new(notifier) }
12
-
13
13
  describe "#dependencies_present?" do
14
14
  subject { described_class.new.dependencies_present? }
15
15
 
16
16
  it { is_expected.to be_truthy }
17
17
  end
18
18
 
19
- it "should instrument an AS notifications instrument call with a block" do
19
+ it "instruments an ActiveSupport::Notifications.instrument event" do
20
20
  expect(Appsignal::Transaction.current).to receive(:start_event)
21
21
  .at_least(:once)
22
22
  expect(Appsignal::Transaction.current).to receive(:finish_event)
23
23
  .at_least(:once)
24
24
  .with("sql.active_record", nil, "SQL", 1)
25
25
 
26
- return_value = instrumenter.instrument("sql.active_record", :sql => "SQL") do
26
+ return_value = as.instrument("sql.active_record", :sql => "SQL") do
27
27
  "value"
28
28
  end
29
29
 
30
30
  expect(return_value).to eq "value"
31
31
  end
32
32
 
33
- it "should not instrument events whose name starts with a bang" do
33
+ it "does not instrument events whose name starts with a bang" do
34
34
  expect(Appsignal::Transaction.current).not_to receive(:start_event)
35
35
  expect(Appsignal::Transaction.current).not_to receive(:finish_event)
36
36
 
37
- return_value = instrumenter.instrument("!sql.active_record", :sql => "SQL") do
37
+ return_value = as.instrument("!sql.active_record", :sql => "SQL") do
38
38
  "value"
39
39
  end
40
40
 
41
41
  expect(return_value).to eq "value"
42
42
  end
43
+
44
+ context "when an error is raised in an instrumented block" do
45
+ it "instruments an ActiveSupport::Notifications.instrument event" do
46
+ expect(Appsignal::Transaction.current).to receive(:start_event)
47
+ .at_least(:once)
48
+ expect(Appsignal::Transaction.current).to receive(:finish_event)
49
+ .at_least(:once)
50
+ .with("sql.active_record", nil, "SQL", 1)
51
+
52
+ expect do
53
+ as.instrument("sql.active_record", :sql => "SQL") do
54
+ raise VerySpecificError, "foo"
55
+ end
56
+ end.to raise_error(VerySpecificError, "foo")
57
+ end
58
+ end
59
+
60
+ context "when a message is thrown in an instrumented block" do
61
+ it "instruments an ActiveSupport::Notifications.instrument event" do
62
+ expect(Appsignal::Transaction.current).to receive(:start_event)
63
+ .at_least(:once)
64
+ expect(Appsignal::Transaction.current).to receive(:finish_event)
65
+ .at_least(:once)
66
+ .with("sql.active_record", nil, "SQL", 1)
67
+
68
+ expect do
69
+ as.instrument("sql.active_record", :sql => "SQL") do
70
+ throw :foo
71
+ end
72
+ end.to throw_symbol(:foo)
73
+ end
74
+ end
43
75
  else
44
76
  describe "#dependencies_present?" do
45
77
  subject { described_class.new.dependencies_present? }