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,23 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Skip over SystemExit exceptions, because they're just noise.
4
+ # @api private
5
+ class SystemExitFilter
6
+ # @return [String]
7
+ SYSTEM_EXIT_TYPE = 'SystemExit'.freeze
8
+
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize
13
+ @weight = 130
14
+ end
15
+
16
+ # @macro call_filter
17
+ def call(notice)
18
+ return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
19
+ notice.ignore!
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,92 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches thread & fiber local variables along with general thread
4
+ # information.
5
+ # @api private
6
+ class ThreadFilter
7
+ # @return [Integer]
8
+ attr_reader :weight
9
+
10
+ # @return [Array<Class>] the list of classes that can be safely converted
11
+ # to JSON
12
+ SAFE_CLASSES = [
13
+ NilClass,
14
+ TrueClass,
15
+ FalseClass,
16
+ String,
17
+ Symbol,
18
+ Regexp,
19
+ Numeric
20
+ ].freeze
21
+
22
+ # Variables starting with this prefix are not attached to a notice.
23
+ # @see https://github.com/airbrake/airbrake-ruby/issues/229
24
+ # @return [String]
25
+ IGNORE_PREFIX = '_'.freeze
26
+
27
+ def initialize
28
+ @weight = 110
29
+ end
30
+
31
+ # @macro call_filter
32
+ def call(notice)
33
+ th = Thread.current
34
+ thread_info = {}
35
+
36
+ if (vars = thread_variables(th)).any?
37
+ thread_info[:thread_variables] = vars
38
+ end
39
+
40
+ if (vars = fiber_variables(th)).any?
41
+ thread_info[:fiber_variables] = vars
42
+ end
43
+
44
+ # Present in Ruby 2.3+.
45
+ if th.respond_to?(:name) && (name = th.name)
46
+ thread_info[:name] = name
47
+ end
48
+
49
+ add_thread_info(th, thread_info)
50
+
51
+ notice[:params][:thread] = thread_info
52
+ end
53
+
54
+ private
55
+
56
+ def thread_variables(th)
57
+ th.thread_variables.map.with_object({}) do |var, h|
58
+ next if var.to_s.start_with?(IGNORE_PREFIX)
59
+ h[var] = sanitize_value(th.thread_variable_get(var))
60
+ end
61
+ end
62
+
63
+ def fiber_variables(th)
64
+ th.keys.map.with_object({}) do |key, h|
65
+ next if key.to_s.start_with?(IGNORE_PREFIX)
66
+ h[key] = sanitize_value(th[key])
67
+ end
68
+ end
69
+
70
+ def add_thread_info(th, thread_info)
71
+ thread_info[:self] = th.inspect
72
+ thread_info[:group] = th.group.list.map(&:inspect)
73
+ thread_info[:priority] = th.priority
74
+
75
+ thread_info[:safe_level] = th.safe_level unless Airbrake::JRUBY
76
+ end
77
+
78
+ def sanitize_value(value)
79
+ return value if SAFE_CLASSES.any? { |klass| value.is_a?(klass) }
80
+
81
+ case value
82
+ when Array
83
+ value = value.map { |elem| sanitize_value(elem) }
84
+ when Hash
85
+ Hash[value.map { |k, v| [k, sanitize_value(v)] }]
86
+ else
87
+ value.to_s
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,37 @@
1
+ module Airbrake
2
+ # HashKeyable allows instances of the class to be used as a Hash key in a
3
+ # consistent manner.
4
+ #
5
+ # The class that includes it must implement *to_h*, which defines properties
6
+ # that all of the instances must share in order to produce the same {#hash}.
7
+ #
8
+ # @example
9
+ # class C
10
+ # include Airbrake::HashKeyable
11
+ #
12
+ # def initialize(key)
13
+ # @key = key
14
+ # end
15
+ #
16
+ # def to_h
17
+ # { 'key' => @key }
18
+ # end
19
+ # end
20
+ #
21
+ # h = {}
22
+ # h[C.new('key1')] = 1
23
+ # h[C.new('key1')] #=> 1
24
+ # h[C.new('key2')] #=> nil
25
+ module HashKeyable
26
+ # @param [Object] other
27
+ # @return [Boolean]
28
+ def eql?(other)
29
+ other.is_a?(self.class) && other.hash == hash
30
+ end
31
+
32
+ # @return [Integer]
33
+ def hash
34
+ to_h.hash
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ module Airbrake
2
+ # Ignorable contains methods that allow the includee to be ignored.
3
+ #
4
+ # @example
5
+ # class A
6
+ # include Airbrake::Ignorable
7
+ # end
8
+ #
9
+ # a = A.new
10
+ # a.ignore!
11
+ # a.ignored? #=> true
12
+ #
13
+ # @since v3.2.0
14
+ # @api private
15
+ module Ignorable
16
+ attr_accessor :ignored
17
+
18
+ # Checks whether the instance was ignored.
19
+ # @return [Boolean]
20
+ # @see #ignore!
21
+ # rubocop:disable Style/DoubleNegation
22
+ def ignored?
23
+ !!ignored
24
+ end
25
+ # rubocop:enable Style/DoubleNegation
26
+
27
+ # Ignores an instance. Ignored instances must never reach the Airbrake
28
+ # dashboard.
29
+ # @return [void]
30
+ # @see #ignored?
31
+ def ignore!
32
+ self.ignored = true
33
+ end
34
+
35
+ private
36
+
37
+ # A method that is meant to be used as a guard.
38
+ # @raise [Airbrake::Error] when instance is ignored
39
+ def raise_if_ignored
40
+ return unless ignored?
41
+ raise Airbrake::Error, "cannot access ignored #{self.class}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ module Airbrake
2
+ # Inspectable provides custom inspect methods that reduce clutter printed in
3
+ # REPLs for notifier objects. These custom methods display only essential
4
+ # information such as project id/key and filters.
5
+ #
6
+ # @since v3.2.6
7
+ # @api private
8
+ module Inspectable
9
+ # @return [String] inspect output template
10
+ INSPECT_TEMPLATE =
11
+ "#<%<classname>s:0x%<id>s project_id=\"%<project_id>s\" " \
12
+ "project_key=\"%<project_key>s\" " \
13
+ "host=\"%<host>s\" filter_chain=%<filter_chain>s>".freeze
14
+
15
+ # @return [String] customized inspect to lessen the amount of clutter
16
+ def inspect
17
+ format(
18
+ INSPECT_TEMPLATE,
19
+ classname: self.class.name,
20
+ id: (object_id << 1).to_s(16).rjust(16, '0'),
21
+ project_id: @config.project_id,
22
+ project_key: @config.project_key,
23
+ host: @config.host,
24
+ filter_chain: @filter_chain.inspect
25
+ )
26
+ end
27
+
28
+ # @return [String] {#inspect} for PrettyPrint
29
+ def pretty_print(q)
30
+ q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
31
+ q.text(
32
+ "project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
33
+ "host=\"#{@config.host}\" filter_chain="
34
+ )
35
+ q.pp(@filter_chain)
36
+ q.text('>')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ module Airbrake
2
+ # Loggable is included into any class that wants to be able to log.
3
+ #
4
+ # By default, Loggable defines a null logger that doesn't do anything. You are
5
+ # supposed to overwrite it via the {instance} method before calling {logger}.
6
+ #
7
+ # @example
8
+ # class A
9
+ # include Loggable
10
+ #
11
+ # def initialize
12
+ # logger.debug('Initialized A')
13
+ # end
14
+ # end
15
+ #
16
+ # @since v4.0.0
17
+ # @api private
18
+ module Loggable
19
+ class << self
20
+ # @return [Logger]
21
+ attr_writer :instance
22
+
23
+ # @return [Logger]
24
+ def instance
25
+ @instance ||= ::Logger.new(File::NULL)
26
+ end
27
+ end
28
+
29
+ # @return [Logger] standard Ruby logger object
30
+ def logger
31
+ Loggable.instance
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Airbrake
2
+ # MonotonicTime is a helper for getting monotonic time suitable for
3
+ # performance measurements. It guarantees that the time is strictly linearly
4
+ # increasing (unlike realtime).
5
+ #
6
+ # @example
7
+ # MonotonicTime.time_in_ms #=> 287138801.144576
8
+ #
9
+ # @see http://pubs.opengroup.org/onlinepubs/9699919799/functions/clock_getres.html
10
+ # @since v4.2.4
11
+ # @api private
12
+ module MonotonicTime
13
+ class << self
14
+ # @return [Integer] current monotonic time in milliseconds
15
+ def time_in_ms
16
+ time_in_nanoseconds / (10.0**6)
17
+ end
18
+
19
+ private
20
+
21
+ if defined?(Process::CLOCK_MONOTONIC)
22
+
23
+ def time_in_nanoseconds
24
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
25
+ end
26
+
27
+ elsif RUBY_ENGINE == 'jruby'
28
+
29
+ def time_in_nanoseconds
30
+ java.lang.System.nanoTime
31
+ end
32
+
33
+ else
34
+
35
+ def time_in_nanoseconds
36
+ time = Time.now
37
+ time.to_i * (10**9) + time.nsec
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ module Airbrake
2
+ # A class that is capable of unwinding nested exceptions and representing them
3
+ # as JSON-like hash.
4
+ #
5
+ # @api private
6
+ # @since v1.0.4
7
+ class NestedException
8
+ # @return [Integer] the maximum number of nested exceptions that a notice
9
+ # can unwrap. Exceptions that have a longer cause chain will be ignored
10
+ MAX_NESTED_EXCEPTIONS = 3
11
+
12
+ def initialize(exception)
13
+ @exception = exception
14
+ end
15
+
16
+ def as_json
17
+ unwind_exceptions.map do |exception|
18
+ { type: exception.class.name,
19
+ message: exception.message,
20
+ backtrace: Backtrace.parse(exception) }
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def unwind_exceptions
27
+ exception_list = []
28
+ exception = @exception
29
+
30
+ while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
31
+ exception_list << exception
32
+ exception = (exception.cause if exception.respond_to?(:cause))
33
+ end
34
+
35
+ exception_list
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,162 @@
1
+ module Airbrake
2
+ # Represents a chunk of information that is meant to be either sent to
3
+ # Airbrake or ignored completely.
4
+ #
5
+ # @since v1.0.0
6
+ class Notice
7
+ # @return [Hash{Symbol=>String}] the information about the notifier library
8
+ NOTIFIER = {
9
+ name: 'airbrake-ruby'.freeze,
10
+ version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
+ url: 'https://github.com/airbrake/airbrake-ruby'.freeze
12
+ }.freeze
13
+
14
+ # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
+ # Context tab in the dashboard
16
+ CONTEXT = {
17
+ os: RUBY_PLATFORM,
18
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
+ notifier: NOTIFIER
20
+ }.freeze
21
+
22
+ # @return [Integer] the maxium size of the JSON payload in bytes
23
+ MAX_NOTICE_SIZE = 64000
24
+
25
+ # @return [Integer] the maximum size of hashes, arrays and strings in the
26
+ # notice.
27
+ PAYLOAD_MAX_SIZE = 10000
28
+
29
+ # @return [Array<StandardError>] the list of possible exceptions that might
30
+ # be raised when an object is converted to JSON
31
+ JSON_EXCEPTIONS = [
32
+ IOError,
33
+ NotImplementedError,
34
+ JSON::GeneratorError,
35
+ Encoding::UndefinedConversionError
36
+ ].freeze
37
+
38
+ # @return [Array<Symbol>] the list of keys that can be be overwritten with
39
+ # {Airbrake::Notice#[]=}
40
+ WRITABLE_KEYS = %i[notifier context environment session params].freeze
41
+
42
+ # @return [Array<Symbol>] parts of a Notice's payload that can be modified
43
+ # by the truncator
44
+ TRUNCATABLE_KEYS = %i[errors environment session params].freeze
45
+
46
+ # @return [String] the name of the host machine
47
+ HOSTNAME = Socket.gethostname.freeze
48
+
49
+ # @return [String]
50
+ DEFAULT_SEVERITY = 'error'.freeze
51
+
52
+ include Ignorable
53
+ include Loggable
54
+ include Stashable
55
+
56
+ # @api private
57
+ def initialize(exception, params = {})
58
+ @config = Airbrake::Config.instance
59
+ @payload = {
60
+ errors: NestedException.new(exception).as_json,
61
+ context: context,
62
+ environment: {
63
+ program_name: $PROGRAM_NAME
64
+ },
65
+ session: {},
66
+ params: params
67
+ }
68
+ @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
69
+
70
+ stash[:exception] = exception
71
+ end
72
+
73
+ # Converts the notice to JSON. Calls +to_json+ on each object inside
74
+ # notice's payload. Truncates notices, JSON representation of which is
75
+ # bigger than {MAX_NOTICE_SIZE}.
76
+ #
77
+ # @return [Hash{String=>String}, nil]
78
+ # @api private
79
+ def to_json
80
+ loop do
81
+ begin
82
+ json = @payload.to_json
83
+ rescue *JSON_EXCEPTIONS => ex
84
+ logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
85
+ else
86
+ return json if json && json.bytesize <= MAX_NOTICE_SIZE
87
+ end
88
+
89
+ break if truncate == 0
90
+ end
91
+ end
92
+
93
+ # Reads a value from notice's payload.
94
+ #
95
+ # @return [Object]
96
+ # @raise [Airbrake::Error] if the notice is ignored
97
+ def [](key)
98
+ raise_if_ignored
99
+ @payload[key]
100
+ end
101
+
102
+ # Writes a value to the payload hash. Restricts unrecognized writes.
103
+ #
104
+ # @example
105
+ # notice[:params][:my_param] = 'foobar'
106
+ #
107
+ # @return [void]
108
+ # @raise [Airbrake::Error] if the notice is ignored
109
+ # @raise [Airbrake::Error] if the +key+ is not recognized
110
+ # @raise [Airbrake::Error] if the root value is not a Hash
111
+ def []=(key, value)
112
+ raise_if_ignored
113
+
114
+ unless WRITABLE_KEYS.include?(key)
115
+ raise Airbrake::Error,
116
+ ":#{key} is not recognized among #{WRITABLE_KEYS}"
117
+ end
118
+
119
+ unless value.respond_to?(:to_hash)
120
+ raise Airbrake::Error, "Got #{value.class} value, wanted a Hash"
121
+ end
122
+
123
+ @payload[key] = value.to_hash
124
+ end
125
+
126
+ private
127
+
128
+ def context
129
+ {
130
+ version: @config.app_version,
131
+ versions: @config.versions,
132
+ # We ensure that root_directory is always a String, so it can always be
133
+ # converted to JSON in a predictable manner (when it's a Pathname and in
134
+ # Rails environment, it converts to unexpected JSON).
135
+ rootDirectory: @config.root_directory.to_s,
136
+ environment: @config.environment,
137
+
138
+ # Make sure we always send hostname.
139
+ hostname: HOSTNAME,
140
+
141
+ severity: DEFAULT_SEVERITY
142
+ }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
143
+ end
144
+
145
+ def truncate
146
+ TRUNCATABLE_KEYS.each do |key|
147
+ @payload[key] = @truncator.truncate(@payload[key])
148
+ end
149
+
150
+ new_max_size = @truncator.reduce_max_size
151
+ if new_max_size == 0
152
+ logger.error(
153
+ "#{LOG_LABEL} truncation failed. File an issue at " \
154
+ "https://github.com/airbrake/airbrake-ruby " \
155
+ "and attach the following payload: #{@payload}"
156
+ )
157
+ end
158
+
159
+ new_max_size
160
+ end
161
+ end
162
+ end