airbrake-ruby 3.2.2-java

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +554 -0
  3. data/lib/airbrake-ruby/async_sender.rb +119 -0
  4. data/lib/airbrake-ruby/backtrace.rb +194 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +53 -0
  6. data/lib/airbrake-ruby/config.rb +238 -0
  7. data/lib/airbrake-ruby/config/validator.rb +63 -0
  8. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  9. data/lib/airbrake-ruby/file_cache.rb +48 -0
  10. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  11. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  18. data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
  19. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  20. data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  25. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  26. data/lib/airbrake-ruby/ignorable.rb +44 -0
  27. data/lib/airbrake-ruby/nested_exception.rb +39 -0
  28. data/lib/airbrake-ruby/notice.rb +165 -0
  29. data/lib/airbrake-ruby/notice_notifier.rb +228 -0
  30. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  31. data/lib/airbrake-ruby/promise.rb +99 -0
  32. data/lib/airbrake-ruby/response.rb +71 -0
  33. data/lib/airbrake-ruby/stat.rb +56 -0
  34. data/lib/airbrake-ruby/sync_sender.rb +111 -0
  35. data/lib/airbrake-ruby/tdigest.rb +393 -0
  36. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  37. data/lib/airbrake-ruby/truncator.rb +115 -0
  38. data/lib/airbrake-ruby/version.rb +6 -0
  39. data/spec/airbrake_spec.rb +171 -0
  40. data/spec/async_sender_spec.rb +154 -0
  41. data/spec/backtrace_spec.rb +438 -0
  42. data/spec/code_hunk_spec.rb +118 -0
  43. data/spec/config/validator_spec.rb +189 -0
  44. data/spec/config_spec.rb +281 -0
  45. data/spec/deploy_notifier_spec.rb +41 -0
  46. data/spec/file_cache.rb +36 -0
  47. data/spec/filter_chain_spec.rb +83 -0
  48. data/spec/filters/context_filter_spec.rb +25 -0
  49. data/spec/filters/dependency_filter_spec.rb +14 -0
  50. data/spec/filters/exception_attributes_filter_spec.rb +63 -0
  51. data/spec/filters/gem_root_filter_spec.rb +44 -0
  52. data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
  53. data/spec/filters/git_repository_filter.rb +53 -0
  54. data/spec/filters/git_revision_filter_spec.rb +126 -0
  55. data/spec/filters/keys_blacklist_spec.rb +236 -0
  56. data/spec/filters/keys_whitelist_spec.rb +205 -0
  57. data/spec/filters/root_directory_filter_spec.rb +42 -0
  58. data/spec/filters/sql_filter_spec.rb +219 -0
  59. data/spec/filters/system_exit_filter_spec.rb +14 -0
  60. data/spec/filters/thread_filter_spec.rb +279 -0
  61. data/spec/fixtures/notroot.txt +7 -0
  62. data/spec/fixtures/project_root/code.rb +221 -0
  63. data/spec/fixtures/project_root/empty_file.rb +0 -0
  64. data/spec/fixtures/project_root/long_line.txt +1 -0
  65. data/spec/fixtures/project_root/short_file.rb +3 -0
  66. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  67. data/spec/helpers.rb +9 -0
  68. data/spec/ignorable_spec.rb +14 -0
  69. data/spec/nested_exception_spec.rb +75 -0
  70. data/spec/notice_notifier_spec.rb +436 -0
  71. data/spec/notice_notifier_spec/options_spec.rb +266 -0
  72. data/spec/notice_spec.rb +297 -0
  73. data/spec/performance_notifier_spec.rb +287 -0
  74. data/spec/promise_spec.rb +165 -0
  75. data/spec/response_spec.rb +82 -0
  76. data/spec/spec_helper.rb +102 -0
  77. data/spec/stat_spec.rb +35 -0
  78. data/spec/sync_sender_spec.rb +140 -0
  79. data/spec/tdigest_spec.rb +230 -0
  80. data/spec/time_truncate_spec.rb +13 -0
  81. data/spec/truncator_spec.rb +238 -0
  82. metadata +278 -0
