appsignal 2.2.1 → 2.3.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,13 +2,14 @@ describe Appsignal::Hooks::SidekiqPlugin do
2
2
  let(:worker) { double }
3
3
  let(:queue) { double }
4
4
  let(:current_transaction) { background_job_transaction }
5
+ let(:args) { ["Model", 1] }
5
6
  let(:item) do
6
7
  {
7
8
  "class" => "TestClass",
8
9
  "retry_count" => 0,
9
10
  "queue" => "default",
10
11
  "enqueued_at" => Time.parse("01-01-2001 10:00:00UTC"),
11
- "args" => ["Model", 1],
12
+ "args" => args,
12
13
  "extra" => "data"
13
14
  }
14
15
  end
@@ -20,7 +21,15 @@ describe Appsignal::Hooks::SidekiqPlugin do
20
21
  end
21
22
 
22
23
  context "with a performance call" do
23
- it "should wrap in a transaction with the correct params" do
24
+ after do
25
+ Timecop.freeze(Time.parse("01-01-2001 10:01:00UTC")) do
26
+ Appsignal::Hooks::SidekiqPlugin.new.call(worker, item, queue) do
27
+ # nothing
28
+ end
29
+ end
30
+ end
31
+
32
+ it "wraps it in a transaction with the correct params" do
24
33
  expect(Appsignal).to receive(:monitor_transaction).with(
25
34
  "perform_job.sidekiq",
26
35
  :class => "TestClass",
@@ -30,12 +39,69 @@ describe Appsignal::Hooks::SidekiqPlugin do
30
39
  "queue" => "default",
31
40
  "extra" => "data"
32
41
  },
33
- :params => %w(Model 1),
42
+ :params => ["Model", 1],
34
43
  :queue_start => Time.parse("01-01-2001 10:00:00UTC"),
35
44
  :queue_time => 60_000.to_f
36
45
  )
37
46
  end
38
47
 
48
+ context "with more complex arguments" do
49
+ let(:default_params) do
50
+ {
51
+ :class => "TestClass",
52
+ :method => "perform",
53
+ :metadata => {
54
+ "retry_count" => "0",
55
+ "queue" => "default",
56
+ "extra" => "data"
57
+ },
58
+ :params => {
59
+ :foo => "Foo",
60
+ :bar => "Bar"
61
+ },
62
+ :queue_start => Time.parse("01-01-2001 10:00:00UTC"),
63
+ :queue_time => 60_000.to_f
64
+ }
65
+ end
66
+ let(:args) do
67
+ {
68
+ :foo => "Foo",
69
+ :bar => "Bar"
70
+ }
71
+ end
72
+
73
+ it "adds the more complex arguments" do
74
+ expect(Appsignal).to receive(:monitor_transaction).with(
75
+ "perform_job.sidekiq",
76
+ default_params.merge(
77
+ :params => {
78
+ :foo => "Foo",
79
+ :bar => "Bar"
80
+ }
81
+ )
82
+ )
83
+ end
84
+
85
+ context "with parameter filtering" do
86
+ before do
87
+ Appsignal.config = project_fixture_config("production")
88
+ Appsignal.config[:filter_parameters] = ["foo"]
89
+ end
90
+
91
+ it "filters selected arguments" do
92
+ expect(Appsignal).to receive(:monitor_transaction).with(
93
+ "perform_job.sidekiq",
94
+ default_params.merge(
95
+ :params => {
96
+ :foo => "[FILTERED]",
97
+ :bar => "Bar"
98
+ }
99
+ )
100
+ )
101
+ end
102
+ end
103
+ end
104
+
39
105
  context "when wrapped by ActiveJob" do
40
106
  let(:item) do
41
107
  {
@@ -46,7 +112,7 @@ describe Appsignal::Hooks::SidekiqPlugin do
46
112
  "job_class" => "TestJob",
47
113
  "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
48
114
  "queue_name" => "default",
49
- "arguments" => ["Model", 1]
115
+ "arguments" => args
50
116
  }],
51
117
  "retry" => true,
