airbrake-ruby 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +515 -0
  3. data/lib/airbrake-ruby/async_sender.rb +80 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +54 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +125 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +155 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +54 -0
  38. data/lib/airbrake-ruby/request.rb +46 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/thread_pool.rb +128 -0
  45. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +115 -0
  48. data/lib/airbrake-ruby/version.rb +6 -0
  49. data/spec/airbrake_spec.rb +324 -0
  50. data/spec/async_sender_spec.rb +72 -0
  51. data/spec/backtrace_spec.rb +427 -0
  52. data/spec/benchmark_spec.rb +33 -0
  53. data/spec/code_hunk_spec.rb +115 -0
  54. data/spec/config/validator_spec.rb +184 -0
  55. data/spec/config_spec.rb +154 -0
  56. data/spec/deploy_notifier_spec.rb +48 -0
  57. data/spec/file_cache_spec.rb +34 -0
  58. data/spec/filter_chain_spec.rb +92 -0
  59. data/spec/filters/context_filter_spec.rb +23 -0
  60. data/spec/filters/dependency_filter_spec.rb +12 -0
  61. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  62. data/spec/filters/gem_root_filter_spec.rb +41 -0
  63. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  64. data/spec/filters/git_repository_filter.rb +61 -0
  65. data/spec/filters/git_revision_filter_spec.rb +126 -0
  66. data/spec/filters/keys_blacklist_spec.rb +225 -0
  67. data/spec/filters/keys_whitelist_spec.rb +194 -0
  68. data/spec/filters/root_directory_filter_spec.rb +39 -0
  69. data/spec/filters/sql_filter_spec.rb +262 -0
  70. data/spec/filters/system_exit_filter_spec.rb +14 -0
  71. data/spec/filters/thread_filter_spec.rb +277 -0
  72. data/spec/fixtures/notroot.txt +7 -0
  73. data/spec/fixtures/project_root/code.rb +221 -0
  74. data/spec/fixtures/project_root/empty_file.rb +0 -0
  75. data/spec/fixtures/project_root/long_line.txt +1 -0
  76. data/spec/fixtures/project_root/short_file.rb +3 -0
  77. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  78. data/spec/helpers.rb +9 -0
  79. data/spec/ignorable_spec.rb +14 -0
  80. data/spec/inspectable_spec.rb +45 -0
  81. data/spec/monotonic_time_spec.rb +12 -0
  82. data/spec/nested_exception_spec.rb +73 -0
  83. data/spec/notice_notifier/options_spec.rb +259 -0
  84. data/spec/notice_notifier_spec.rb +356 -0
  85. data/spec/notice_spec.rb +296 -0
  86. data/spec/performance_breakdown_spec.rb +12 -0
  87. data/spec/performance_notifier_spec.rb +491 -0
  88. data/spec/promise_spec.rb +197 -0
  89. data/spec/query_spec.rb +11 -0
  90. data/spec/request_spec.rb +11 -0
  91. data/spec/response_spec.rb +88 -0
  92. data/spec/spec_helper.rb +100 -0
  93. data/spec/stashable_spec.rb +23 -0
  94. data/spec/stat_spec.rb +47 -0
  95. data/spec/sync_sender_spec.rb +133 -0
  96. data/spec/tdigest_spec.rb +230 -0
  97. data/spec/thread_pool_spec.rb +158 -0
  98. data/spec/time_truncate_spec.rb +13 -0
  99. data/spec/timed_trace_spec.rb +125 -0
  100. data/spec/truncator_spec.rb +238 -0
  101. metadata +216 -0