@@ -0,0 +1,393 @@
1
+ require 'rbtree'
2
+
3
+ module Airbrake
4
+ # Ruby implementation of Ted Dunning's t-digest data structure.
5
+ #
6
+ # This implementation is imported from https://github.com/castle/tdigest with
7
+ # custom modifications. Huge thanks to Castle for the implementation :beer:
8
+ #
9
+ # The difference is that we pack with Big Endian (unlike Native Endian in
10
+ # Castle's version). Our backend does not permit little endian.
11
+ #
12
+ # @see https://github.com/tdunning/t-digest
13
+ # @see https://github.com/castle/tdigest
14
+ # @api private
15
+ # @since v3.2.0
16
+ #
17
+ # rubocop:disable Metrics/ClassLength
18
+ class TDigest
19
+ VERBOSE_ENCODING = 1
20
+ SMALL_ENCODING = 2
21
+
22
+ # Centroid represents a number of data points.
23
+ # @api private
24
+ # @since v3.2.0
25
+ class Centroid
26
+ attr_accessor :mean, :n, :cumn, :mean_cumn
27
+ def initialize(mean, n, cumn, mean_cumn = nil)
28
+ @mean = mean
29
+ @n = n
30
+ @cumn = cumn
31
+ @mean_cumn = mean_cumn
32
+ end
33
+
34
+ def as_json(_ = nil)
35
+ { m: mean, n: n }
36
+ end
37
+ end
38
+
39
+ attr_accessor :centroids
40
+ def initialize(delta = 0.01, k = 25, cx = 1.1)
41
+ @delta = delta
42
+ @k = k
43
+ @cx = cx
44
+ @centroids = RBTree.new
45
+ @nreset = 0
46
+ @n = 0
47
+ reset!
48
+ end
49
+
50
+ def +(other)
51
+ # Uses delta, k and cx from the caller
52
+ t = self.class.new(@delta, @k, @cx)
53
+ data = centroids.values + other.centroids.values
54
+ t.push_centroid(data.delete_at(rand(data.length))) while data.any?
55
+ t
56
+ end
57
+
58
+ def as_bytes
59
+ # compression as defined by Java implementation
60
+ size = @centroids.size
61
+ output = [VERBOSE_ENCODING, compression, size]
62
+ output += @centroids.map { |_, c| c.mean }
63
+ output += @centroids.map { |_, c| c.n }
64
+ output.pack("NGNG#{size}N#{size}")
65
+ end
66
+
67
+ # rubocop:disable Metrics/AbcSize
68
+ def as_small_bytes
69
+ size = @centroids.size
70
+ output = [self.class::SMALL_ENCODING, compression, size]
71
+ x = 0
72
+ # delta encoding allows saving 4-bytes floats
73
+ mean_arr = @centroids.map do |_, c|
74
+ val = c.mean - x
75
+ x = c.mean
76
+ val
77
+ end
78
+ output += mean_arr
79
+ # Variable length encoding of numbers
80
+ c_arr = @centroids.each_with_object([]) do |(_, c), arr|
81
+ k = 0
82
+ n = c.n
83
+ while n < 0 || n > 0x7f
84
+ b = 0x80 | (0x7f & n)
85
+ arr << b
86
+ n = n >> 7
87
+ k += 1
88
+ raise 'Unreasonable large number' if k > 6
89
+ end
90
+ arr << n
91
+ end
92
+ output += c_arr
93
+ output.pack("NGNg#{size}C#{size}")
94
+ end
95
+ # rubocop:enable Metrics/AbcSize
96
+
97
+ def as_json(_ = nil)
98
+ @centroids.map { |_, c| c.as_json }
99
+ end
100
+
101
+ def bound_mean(x)
102
+ upper = @centroids.upper_bound(x)
103
+ lower = @centroids.lower_bound(x)
104
+ [lower[1], upper[1]]
105
+ end
106
+
107
+ def bound_mean_cumn(cumn)
108
+ last_c = nil
109
+ bounds = []
110
+ @centroids.each_value do |v|
111
+ if v.mean_cumn == cumn
112
+ bounds << v
113
+ break
114
+ elsif v.mean_cumn > cumn
115
+ bounds << last_c
116
+ bounds << v
117
+ break
118
+ else
119
+ last_c = v
120
+ end
121
+ end
122
+ # If still no results, pick lagging value if any
123
+ bounds << last_c if bounds.empty? && !last_c.nil?
124
+
125
+ bounds
126
+ end
127
+
128
+ def compress!
129
+ points = to_a
130
+ reset!
131
+ push_centroid(points.shuffle)
132
+ _cumulate(true, true)
133
+ nil
134
+ end
135
+
136
+ def compression
137
+ 1 / @delta
138
+ end
139
+
140
+ def find_nearest(x)
141
+ return nil if size == 0
142
+
143
+ ceil = @centroids.upper_bound(x)
144
+ floor = @centroids.lower_bound(x)
145
+
146
+ return floor[1] if ceil.nil?
147
+ return ceil[1] if floor.nil?
148
+
149
+ ceil_key = ceil[0]
150
+ floor_key = floor[0]
151
+
152
+ if (floor_key - x).abs < (ceil_key - x).abs
153
+ floor[1]
154
+ else
155
+ ceil[1]
156
+ end
157
+ end
158
+
159
+ def merge!(other)
160
+ push_centroid(other.centroids.values.shuffle)
161
+ self
162
+ end
163
+
164
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
165
+ # rubocop:disable Metrics/CyclomaticComplexity
166
+ def p_rank(x)
167
+ is_array = x.is_a? Array
168
+ x = [x] unless is_array
169
+
170
+ min = @centroids.first
171
+ max = @centroids.last
172
+
173
+ x.map! do |item|
174
+ if size == 0
175
+ nil
176
+ elsif item < min[1].mean
177
+ 0.0
178
+ elsif item > max[1].mean
179
+ 1.0
180
+ else
181
+ _cumulate(true)
182
+ bound = bound_mean(item)
183
+ lower, upper = bound
184
+ mean_cumn = lower.mean_cumn
185
+ if lower != upper
186
+ mean_cumn += (item - lower.mean) * (upper.mean_cumn - lower.mean_cumn) \
187
+ / (upper.mean - lower.mean)
188
+ end
189
+ mean_cumn / @n
190
+ end
191
+ end
192
+ is_array ? x : x.first
193
+ end
194
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize
195
+ # rubocop:enable Metrics/CyclomaticComplexity
196
+
197
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
198
+ # rubocop:disable Metrics/AbcSize
199
+ def percentile(p)
200
+ is_array = p.is_a? Array
201
+ p = [p] unless is_array
202
+ p.map! do |item|
203
+ unless (0..1).cover?(item)
204
+ raise ArgumentError, "p should be in [0,1], got #{item}"
205
+ end
206
+ if size == 0
207
+ nil
208
+ else
209
+ _cumulate(true)
210
+ h = @n * item
211
+ lower, upper = bound_mean_cumn(h)
212
+ if lower.nil? && upper.nil?
213
+ nil
214
+ elsif upper == lower || lower.nil? || upper.nil?
215
+ (lower || upper).mean
216
+ elsif h == lower.mean_cumn
217
+ lower.mean
218
+ else
219
+ upper.mean
220
+ end
221
+ end
222
+ end
223
+ is_array ? p : p.first
224
+ end
225
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
226
+ # rubocop:enable Metrics/AbcSize
227
+
228
+ def push(x, n = 1)
229
+ x = [x] unless x.is_a? Array
230
+ x.each { |value| _digest(value, n) }
231
+ end
232
+
233
+ def push_centroid(c)
234
+ c = [c] unless c.is_a? Array
235
+ c.each { |centroid| _digest(centroid.mean, centroid.n) }
236
+ end
237
+
238
+ def reset!
239
+ @centroids.clear
240
+ @n = 0
241
+ @nreset += 1
242
+ @last_cumulate = 0
243
+ end
244
+
245
+ def size
246
+ @n || 0
247
+ end
248
+
249
+ def to_a
250
+ @centroids.map { |_, c| c }
251
+ end
252
+
253
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
254
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
255
+ def self.from_bytes(bytes)
256
+ format, compression, size = bytes.unpack('NGN')
257
+ tdigest = new(1 / compression)
258
+
259
+ start_idx = 16 # after header
260
+ case format
261
+ when VERBOSE_ENCODING
262
+ array = bytes[start_idx..-1].unpack("G#{size}N#{size}")
263
+ means, counts = array.each_slice(size).to_a if array.any?
264
+ when SMALL_ENCODING
265
+ means = bytes[start_idx..(start_idx + 4 * size)].unpack("g#{size}")
266
+ # Decode delta encoding of means
267
+ x = 0
268
+ means.map! do |m|
269
+ m += x
270
+ x = m
271
+ m
272
+ end
273
+ counts_bytes = bytes[(start_idx + 4 * size)..-1].unpack('C*')
274
+ counts = []
275
+ # Decode variable length integer bytes
276
+ size.times do
277
+ v = counts_bytes.shift
278
+ z = 0x7f & v
279
+ shift = 7
280
+ while (v & 0x80) != 0
281
+ raise 'Shift too large in decode' if shift > 28
282
+ v = counts_bytes.shift || 0
283
+ z += (v & 0x7f) << shift
284
+ shift += 7
285
+ end
286
+ counts << z
287
+ end
288
+ # This shouldn't happen
289
+ raise 'Mismatch' unless counts.size == means.size
290
+ else
291
+ raise 'Unknown compression format'
292
+ end
293
+
294
+ means.zip(counts).each { |val| tdigest.push(val[0], val[1]) } if means && counts
295
+
296
+ tdigest
297
+ end
298
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength
299
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize
300
+
301
+ def self.from_json(array)
302
+ tdigest = new
303
+ # Handle both string and symbol keys
304
+ array.each { |a| tdigest.push(a['m'] || a[:m], a['n'] || a[:n]) }
305
+ tdigest
306
+ end
307
+
308
+ private
309
+
310
+ def _add_weight(nearest, x, n)
311
+ nearest.mean += n * (x - nearest.mean) / (nearest.n + n) unless x == nearest.mean
312
+
313
+ _cumulate(false, true) if nearest.mean_cumn.nil?
314
+
315
+ nearest.cumn += n
316
+ nearest.mean_cumn += n / 2.0
317
+ nearest.n += n
318
+
319
+ nil
320
+ end
321
+
322
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
323
+ def _cumulate(exact = false, force = false)
324
+ unless force
325
+ factor = if @last_cumulate == 0
326
+ Float::INFINITY
327
+ else
328
+ (@n.to_f / @last_cumulate)
329
+ end
330
+ return if @n == @last_cumulate || (!exact && @cx && @cx > factor)
331
+ end
332
+
333
+ cumn = 0
334
+ @centroids.each do |_, c|
335
+ c.mean_cumn = cumn + c.n / 2.0
336
+ cumn = c.cumn = cumn + c.n
337
+ end
338
+ @n = @last_cumulate = cumn
339
+ nil
340
+ end
341
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
342
+
343
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
344
+ # rubocop:disable Metrics/AbcSize
345
+ def _digest(x, n)
346
+ # Use 'first' and 'last' instead of min/max because of performance reasons
347
+ # This works because RBTree is sorted
348
+ min = @centroids.first
349
+ max = @centroids.last
350
+
351
+ min = min.nil? ? nil : min[1]
352
+ max = max.nil? ? nil : max[1]
353
+ nearest = find_nearest(x)
354
+
355
+ @n += n
356
+
357
+ if nearest && nearest.mean == x
358
+ _add_weight(nearest, x, n)
359
+ elsif nearest == min
360
+ _new_centroid(x, n, 0)
361
+ elsif nearest == max
362
+ _new_centroid(x, n, @n)
363
+ else
364
+ p = nearest.mean_cumn.to_f / @n
365
+ max_n = (4 * @n * @delta * p * (1 - p)).floor
366
+ if max_n - nearest.n >= n
367
+ _add_weight(nearest, x, n)
368
+ else
369
+ _new_centroid(x, n, nearest.cumn)
370
+ end
371
+ end
372
+
373
+ _cumulate(false)
374
+
375
+ # If the number of centroids has grown to a very large size,
376
+ # it may be due to values being inserted in sorted order.
377
+ # We combat that by replaying the centroids in random order,
378
+ # which is what compress! does
379
+ compress! if @centroids.size > (@k / @delta)
380
+
381
+ nil
382
+ end
383
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity,
384
+ # rubocop:enable Metrics/AbcSize
385
+
386
+ def _new_centroid(x, n, cumn)
387
+ c = Centroid.new(x, n, cumn)
388
+ @centroids[x] = c
389
+ c
390
+ end
391
+ end
392
+ # rubocop:enable Metrics/ClassLength
393
+ end
@@ -0,0 +1,17 @@
1
+ module Airbrake
2
+ # TimeTruncate contains methods for truncating time.
3
+ #
4
+ # @api private
5
+ # @since v3.2.0
6
+ module TimeTruncate
7
+ # Truncate +time+ to floor minute and turn it into an RFC3339 timestamp.
8
+ #
9
+ # @param [Time] time
10
+ # @return [String]
11
+ def self.utc_truncate_minutes(time)
12
+ tm = time.getutc
13
+
14
+ Time.utc(tm.year, tm.month, tm.day, tm.hour, tm.min).to_datetime.rfc3339
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,115 @@
1
+ module Airbrake
2
+ # This class is responsible for truncation of too big objects. Mainly, you
3
+ # should use it for simple objects such as strings, hashes, & arrays.
4
+ #
5
+ # @api private
6
+ # @since v1.0.0
7
+ class Truncator
8
+ # @return [Hash] the options for +String#encode+
9
+ ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
10
+
11
+ # @return [String] the temporary encoding to be used when fixing invalid
12
+ # strings with +ENCODING_OPTIONS+
13
+ TEMP_ENCODING = 'utf-16'.freeze
14
+
15
+ # @return [String] what to append when something is a circular reference
16
+ CIRCULAR = '[Circular]'.freeze
17
+
18
+ # @return [String] what to append when something is truncated
19
+ TRUNCATED = '[Truncated]'.freeze
20
+
21
+ # @return [Array<Class>] The types that can contain references to itself
22
+ CIRCULAR_TYPES = [Array, Hash, Set].freeze
23
+
24
+ # @param [Integer] max_size maximum size of hashes, arrays and strings
25
+ def initialize(max_size)
26
+ @max_size = max_size
27
+ end
28
+
29
+ # Performs deep truncation of arrays, hashes, sets & strings. Uses a
30
+ # placeholder for recursive objects (`[Circular]`).
31
+ #
32
+ # @param [Object] object The object to truncate
33
+ # @param [Set] seen The cache that helps to detect recursion
34
+ # @return [Object] truncated object
35
+ def truncate(object, seen = Set.new)
36
+ if seen.include?(object.object_id)
37
+ return CIRCULAR if CIRCULAR_TYPES.any? { |t| object.is_a?(t) }
38
+ return object
39
+ end
40
+ truncate_object(object, seen << object.object_id)
41
+ end
42
+
43
+ # Reduces maximum allowed size of hashes, arrays, sets & strings by half.
44
+ # @return [Integer] current +max_size+ value
45
+ def reduce_max_size
46
+ @max_size /= 2
47
+ end
48
+
49
+ private
50
+
51
+ def truncate_object(object, seen)
52
+ case object
53
+ when Hash then truncate_hash(object, seen)
54
+ when Array then truncate_array(object, seen)
55
+ when Set then truncate_set(object, seen)
56
+ when String then truncate_string(object)
57
+ when Numeric, TrueClass, FalseClass, Symbol, NilClass then object
58
+ else
59
+ truncate_string(stringify_object(object))
60
+ end
61
+ end
62
+
63
+ def truncate_string(str)
64
+ fixed_str = replace_invalid_characters(str)
65
+ return fixed_str if fixed_str.length <= @max_size
66
+ (fixed_str.slice(0, @max_size) + TRUNCATED).freeze
67
+ end
68
+
69
+ def stringify_object(object)
70
+ object.to_json
71
+ rescue *Notice::JSON_EXCEPTIONS
72
+ object.to_s
73
+ end
74
+
75
+ def truncate_hash(hash, seen)
76
+ truncated_hash = {}
77
+ hash.each_with_index do |(key, val), idx|
78
+ break if idx + 1 > @max_size
79
+ truncated_hash[key] = truncate(val, seen)
80
+ end
81
+
82
+ truncated_hash.freeze
83
+ end
84
+
85
+ def truncate_array(array, seen)
86
+ array.slice(0, @max_size).map! { |elem| truncate(elem, seen) }.freeze
87
+ end
88
+
89
+ def truncate_set(set, seen)
90
+ truncated_set = Set.new
91
+
92
+ set.each do |elem|
93
+ truncated_set << truncate(elem, seen)
94
+ break if truncated_set.size >= @max_size
95
+ end
96
+
97
+ truncated_set.freeze
98
+ end
99
+
100
+ # Replaces invalid characters in a string with arbitrary encoding.
101
+ #
102
+ # @param [String] str The string to replace characters
103
+ # @return [String] a UTF-8 encoded string
104
+ # @see https://github.com/flori/json/commit/3e158410e81f94dbbc3da6b7b35f4f64983aa4e3
105
+ def replace_invalid_characters(str)
106
+ encoding = str.encoding
107
+ utf8_string = (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII)
108
+ return str if utf8_string && str.valid_encoding?
109
+
110
+ temp_str = str.dup
111
+ temp_str.encode!(TEMP_ENCODING, ENCODING_OPTIONS) if utf8_string
112
+ temp_str.encode!('utf-8', ENCODING_OPTIONS)
113
+ end
114
+ end
115
+ end