52
118
  "jid" => "efb140489485999d32b5504c",
@@ -54,50 +120,103 @@ describe Appsignal::Hooks::SidekiqPlugin do
54
120
  "enqueued_at" => Time.parse("01-01-2001 10:00:00UTC").to_f
55
121
  }
56
122
  end
57
-
58
- it "should wrap in a transaction with the correct params" do
59
- expect(Appsignal).to receive(:monitor_transaction).with(
60
- "perform_job.sidekiq",
123
+ let(:default_params) do
124
+ {
61
125
  :class => "TestClass",
62
126
  :method => "perform",
63
127
  :metadata => {
64
128
  "queue" => "default"
65
129
  },
66
- :params => %w(Model 1),
67
130
  :queue_start => Time.parse("01-01-2001 10:00:00UTC").to_f,
68
131
  :queue_time => 60_000.to_f
132
+ }
133
+ end
134
+
135
+ it "wraps it in a transaction with the correct params" do
136
+ expect(Appsignal).to receive(:monitor_transaction).with(
137
+ "perform_job.sidekiq",
138
+ default_params.merge(:params => ["Model", 1])
69
139
  )
70
140
  end
71
- end
72
141
 
73
- after do
74
- Timecop.freeze(Time.parse("01-01-2001 10:01:00UTC")) do
75
- Appsignal::Hooks::SidekiqPlugin.new.call(worker, item, queue) do
76
- # nothing
142
+ context "with more complex arguments" do
143
+ let(:args) do
144
+ {
145
+ :foo => "Foo",
146
+ :bar => "Bar"
147
+ }
148
+ end
149
+
150
+ it "adds the more complex arguments" do
151
+ expect(Appsignal).to receive(:monitor_transaction).with(
152
+ "perform_job.sidekiq",
153
+ default_params.merge(
154
+ :params => {
155
+ :foo => "Foo",
156
+ :bar => "Bar"
157
+ }
158
+ )
159
+ )
160
+ end
161
+
162
+ context "with parameter filtering" do
163
+ before do
164
+ Appsignal.config = project_fixture_config("production")
165
+ Appsignal.config[:filter_parameters] = ["foo"]
166
+ end
167
+
168
+ it "filters selected arguments" do
169
+ expect(Appsignal).to receive(:monitor_transaction).with(
170
+ "perform_job.sidekiq",
171
+ default_params.merge(
172
+ :params => {
173
+ :foo => "[FILTERED]",
174
+ :bar => "Bar"
175
+ }
176
+ )
177
+ )
178
+ end
77
179
  end
78
180
  end
79
181
  end
80
182
  end
81
183
 
82
184
  context "with an erroring call" do
83
- let(:error) { VerySpecificError.new }
185
+ let(:error) { VerySpecificError }
186
+ let(:transaction) do
187
+ Appsignal::Transaction.new(
188
+ SecureRandom.uuid,
189
+ Appsignal::Transaction::BACKGROUND_JOB,
190
+ Appsignal::Transaction::GenericRequest.new({})
191
+ )
192
+ end
193
+ before do
194
+ allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
195
+ expect(Appsignal::Transaction).to receive(:create)
196
+ .with(
197
+ kind_of(String),
198
+ Appsignal::Transaction::BACKGROUND_JOB,
199
+ kind_of(Appsignal::Transaction::GenericRequest)
200
+ ).and_return(transaction)
201
+ end
84
202
 
85
- it "should add the exception to appsignal" do
86
- expect_any_instance_of(Appsignal::Transaction).to receive(:set_error).with(error)
203
+ it "adds the error to the transaction" do
204
+ expect(transaction).to receive(:set_error).with(error)
205
+ expect(transaction).to receive(:complete)
87
206
  end
88
207
 
89
208
  after do
90
- begin
209
+ expect do
91
210
  Timecop.freeze(Time.parse("01-01-2001 10:01:00UTC")) do
92
211
  Appsignal::Hooks::SidekiqPlugin.new.call(worker, item, queue) do
93
212
  raise error
94
213
  end
95
214
  end
96
- rescue VerySpecificError
97
- end
215
+ end.to raise_error(error)
98
216
  end
99
217
  end
100
218
 
219
+ # TODO: Don't test (what are basically) private methods
101
220
  describe "#formatted_data" do
102
221
  let(:item) do
103
222
  {
@@ -106,7 +225,7 @@ describe Appsignal::Hooks::SidekiqPlugin do
106
225
  }
107
226
  end
108
227
 
109
- it "should only add items to the hash that do not appear in JOB_KEYS" do
228
+ it "only adds items to the hash that do not appear in JOB_KEYS" do
110
229
  expect(plugin.formatted_metadata(item)).to eq("foo" => "bar")
111
230
  end
112
231
  end
@@ -192,25 +192,4 @@ describe Appsignal::Hooks::Helpers do
192
192
  it { is_expected.to eq "1" }
193
193
  end
194
194
  end
195
-
196
- describe "#format_args" do
197
- let(:object) { Object.new }
198
- let(:args) do
199
- [
200
- "Model",
201
- 1,
202
- object,
203
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
204
- ]
205
- end
206
-
207
- it "should format the arguments" do
208
- expect(with_helpers.format_args(args)).to eq([
209
- "Model",
210
- "1",
211
- object.inspect,
212
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ..."
213
- ])
214
- end
215
- end
216
195
  end
@@ -1,38 +1,83 @@
1
1
  if DependencyHelper.resque_present? && DependencyHelper.active_job_present?
2
- describe "Resque ActiveJob integration" do
3
- let(:file) { File.expand_path("lib/appsignal/integrations/resque_active_job.rb") }
2
+ require "active_job"
4
3
 
5
- context "with Resque and ActiveJob" do
6
- before do
7
- load file
8
- start_agent
4
+ describe Appsignal::Integrations::ResqueActiveJobPlugin do
5
+ let(:file) { File.expand_path("lib/appsignal/integrations/resque_active_job.rb") }
6
+ let(:args) { "argument" }
7
+ let(:job) { TestActiveJob.new(args) }
8
+ before do
9
+ load file
10
+ start_agent
9
11
 
10
- class TestActiveJob < ActiveJob::Base
11
- include Appsignal::Integrations::ResqueActiveJobPlugin
12
+ class TestActiveJob < ActiveJob::Base
13
+ include Appsignal::Integrations::ResqueActiveJobPlugin
12
14
 
13
- def perform(param)
14
- end
15
+ def perform(_)
15
16
  end
16
17
  end
18
+ end
17
19
 
18
- describe :around_perform_plugin do
19
- before { allow(SecureRandom).to receive(:uuid).and_return(123) }
20
- let(:job) { TestActiveJob.new("moo") }
20
+ it "wraps it in a transaction with the correct params" do
21
+ expect(Appsignal).to receive(:monitor_single_transaction).with(
22
+ "perform_job.resque",
23
+ :class => "TestActiveJob",
24
+ :method => "perform",
25
+ :params => ["argument"],
26
+ :metadata => {
27
+ :id => kind_of(String),
28
+ :queue => "default"
29
+ }
30
+ )
31
+ end
32
+
33
+ context "with complex arguments" do
34
+ let(:args) do
35
+ {
36
+ :foo => "Foo",
37
+ :bar => "Bar"
38
+ }
39
+ end
21
40
 
22
- it "should wrap in a transaction with the correct params" do
41
+ it "truncates large argument values" do
42
+ expect(Appsignal).to receive(:monitor_single_transaction).with(
43
+ "perform_job.resque",
44
+ :class => "TestActiveJob",
45
+ :method => "perform",
46
+ :params => [
47
+ :foo => "Foo",
48
+ :bar => "Bar"
49
+ ],
50
+ :metadata => {
51
+ :id => kind_of(String),
52
+ :queue => "default"
53
+ }
54
+ )
55
+ end
56
+
57
+ context "with parameter filtering" do
58
+ before do
59
+ Appsignal.config = project_fixture_config("production")
60
+ Appsignal.config[:filter_parameters] = ["foo"]
61
+ end
62
+
63
+ it "filters selected arguments" do
23
64
  expect(Appsignal).to receive(:monitor_single_transaction).with(
24
65
  "perform_job.resque",
25
66
  :class => "TestActiveJob",
26
67
  :method => "perform",
27
- :params => ["moo"],
68
+ :params => [
69
+ :foo => "[FILTERED]",
70
+ :bar => "Bar"
71
+ ],
28
72
  :metadata => {
29
- :id => 123,
73
+ :id => kind_of(String),
30
74
  :queue => "default"
31
75
  }
32
76
  )
33
77
  end
34
- after { job.perform_now }
35
78
  end
36
79
  end
80
+
81
+ after { job.perform_now }
37
82
  end
38
83
  end
@@ -33,11 +33,11 @@ if DependencyHelper.resque_present?
33
33
  end
34
34
 
35
35
  context "without exception" do
36
- it "should create a new transaction" do
36
+ it "creates a new transaction" do
37
37
  expect(Appsignal::Transaction).to receive(:create).and_return(transaction)
38
38
  end
39
39
 
40
- it "should wrap in a transaction with the correct params" do
40
+ it "wraps it in a transaction with the correct params" do
41
41
  expect(Appsignal).to receive(:monitor_transaction).with(
42
42
  "perform_job.resque",
43
43
  :class => "TestJob",
@@ -45,7 +45,7 @@ if DependencyHelper.resque_present?
45
45
  )
46
46
  end
47
47
 
48
- it "should close the transaction" do
48
+ it "closes the transaction" do
49
49
  expect(transaction).to receive(:complete)
50
50
  end
51
51
 
@@ -54,17 +54,29 @@ if DependencyHelper.resque_present?
54
54
 
55
55
  context "with exception" do
56
56
  let(:job) { ::Resque::Job.new("default", "class" => "BrokenTestJob") }
57
+ let(:transaction) do
58
+ Appsignal::Transaction.new(
59
+ SecureRandom.uuid,
60
+ Appsignal::Transaction::BACKGROUND_JOB,
61
+ Appsignal::Transaction::GenericRequest.new({})
62
+ )
63
+ end
64
+ before do
65
+ allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
66
+ expect(Appsignal::Transaction).to receive(:create)
67
+ .with(
68
+ kind_of(String),
69
+ Appsignal::Transaction::BACKGROUND_JOB,
70
+ kind_of(Appsignal::Transaction::GenericRequest)
71
+ ).and_return(transaction)
72
+ end
57
73
 
58
- it "should set the exception" do
59
- expect_any_instance_of(Appsignal::Transaction).to receive(:set_error)
74
+ it "sets the exception on the transaction" do
75
+ expect(transaction).to receive(:set_error).with(VerySpecificError)
60
76
  end
61
77
 
62
78
  after do
63
- begin
64
- job.perform
65
- rescue VerySpecificError
66
- # Do nothing
67
- end
79
+ expect { job.perform }.to raise_error(VerySpecificError)
68
80
  end
69
81
  end
70
82
  end
@@ -73,8 +85,8 @@ if DependencyHelper.resque_present?
73
85
  context "without resque" do
74
86
  before(:context) { Object.send(:remove_const, :Resque) }
75
87
 
76
- specify { expect { ::Resque }.to raise_error(NameError) }
77
- specify { expect { load file }.to_not raise_error }
88
+ it { expect { ::Resque }.to raise_error(NameError) }
89
+ it { expect { load file }.to_not raise_error }
78
90
  end
79
91
  end
80
92
  end
@@ -3,151 +3,212 @@ describe Appsignal::Transaction do
3
3
  start_agent
4
4
  end
5
5
 
6
- let(:time) { Time.at(fixed_time) }
7
- let(:namespace) { Appsignal::Transaction::HTTP_REQUEST }
8
- let(:env) { {} }
9
- let(:merged_env) { http_request_env_with_data(env) }
10
- let(:options) { {} }
11
- let(:request) { Rack::Request.new(merged_env) }
12
- let(:transaction) { Appsignal::Transaction.new("1", namespace, request, options) }
6
+ let(:transaction_id) { "1" }
7
+ let(:time) { Time.at(fixed_time) }
8
+ let(:namespace) { Appsignal::Transaction::HTTP_REQUEST }
9
+ let(:env) { {} }
10
+ let(:merged_env) { http_request_env_with_data(env) }
11
+ let(:options) { {} }
12
+ let(:request) { Rack::Request.new(merged_env) }
13
+ let(:transaction) { Appsignal::Transaction.new(transaction_id, namespace, request, options) }
14
+ let(:log) { StringIO.new }
13
15
 
14
16
  before { Timecop.freeze(time) }
15
- after { Timecop.return }
17
+ after { Timecop.return }
18
+ around do |example|
19
+ use_logger_with log do
20
+ example.run
21
+ end
22
+ end
16
23
 
17
24
  describe "class methods" do
25
+ def current_transaction
26
+ Appsignal::Transaction.current
27
+ end
28
+
18
29
  describe ".create" do
19
- it "should add the transaction to thread local" do
20
- expect(Appsignal::Extension).to receive(:start_transaction).with("1", "http_request", 0)
30
+ def create_transaction(id = transaction_id)
31
+ Appsignal::Transaction.create(id, namespace, request, options)
32
+ end
21
33
 
22
- created_transaction = Appsignal::Transaction.create("1", namespace, request, options)
34
+ context "when no transaction is running" do
35
+ let!(:transaction) { create_transaction }
23
36
 
24
- expect(Thread.current[:appsignal_transaction]).to eq created_transaction
25
- end
37
+ it "returns the created transaction" do
38
+ expect(transaction).to be_a Appsignal::Transaction
39
+ expect(transaction.transaction_id).to eq transaction_id
40
+ expect(transaction.namespace).to eq namespace
41
+ expect(transaction.request).to eq request
26
42
 
27
- it "should create a transaction" do
28
- created_transaction = Appsignal::Transaction.create("1", namespace, request, options)
43
+ expect(transaction.to_h).to include(
44
+ "id" => transaction_id,
45
+ "namespace" => namespace
46
+ )
47
+ end
29
48
 
30
- expect(created_transaction).to be_a Appsignal::Transaction
31
- expect(created_transaction.transaction_id).to eq "1"
32
- expect(created_transaction.namespace).to eq "http_request"
49
+ it "assigns the transaction to current" do
50
+ expect(transaction).to eq current_transaction
51
+ end
33
52
  end
34
53
 
35
54
  context "when a transaction is already running" do
36
- let(:running_transaction) { double(:transaction_id => 2) }
37
- before { Thread.current[:appsignal_transaction] = running_transaction }
38
-
39
- it "should not create a new transaction" do
40
- expect(
41
- Appsignal::Transaction.create("1", namespace, request, options)
42
- ).to eq(running_transaction)
55
+ before { create_transaction }
56
+
57
+ it "does not create a new transaction, but returns the current transaction" do
58
+ expect do
59
+ new_transaction = create_transaction("2")
60
+ expect(new_transaction).to eq(current_transaction)
61
+ expect(new_transaction.transaction_id).to eq(transaction_id)
62
+ end.to_not change { current_transaction }
43
63
  end
44
64
 
45
- it "should output a debug message" do
46
- expect(Appsignal.logger).to receive(:debug)
47
- .with("Trying to start new transaction 1 but 2 is already running. Using 2")
48
-
49
- Appsignal::Transaction.create("1", namespace, request, options)
65
+ it "logs a debug message" do
66
+ create_transaction("2")
67
+ expect(log_contents(log)).to contains_log :debug,
68
+ "Trying to start new transaction with id '2', but a " \
69
+ "transaction with id '#{transaction_id}' is already " \
70
+ "running. Using transaction '#{transaction_id}'."
50
71
  end
51
72
 
52
- context "with option to force a new transaction" do
53
- let(:options) { { :force => true } }
54
- it "should not create a new transaction" do
55
- expect(
56
- Appsignal::Transaction.create("1", namespace, request, options)
57
- ).to_not eq(running_transaction)
73
+ context "with option :force => true" do
74
+ it "returns the newly created (and current) transaction" do
75
+ original_transaction = current_transaction
76
+ expect(original_transaction).to_not be_nil
77
+ expect(current_transaction.transaction_id).to eq transaction_id
78
+
79
+ options[:force] = true
80
+ expect(create_transaction("2")).to_not eq original_transaction
81
+ expect(current_transaction.transaction_id).to eq "2"
58
82
  end
59
83
  end
60
84
  end
61
85
  end
62
86
 
63
87
  describe ".current" do
64
- before { Thread.current[:appsignal_transaction] = transaction }
65
-
66
88
  subject { Appsignal::Transaction.current }
67
89
 
68
- context "if there is a transaction" do
69
- before { Appsignal::Transaction.create("1", namespace, request, options) }
70
-
71
- it "should return the correct transaction" do
72
- is_expected.to eq transaction
90
+ context "when there is a current transaction" do
91
+ let!(:transaction) do
92
+ Appsignal::Transaction.create(transaction_id, namespace, request, options)
73
93
  end
74
94
 
75
- it "should indicate it's not a nil transaction" do
76
- expect(subject.nil_transaction?).to be_falsy
95
+ it "reads :appsignal_transaction from the current Thread" do
96
+ expect(subject).to eq Thread.current[:appsignal_transaction]
97
+ expect(subject).to eq transaction
77
98
  end
78
- end
79
99
 
80
- context "if there is no transaction" do
81
- before do
82
- Thread.current[:appsignal_transaction] = nil
100
+ it "is not a NilTransaction" do
101
+ expect(subject.nil_transaction?).to eq false
102
+ expect(subject).to be_a Appsignal::Transaction
83
103
  end
104
+ end
84
105
 
85
- it "should return a nil transaction stub" do
86
- is_expected.to be_a Appsignal::Transaction::NilTransaction
106
+ context "when there is no current transaction" do
107
+ it "has no :appsignal_transaction registered on the current Thread" do
108
+ expect(Thread.current[:appsignal_transaction]).to be_nil
87
109
  end
88
110
 
89
- it "should indicate it's a nil transaction" do
90
- expect(subject.nil_transaction?).to be_truthy
111
+ it "returns a NilTransaction stub" do
112
+ expect(subject.nil_transaction?).to eq true
113
+ expect(subject).to be_a Appsignal::Transaction::NilTransaction
91
114
  end
92
115
  end
93
116
  end
94
117
 
95
- describe "complete_current!" do
96
- before { Appsignal::Transaction.create("2", Appsignal::Transaction::HTTP_REQUEST, {}) }
118
+ describe ".complete_current!" do
119
+ let!(:transaction) { Appsignal::Transaction.create(transaction_id, namespace, options) }
97
120
 
98
- it "should complete the current transaction and set the thread appsignal_transaction to nil" do
99
- expect(Appsignal::Transaction.current).to receive(:complete)
121
+ it "completes the current transaction" do
122
+ expect(transaction).to eq current_transaction
123
+ expect(transaction).to receive(:complete).and_call_original
100
124
 
101
125
  Appsignal::Transaction.complete_current!
102
-
103
- expect(Thread.current[:appsignal_transaction]).to be_nil
104
126
  end
105
127
 
106
- it "should still clear the transaction if there is an error" do
107
- expect(Appsignal::Transaction.current).to receive(:complete).and_raise "Error"
108
-
109
- Appsignal::Transaction.complete_current!
110
-
111
- expect(Thread.current[:appsignal_transaction]).to be_nil
128
+ it "unsets the current transaction on the current Thread" do
129
+ expect do
130
+ Appsignal::Transaction.complete_current!
131
+ end.to change { Thread.current[:appsignal_transaction] }.from(transaction).to(nil)
112
132
  end
113
133
 
114
- context "if a transaction is discarded" do
115
- it "should not complete the transaction" do
116
- expect(Appsignal::Transaction.current.ext).to_not receive(:complete)
117
-
118
- Appsignal::Transaction.current.discard!
119
- expect(Appsignal::Transaction.current.discarded?).to be_truthy
134
+ context "when encountering an error while completing" do
135
+ before do
136
+ expect(transaction).to receive(:complete).and_raise VerySpecificError
137
+ end
120
138
 
139
+ it "logs an error message" do
121
140
  Appsignal::Transaction.complete_current!
122
-
123
- expect(Thread.current[:appsignal_transaction]).to be_nil
141
+ expect(log_contents(log)).to contains_log :error,
142
+ "Failed to complete transaction ##{transaction.transaction_id}. VerySpecificError"
124
143
  end
125
144
 
126
- it "should not be discarded when restore! is called" do
127
- Appsignal::Transaction.current.discard!
128
- expect(Appsignal::Transaction.current.discarded?).to be_truthy
129
- Appsignal::Transaction.current.restore!
130
- expect(Appsignal::Transaction.current.discarded?).to be_falsy
145
+ it "clears the current transaction" do
146
+ expect do
147
+ Appsignal::Transaction.complete_current!
148
+ end.to change { Thread.current[:appsignal_transaction] }.from(transaction).to(nil)
131
149
  end
132
150
  end
133
151
  end
134
152
  end
135
153
 
136
154
  describe "#complete" do
137
- it "should sample data if it needs to be sampled" do
138
- expect(transaction.ext).to receive(:finish).and_return(true)
139
- expect(transaction).to receive(:sample_data)
140
- expect(transaction.ext).to receive(:complete)
155
+ context "when transaction is being sampled" do
156
+ it "samples data" do
157
+ expect(transaction.ext).to receive(:finish).and_return(true)
158
+ # Stub call to extension, because that would remove the transaction
159
+ # from the extension.
160
+ expect(transaction.ext).to receive(:complete)
161
+
162
+ transaction.set_tags(:foo => "bar")
163
+ transaction.complete
164
+ expect(transaction.to_h["sample_data"]).to include(
165
+ "tags" => { "foo" => "bar" }
166
+ )
167
+ end
168
+ end
169
+
170
+ context "when transaction is not being sampled" do
171
+ it "does not sample data" do
172
+ expect(transaction).to_not receive(:sample_data)
173
+ expect(transaction.ext).to receive(:finish).and_return(false)
174
+ expect(transaction.ext).to receive(:complete).and_call_original
141
175
 
142
- transaction.complete
176
+ transaction.complete
177
+ end
143
178
  end
144
179
 
145
- it "should not sample data if it does not need to be sampled" do
146
- expect(transaction.ext).to receive(:finish).and_return(false)
147
- expect(transaction).to_not receive(:sample_data)
148
- expect(transaction.ext).to receive(:complete)
180
+ context "when a transaction is marked as discarded" do
181
+ it "does not complete the transaction" do
182
+ expect(transaction.ext).to_not receive(:complete)
183
+
184
+ expect do
185
+ transaction.discard!
186
+ end.to change { transaction.discarded? }.from(false).to(true)
187
+
188
+ transaction.complete
189
+ end
190
+
191
+ it "logs a debug message" do
192
+ transaction.discard!
193
+ transaction.complete
194
+
195
+ expect(log_contents(log)).to contains_log :debug,
196
+ "Skipping transaction '#{transaction_id}' because it was manually discarded."
197
+ end
198
+
199
+ context "when a discarded transaction is restored" do
200
+ before { transaction.discard! }
201
+
202
+ it "completes the transaction" do
203
+ expect(transaction.ext).to receive(:complete).and_call_original
149
204
 
150
- transaction.complete
205
+ expect do
206
+ transaction.restore!
207
+ end.to change { transaction.discarded? }.from(true).to(false)
208
+
209
+ transaction.complete
210
+ end
211
+ end
151
212
  end
152
213
  end
153
214
 
@@ -241,6 +302,37 @@ describe Appsignal::Transaction do
241
302
  end
242
303
  end
243
304
 
305
+ describe "#params" do
306
+ subject { transaction.params }
307
+
308
+ context "with custom params set on transaction" do
309
+ before do
310
+ transaction.params = { :foo => "bar" }
311
+ end
312
+
313
+ it "returns custom parameters" do
314
+ expect(subject).to eq(:foo => "bar")
315
+ end
316
+ end
317
+
318
+ context "without custom params set on transaction" do
319
+ it "returns parameters from request" do
320
+ expect(subject).to eq(
321
+ "action" => "show",
322
+ "controller" => "blog_posts",
323
+ "id" => "1"
324
+ )
325
+ end
326
+ end
327
+ end
328
+
329
+ describe "#params=" do
330
+ it "sets params on the transaction" do
331
+ transaction.params = { :foo => "bar" }
332
+ expect(transaction.params).to eq(:foo => "bar")
333
+ end
334
+ end
335
+
244
336
  describe "#set_tags" do
245
337
  it "should add tags to transaction" do
246
338
  expect do
@@ -733,19 +825,38 @@ describe Appsignal::Transaction do
733
825
  describe "#sanitized_params" do
734
826
  subject { transaction.send(:sanitized_params) }
735
827
 
736
- context "without params" do
828
+ context "with custom params" do
829
+ before do
830
+ transaction.params = { :foo => "bar", :baz => :bat }
831
+ end
832
+
833
+ it "returns custom params" do
834
+ is_expected.to eq(:foo => "bar", :baz => :bat)
835
+ end
836
+
837
+ context "with AppSignal filtering" do
838
+ before { Appsignal.config.config_hash[:filter_parameters] = %w(foo) }
839
+ after { Appsignal.config.config_hash[:filter_parameters] = [] }
840
+
841
+ it "returns sanitized custom params" do
842
+ expect(subject).to eq(:foo => "[FILTERED]", :baz => :bat)
843
+ end
844
+ end
845
+ end
846
+
847
+ context "without request params" do
737
848
  before { allow(transaction.request).to receive(:params).and_return(nil) }
738
849
 
739
850
  it { is_expected.to be_nil }
740
851
  end
741
852
 
742
- context "when params crashes" do
853
+ context "when request params crashes" do
743
854
  before { allow(transaction.request).to receive(:params).and_raise(NoMethodError) }
744
855
 
745
856
  it { is_expected.to be_nil }
746
857
  end
747
858
 
748
- context "when params method does not exist" do
859
+ context "when request params method does not exist" do
749
860
  let(:options) { { :params_method => :nonsense } }
750
861
 
751
862
  it { is_expected.to be_nil }
@@ -982,6 +1093,34 @@ describe Appsignal::Transaction do
982
1093
  end
983
1094
  end
984
1095
 
1096
+ describe ".to_hash / .to_h" do
1097
+ subject { transaction.to_hash }
1098
+
1099
+ context "when extension returns serialized JSON" do
1100
+ it "parses the result and returns a Hash" do
1101
+ expect(subject).to include(
1102
+ "action" => nil,
1103
+ "error" => nil,
1104
+ "events" => [],
1105
+ "id" => transaction_id,
1106
+ "metadata" => {},
1107
+ "namespace" => namespace,
1108
+ "sample_data" => {}
1109
+ )
1110
+ end
1111
+ end
1112
+
1113
+ context "when the extension returns invalid serialized JSON" do
1114
+ before do
1115
+ expect(transaction.ext).to receive(:to_json).and_return("foo")
1116
+ end
1117
+
1118
+ it "raises a JSON parse error" do
1119
+ expect { subject }.to raise_error(JSON::ParserError)
1120
+ end
1121
+ end
1122
+ end
1123
+
985
1124
  describe Appsignal::Transaction::NilTransaction do
986
1125
  subject { Appsignal::Transaction::NilTransaction.new }
987
1126