@@ -0,0 +1,33 @@
1
+ RSpec.describe Airbrake::Benchmark do
2
+ describe ".measure" do
3
+ it "returns measured performance time" do
4
+ expect(described_class.measure { '10' * 10 }).to be_kind_of(Numeric)
5
+ end
6
+ end
7
+
8
+ describe "#stop" do
9
+ before { subject }
10
+
11
+ context "when called one time" do
12
+ its(:stop) { is_expected.to eq(true) }
13
+ end
14
+
15
+ context "when called twice or more" do
16
+ before { subject.stop }
17
+
18
+ its(:stop) { is_expected.to eq(false) }
19
+ end
20
+ end
21
+
22
+ describe "#duration" do
23
+ context "when #stop wasn't called yet" do
24
+ its(:duration) { is_expected.to be_zero }
25
+ end
26
+
27
+ context "when #stop was called" do
28
+ before { subject.stop }
29
+
30
+ its(:duration) { is_expected.to be > 0 }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,115 @@
1
+ RSpec.describe Airbrake::CodeHunk do
2
+ after do
3
+ %w[empty_file.rb code.rb banana.rb short_file.rb long_line.txt].each do |f|
4
+ Airbrake::FileCache[project_root_path(f)] = nil
5
+ end
6
+ end
7
+
8
+ describe "#to_h" do
9
+ context "when file is empty" do
10
+ subject do
11
+ described_class.new.get(project_root_path('empty_file.rb'), 1)
12
+ end
13
+
14
+ it { is_expected.to eq(1 => '') }
15
+ end
16
+
17
+ context "when line is nil" do
18
+ subject { described_class.new.get(project_root_path('code.rb'), nil) }
19
+
20
+ it { is_expected.to be_nil }
21
+ end
22
+
23
+ context "when a file doesn't exist" do
24
+ subject { described_class.new.get(project_root_path('banana.rb'), 1) }
25
+
26
+ it { is_expected.to be_nil }
27
+ end
28
+
29
+ context "when a file has less than NLINES lines before start line" do
30
+ subject { described_class.new.get(project_root_path('code.rb'), 1) }
31
+
32
+ it do
33
+ is_expected.to(
34
+ eq(
35
+ 1 => 'module Airbrake',
36
+ 2 => ' ##',
37
+ # rubocop:disable Metrics/LineLength
38
+ 3 => ' # Represents a chunk of information that is meant to be either sent to',
39
+ # rubocop:enable Metrics/LineLength
40
+ )
41
+ )
42
+ end
43
+ end
44
+
45
+ context "when a file has less than NLINES lines after end line" do
46
+ subject { described_class.new.get(project_root_path('code.rb'), 222) }
47
+
48
+ it do
49
+ is_expected.to(
50
+ eq(
51
+ 220 => ' end',
52
+ 221 => 'end'
53
+ )
54
+ )
55
+ end
56
+ end
57
+
58
+ context "when a file has less than NLINES lines before and after" do
59
+ subject do
60
+ described_class.new.get(project_root_path('short_file.rb'), 2)
61
+ end
62
+
63
+ it do
64
+ is_expected.to(
65
+ eq(
66
+ 1 => 'module Banana',
67
+ 2 => ' attr_reader :bingo',
68
+ 3 => 'end'
69
+ )
70
+ )
71
+ end
72
+ end
73
+
74
+ context "when a file has enough lines before and after" do
75
+ subject { described_class.new.get(project_root_path('code.rb'), 100) }
76
+
77
+ it do
78
+ is_expected.to(
79
+ eq(
80
+ 98 => ' return json if json && json.bytesize <= MAX_NOTICE_SIZE',
81
+ 99 => ' end',
82
+ 100 => '',
83
+ 101 => ' break if truncate == 0',
84
+ 102 => ' end'
85
+ )
86
+ )
87
+ end
88
+ end
89
+
90
+ context "when a line exceeds the length limit" do
91
+ subject do
92
+ described_class.new.get(project_root_path('long_line.txt'), 1)
93
+ end
94
+
95
+ it "strips the line" do
96
+ expect(subject[1]).to eq('l' + 'o' * 196 + 'ng')
97
+ end
98
+ end
99
+
100
+ context "when an error occurrs while fetching code" do
101
+ before do
102
+ expect(Airbrake::FileCache).to receive(:[]).and_raise(Errno::EACCES)
103
+ end
104
+
105
+ it "logs error and returns nil" do
106
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
107
+ /can't read code hunk.+Permission denied/
108
+ )
109
+ expect(subject.get(project_root_path('code.rb'), 1)).to(
110
+ eq(1 => '')
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,184 @@
1
+ RSpec.describe Airbrake::Config::Validator do
2
+ let(:valid_id) { 123 }
3
+ let(:valid_key) { '123' }
4
+ let(:config) { Airbrake::Config.new(config_params) }
5
+
6
+ describe ".validate" do
7
+ context "when project_id is numerical" do
8
+ let(:config_params) { { project_id: valid_id, project_key: valid_key } }
9
+
10
+ it "returns a resolved promise" do
11
+ promise = described_class.validate(config)
12
+ expect(promise).to be_resolved
13
+ end
14
+ end
15
+
16
+ context "when project_id is a numerical String" do
17
+ let(:config_params) { { project_id: '123', project_key: valid_key } }
18
+
19
+ it "returns a resolved promise" do
20
+ promise = described_class.validate(config)
21
+ expect(promise).to be_resolved
22
+ end
23
+ end
24
+
25
+ context "when project_id is zero" do
26
+ let(:config_params) { { project_id: 0, project_key: valid_key } }
27
+
28
+ it "returns a rejected promise" do
29
+ promise = described_class.validate(config)
30
+ expect(promise.value).to eq('error' => ':project_id is required')
31
+ end
32
+ end
33
+
34
+ context "when project_id consists of letters" do
35
+ let(:config_params) { { project_id: 'foo', project_key: valid_key } }
36
+
37
+ it "returns a rejected promise" do
38
+ promise = described_class.validate(config)
39
+ expect(promise.value).to eq('error' => ':project_id is required')
40
+ end
41
+ end
42
+
43
+ context "when project_id is less than zero" do
44
+ let(:config_params) { { project_id: -123, project_key: valid_key } }
45
+
46
+ it "returns a rejected promise" do
47
+ promise = described_class.validate(config)
48
+ expect(promise.value).to eq('error' => ':project_id is required')
49
+ end
50
+ end
51
+
52
+ context "when project_key is a non-empty String" do
53
+ let(:config_params) { { project_id: valid_id, project_key: '123' } }
54
+
55
+ it "returns a resolved promise" do
56
+ promise = described_class.validate(config)
57
+ expect(promise).to be_resolved
58
+ end
59
+ end
60
+
61
+ context "when project_key is an empty String" do
62
+ let(:config_params) { { project_id: valid_id, project_key: '' } }
63
+
64
+ it "returns a rejected promise" do
65
+ promise = described_class.validate(config)
66
+ expect(promise.value).to eq('error' => ':project_key is required')
67
+ end
68
+ end
69
+
70
+ context "when project_key is a non-String" do
71
+ let(:config_params) { { project_id: valid_id, project_key: 123 } }
72
+
73
+ it "returns a rejected promise" do
74
+ promise = described_class.validate(config)
75
+ expect(promise.value).to eq('error' => ':project_key is required')
76
+ end
77
+ end
78
+
79
+ context "when environment is nil" do
80
+ let(:config_params) do
81
+ { project_id: valid_id, project_key: valid_key, environment: nil }
82
+ end
83
+
84
+ it "returns a resolved promise" do
85
+ promise = described_class.validate(config)
86
+ expect(promise).to be_resolved
87
+ end
88
+ end
89
+
90
+ context "when environment is a String" do
91
+ let(:config_params) do
92
+ { project_id: valid_id, project_key: valid_key, environment: 'test' }
93
+ end
94
+
95
+ it "returns a resolved promise" do
96
+ promise = described_class.validate(config)
97
+ expect(promise).to be_resolved
98
+ end
99
+ end
100
+
101
+ context "when environment is a Symbol" do
102
+ let(:config_params) do
103
+ { project_id: valid_id, project_key: valid_key, environment: :test }
104
+ end
105
+
106
+ it "returns a resolved promise" do
107
+ promise = described_class.validate(config)
108
+ expect(promise).to be_resolved
109
+ end
110
+ end
111
+
112
+ context "when environment is non-String and non-Symbol" do
113
+ let(:config_params) do
114
+ { project_id: valid_id, project_key: valid_key, environment: 1.0 }
115
+ end
116
+
117
+ it "returns a rejected promise" do
118
+ promise = described_class.validate(config)
119
+ expect(promise.value).to eq(
120
+ 'error' => "the 'environment' option must be configured with a " \
121
+ "Symbol (or String), but 'Float' was provided: 1.0"
122
+ )
123
+ end
124
+ end
125
+
126
+ context "when environment is String-like" do
127
+ let(:string_inquirer) { Class.new(String) }
128
+
129
+ let(:config_params) do
130
+ {
131
+ project_id: valid_id,
132
+ project_key: valid_key,
133
+ environment: string_inquirer.new('test')
134
+ }
135
+ end
136
+
137
+ it "returns a resolved promise" do
138
+ promise = described_class.validate(config)
139
+ expect(promise).to be_resolved
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "#check_notify_ability" do
145
+ context "when current environment is ignored" do
146
+ let(:config_params) do
147
+ {
148
+ project_id: valid_id,
149
+ project_key: valid_key,
150
+ environment: 'test',
151
+ ignore_environments: ['test']
152
+ }
153
+ end
154
+
155
+ it "returns a rejected promise" do
156
+ promise = described_class.check_notify_ability(config)
157
+ expect(promise.value).to eq(
158
+ 'error' => "current environment 'test' is ignored"
159
+ )
160
+ end
161
+ end
162
+
163
+ context "when no environment is specified but ignore_environments is" do
164
+ let(:config_params) do
165
+ {
166
+ project_id: valid_id,
167
+ project_key: valid_key,
168
+ ignore_environments: ['test']
169
+ }
170
+ end
171
+
172
+ it "returns a rejected promise" do
173
+ promise = described_class.check_notify_ability(config)
174
+ expect(promise).to be_resolved
175
+ end
176
+
177
+ it "warns about 'no effect'" do
178
+ expect(config.logger).to receive(:warn)
179
+ .with(/'ignore_environments' has no effect/)
180
+ described_class.check_notify_ability(config)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,154 @@
1
+ RSpec.describe Airbrake::Config do
2
+ let(:resolved_promise) { Airbrake::Promise.new.resolve }
3
+ let(:rejected_promise) { Airbrake::Promise.new.reject }
4
+
5
+ let(:valid_params) { { project_id: 1, project_key: '2' } }
6
+
7
+ its(:project_id) { is_expected.to be_nil }
8
+ its(:project_key) { is_expected.to be_nil }
9
+ its(:logger) { is_expected.to be_a(Logger) }
10
+ its(:app_version) { is_expected.to be_nil }
11
+ its(:versions) { is_expected.to be_empty }
12
+ its(:host) { is_expected.to eq('https://api.airbrake.io') }
13
+ its(:endpoint) { is_expected.not_to be_nil }
14
+ its(:workers) { is_expected.to eq(1) }
15
+ its(:queue_size) { is_expected.to eq(100) }
16
+ its(:root_directory) { is_expected.to eq(Bundler.root.realpath.to_s) }
17
+ its(:environment) { is_expected.to be_nil }
18
+ its(:ignore_environments) { is_expected.to be_empty }
19
+ its(:timeout) { is_expected.to be_nil }
20
+ its(:blacklist_keys) { is_expected.to be_empty }
21
+ its(:whitelist_keys) { is_expected.to be_empty }
22
+ its(:performance_stats) { is_expected.to eq(true) }
23
+ its(:performance_stats_flush_period) { is_expected.to eq(15) }
24
+ its(:query_stats) { is_expected.to eq(false) }
25
+
26
+ describe "#new" do
27
+ context "when user config is passed" do
28
+ subject { described_class.new(logger: StringIO.new) }
29
+ its(:logger) { is_expected.to be_a(StringIO) }
30
+ end
31
+ end
32
+
33
+ describe "#valid?" do
34
+ context "when #validate returns a resolved promise" do
35
+ before { expect(subject).to receive(:validate).and_return(resolved_promise) }
36
+ it { is_expected.to be_valid }
37
+ end
38
+
39
+ context "when #validate returns a rejected promise" do
40
+ before { expect(subject).to receive(:validate).and_return(rejected_promise) }
41
+ it { is_expected.not_to be_valid }
42
+ end
43
+ end
44
+
45
+ describe "#ignored_environment?" do
46
+ context "when Validator returns a resolved promise" do
47
+ before do
48
+ expect(Airbrake::Config::Validator).to receive(:check_notify_ability)
49
+ .and_return(resolved_promise)
50
+ end
51
+
52
+ its(:ignored_environment?) { is_expected.to be_falsey }
53
+ end
54
+
55
+ context "when Validator returns a rejected promise" do
56
+ before do
57
+ expect(Airbrake::Config::Validator).to receive(:check_notify_ability)
58
+ .and_return(rejected_promise)
59
+ end
60
+
61
+ its(:ignored_environment?) { is_expected.to be_truthy }
62
+ end
63
+ end
64
+
65
+ describe "#endpoint" do
66
+ subject { described_class.new(valid_params.merge(user_config)) }
67
+
68
+ context "when host ends with a URL with a slug with a trailing slash" do
69
+ let(:user_config) { { host: 'https://localhost/bingo/' } }
70
+
71
+ its(:endpoint) do
72
+ is_expected.to eq(URI('https://localhost/bingo/api/v3/projects/1/notices'))
73
+ end
74
+ end
75
+
76
+ context "when host ends with a URL with a slug without a trailing slash" do
77
+ let(:user_config) { { host: 'https://localhost/bingo' } }
78
+
79
+ its(:endpoint) do
80
+ is_expected.to eq(URI('https://localhost/api/v3/projects/1/notices'))
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "#validate" do
86
+ its(:validate) { is_expected.to be_an(Airbrake::Promise) }
87
+ end
88
+
89
+ describe "#check_configuration" do
90
+ let(:user_config) { {} }
91
+
92
+ subject { described_class.new(valid_params.merge(user_config)) }
93
+
94
+ its(:check_configuration) { is_expected.to be_an(Airbrake::Promise) }
95
+
96
+ context "when config is invalid" do
97
+ let(:user_config) { { project_id: nil } }
98
+ its(:check_configuration) { is_expected.to be_rejected }
99
+ end
100
+
101
+ context "when current environment is ignored" do
102
+ let(:user_config) { { environment: 'test', ignore_environments: ['test'] } }
103
+ its(:check_configuration) { is_expected.to be_rejected }
104
+ end
105
+
106
+ context "when config is valid and allows notifying" do
107
+ its(:check_configuration) { is_expected.not_to be_rejected }
108
+ end
109
+ end
110
+
111
+ describe "#check_performance_options" do
112
+ it "returns a promise" do
113
+ resource = Airbrake::Query.new(
114
+ method: '', route: '', query: '', start_time: Time.now
115
+ )
116
+ expect(subject.check_performance_options(resource))
117
+ .to be_an(Airbrake::Promise)
118
+ end
119
+
120
+ context "when performance stats are disabled" do
121
+ before { subject.performance_stats = false }
122
+
123
+ let(:resource) do
124
+ Airbrake::Request.new(
125
+ method: 'GET', route: '/foo', status_code: 200, start_time: Time.new
126
+ )
127
+ end
128
+
129
+ it "returns a rejected promise" do
130
+ promise = subject.check_performance_options(resource)
131
+ expect(promise.value).to eq(
132
+ 'error' => "The Performance Stats feature is disabled"
133
+ )
134
+ end
135
+ end
136
+
137
+ context "when query stats are disabled" do
138
+ before { subject.query_stats = false }
139
+
140
+ let(:resource) do
141
+ Airbrake::Query.new(
142
+ method: 'GET', route: '/foo', query: '', start_time: Time.new
143
+ )
144
+ end
145
+
146
+ it "returns a rejected promise" do
147
+ promise = subject.check_performance_options(resource)
148
+ expect(promise.value).to eq(
149
+ 'error' => "The Query Stats feature is disabled"
150
+ )
151
+ end
152
+ end
153
+ end
154
+ end