airbrake-ruby 4.6.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +513 -0
  3. data/lib/airbrake-ruby/async_sender.rb +142 -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 +48 -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 +104 -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 +45 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +125 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +53 -0
  38. data/lib/airbrake-ruby/request.rb +45 -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/time_truncate.rb +17 -0
  45. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  46. data/lib/airbrake-ruby/truncator.rb +115 -0
  47. data/lib/airbrake-ruby/version.rb +6 -0
  48. data/spec/airbrake_spec.rb +324 -0
  49. data/spec/async_sender_spec.rb +155 -0
  50. data/spec/backtrace_spec.rb +427 -0
  51. data/spec/benchmark_spec.rb +33 -0
  52. data/spec/code_hunk_spec.rb +115 -0
  53. data/spec/config/validator_spec.rb +184 -0
  54. data/spec/config_spec.rb +154 -0
  55. data/spec/deploy_notifier_spec.rb +48 -0
  56. data/spec/file_cache.rb +36 -0
  57. data/spec/filter_chain_spec.rb +92 -0
  58. data/spec/filters/context_filter_spec.rb +23 -0
  59. data/spec/filters/dependency_filter_spec.rb +12 -0
  60. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  61. data/spec/filters/gem_root_filter_spec.rb +41 -0
  62. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  63. data/spec/filters/git_repository_filter.rb +61 -0
  64. data/spec/filters/git_revision_filter_spec.rb +126 -0
  65. data/spec/filters/keys_blacklist_spec.rb +225 -0
  66. data/spec/filters/keys_whitelist_spec.rb +194 -0
  67. data/spec/filters/root_directory_filter_spec.rb +39 -0
  68. data/spec/filters/sql_filter_spec.rb +219 -0
  69. data/spec/filters/system_exit_filter_spec.rb +14 -0
  70. data/spec/filters/thread_filter_spec.rb +277 -0
  71. data/spec/fixtures/notroot.txt +7 -0
  72. data/spec/fixtures/project_root/code.rb +221 -0
  73. data/spec/fixtures/project_root/empty_file.rb +0 -0
  74. data/spec/fixtures/project_root/long_line.txt +1 -0
  75. data/spec/fixtures/project_root/short_file.rb +3 -0
  76. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  77. data/spec/helpers.rb +9 -0
  78. data/spec/ignorable_spec.rb +14 -0
  79. data/spec/inspectable_spec.rb +45 -0
  80. data/spec/monotonic_time_spec.rb +12 -0
  81. data/spec/nested_exception_spec.rb +73 -0
  82. data/spec/notice_notifier_spec.rb +356 -0
  83. data/spec/notice_notifier_spec/options_spec.rb +259 -0
  84. data/spec/notice_spec.rb +296 -0
  85. data/spec/performance_breakdown_spec.rb +12 -0
  86. data/spec/performance_notifier_spec.rb +435 -0
  87. data/spec/promise_spec.rb +197 -0
  88. data/spec/query_spec.rb +11 -0
  89. data/spec/request_spec.rb +11 -0
  90. data/spec/response_spec.rb +88 -0
  91. data/spec/spec_helper.rb +100 -0
  92. data/spec/stashable_spec.rb +23 -0
  93. data/spec/stat_spec.rb +47 -0
  94. data/spec/sync_sender_spec.rb +133 -0
  95. data/spec/tdigest_spec.rb +230 -0
  96. data/spec/time_truncate_spec.rb +13 -0
  97. data/spec/timed_trace_spec.rb +125 -0
  98. data/spec/truncator_spec.rb +238 -0
  99. metadata +213 -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