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,142 @@
1
+ module Airbrake
2
+ # Responsible for sending notices to Airbrake asynchronously. The class
3
+ # supports an unlimited number of worker threads and an unlimited queue size
4
+ # (both values are configurable).
5
+ #
6
+ # @see SyncSender
7
+ # @api private
8
+ # @since v1.0.0
9
+ class AsyncSender
10
+ include Loggable
11
+
12
+ # @return [String]
13
+ WILL_NOT_DELIVER_MSG =
14
+ "%<log_label>s AsyncSender has reached its capacity of %<capacity>s " \
15
+ "and the following notice will not be delivered " \
16
+ "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
17
+
18
+ # @return [ThreadGroup] the list of workers
19
+ # @note This is exposed for eaiser unit testing
20
+ # @since v4.0.0
21
+ attr_reader :workers
22
+
23
+ # @return [Array<[Airbrake::Notice,Airbrake::Promise]>] the list of unsent
24
+ # payload
25
+ # @note This is exposed for eaiser unit testing
26
+ # @since v4.0.0
27
+ attr_reader :unsent
28
+
29
+ def initialize
30
+ @config = Airbrake::Config.instance
31
+ @unsent = SizedQueue.new(Airbrake::Config.instance.queue_size)
32
+ @sender = SyncSender.new
33
+ @closed = false
34
+ @workers = ThreadGroup.new
35
+ @mutex = Mutex.new
36
+ @pid = nil
37
+ end
38
+
39
+ # Asynchronously sends a notice to Airbrake.
40
+ #
41
+ # @param [Airbrake::Notice] notice A notice that was generated by the
42
+ # library
43
+ # @return [Airbrake::Promise]
44
+ def send(notice, promise)
45
+ return will_not_deliver(notice, promise) if @unsent.size >= @unsent.max
46
+
47
+ @unsent << [notice, promise]
48
+ promise
49
+ end
50
+
51
+ # Closes the instance making it a no-op (it shut downs all worker
52
+ # threads). Before closing, waits on all unsent notices to be sent.
53
+ #
54
+ # @return [void]
55
+ # @raise [Airbrake::Error] when invoked more than one time
56
+ def close
57
+ threads = @mutex.synchronize do
58
+ raise Airbrake::Error, 'attempted to close already closed sender' if closed?
59
+
60
+ unless @unsent.empty?
61
+ msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
62
+ logger.debug(msg + ' (Ctrl-C to abort)')
63
+ end
64
+
65
+ @config.workers.times { @unsent << [:stop, Airbrake::Promise.new] }
66
+ @closed = true
67
+ @workers.list.dup
68
+ end
69
+
70
+ threads.each(&:join)
71
+ logger.debug("#{LOG_LABEL} closed")
72
+ end
73
+
74
+ # Checks whether the sender is closed and thus usable.
75
+ # @return [Boolean]
76
+ def closed?
77
+ @closed
78
+ end
79
+
80
+ # Checks if an active sender has any workers. A sender doesn't have any
81
+ # workers only in two cases: when it was closed or when all workers
82
+ # crashed. An *active* sender doesn't have any workers only when something
83
+ # went wrong.
84
+ #
85
+ # Workers are expected to crash when you +fork+ the process the workers are
86
+ # living in. In this case we detect a +fork+ and try to revive them here.
87
+ #
88
+ # Another possible scenario that crashes workers is when you close the
89
+ # instance on +at_exit+, but some other +at_exit+ hook prevents the process
90
+ # from exiting.
91
+ #
92
+ # @return [Boolean] true if an instance wasn't closed, but has no workers
93
+ # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
94
+ def has_workers?
95
+ @mutex.synchronize do
96
+ return false if @closed
97
+
98
+ if @pid != Process.pid && @workers.list.empty?
99
+ @pid = Process.pid
100
+ spawn_workers
101
+ end
102
+
103
+ !@closed && @workers.list.any?
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def spawn_workers
110
+ @workers = ThreadGroup.new
111
+ @config.workers.times { @workers.add(spawn_worker) }
112
+ @workers.enclose
113
+ end
114
+
115
+ def spawn_worker
116
+ Thread.new do
117
+ while (message = @unsent.pop)
118
+ break if message.first == :stop
119
+ @sender.send(*message)
120
+ end
121
+ end
122
+ end
123
+
124
+ def will_not_deliver(notice, promise)
125
+ error = notice[:errors].first
126
+
127
+ logger.error(
128
+ format(
129
+ WILL_NOT_DELIVER_MSG,
130
+ log_label: LOG_LABEL,
131
+ capacity: @unsent.max,
132
+ type: error[:type],
133
+ message: error[:message],
134
+ backtrace: error[:backtrace].map do |line|
135
+ "#{line[:file]}:#{line[:line]} in `#{line[:function]}'"
136
+ end.join("\n")
137
+ )
138
+ )
139
+ promise.reject("AsyncSender has reached its capacity of #{@unsent.max}")
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,196 @@
1
+ module Airbrake
2
+ # Represents a cross-Ruby backtrace from exceptions (including JRuby Java
3
+ # exceptions). Provides information about stack frames (such as line number,
4
+ # file and method) in convenient for Airbrake format.
5
+ #
6
+ # @example
7
+ # begin
8
+ # raise 'Oops!'
9
+ # rescue
10
+ # Backtrace.parse($!)
11
+ # end
12
+ #
13
+ # @api private
14
+ # @since v1.0.0
15
+ module Backtrace
16
+ module Patterns
17
+ # @return [Regexp] the pattern that matches standard Ruby stack frames,
18
+ # such as ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
19
+ RUBY = %r{\A
20
+ (?<file>.+) # Matches './spec/notice_spec.rb'
21
+ :
22
+ (?<line>\d+) # Matches '43'
23
+ :in\s
24
+ `(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
25
+ \z}x
26
+
27
+ # @return [Regexp] the pattern that matches JRuby Java stack frames, such
28
+ # as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
29
+ JAVA = %r{\A
30
+ (?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret'
31
+ \(
32
+ (?<file>
33
+ (?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb'
34
+ |
35
+ (?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...'
36
+ |
37
+ [^:]+ # Matches 'NewlineNode.java'
38
+ )
39
+ :?
40
+ (?<line>\d+)? # Matches '105'
41
+ \)
42
+ \z}x
43
+
44
+ # @return [Regexp] the pattern that tries to assume what a generic stack
45
+ # frame might look like, when exception's backtrace is set manually.
46
+ GENERIC = %r{\A
47
+ (?:from\s)?
48
+ (?<file>.+) # Matches '/foo/bar/baz.ext'
49
+ :
50
+ (?<line>\d+)? # Matches '43' or nothing
51
+ (?:
52
+ in\s`(?<function>.+)' # Matches "in `func'"
53
+ |
54
+ :in\s(?<function>.+) # Matches ":in func"
55
+ )? # ... or nothing
56
+ \z}x
57
+
58
+ # @return [Regexp] the pattern that matches exceptions from PL/SQL such as
59
+ # ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
60
+ # @note This is raised by https://github.com/kubo/ruby-oci8
61
+ OCI = /\A
62
+ (?:
63
+ ORA-\d{5}
64
+ :\sat\s
65
+ (?:"(?<function>.+)",\s)?
66
+ line\s(?<line>\d+)
67
+ |
68
+ #{GENERIC}
69
+ )
70
+ \z/x
71
+
72
+ # @return [Regexp] the pattern that matches CoffeeScript backtraces
73
+ # usually coming from Rails & ExecJS
74
+ EXECJS = /\A
75
+ (?:
76
+ # Matches 'compile ((execjs):6692:19)'
77
+ (?<function>.+)\s\((?<file>.+):(?<line>\d+):\d+\)
78
+ |
79
+ # Matches 'bootstrap_node.js:467:3'
80
+ (?<file>.+):(?<line>\d+):\d+(?<function>)
81
+ |
82
+ # Matches the Ruby part of the backtrace
83
+ #{RUBY}
84
+ )
85
+ \z/x
86
+ end
87
+
88
+ # @return [Integer] how many first frames should include code hunks
89
+ CODE_FRAME_LIMIT = 10
90
+
91
+ # Parses an exception's backtrace.
92
+ #
93
+ # @param [Exception] exception The exception, which contains a backtrace to
94
+ # parse
95
+ # @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
96
+ def self.parse(exception)
97
+ return [] if exception.backtrace.nil? || exception.backtrace.none?
98
+ parse_backtrace(exception)
99
+ end
100
+
101
+ # Checks whether the given exception was generated by JRuby's VM.
102
+ #
103
+ # @param [Exception] exception
104
+ # @return [Boolean]
105
+ def self.java_exception?(exception)
106
+ if defined?(Java::JavaLang::Throwable) &&
107
+ exception.is_a?(Java::JavaLang::Throwable)
108
+ return true
109
+ end
110
+
111
+ return false unless exception.respond_to?(:backtrace)
112
+
113
+ (Patterns::JAVA =~ exception.backtrace.first) != nil
114
+ end
115
+
116
+ class << self
117
+ include Loggable
118
+
119
+ private
120
+
121
+ def best_regexp_for(exception)
122
+ if java_exception?(exception)
123
+ Patterns::JAVA
124
+ elsif oci_exception?(exception)
125
+ Patterns::OCI
126
+ elsif execjs_exception?(exception)
127
+ Patterns::EXECJS
128
+ else
129
+ Patterns::RUBY
130
+ end
131
+ end
132
+
133
+ def oci_exception?(exception)
134
+ defined?(OCIError) && exception.is_a?(OCIError)
135
+ end
136
+
137
+ def execjs_exception?(exception)
138
+ return false unless defined?(ExecJS::RuntimeError)
139
+ return true if exception.is_a?(ExecJS::RuntimeError)
140
+ return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
141
+
142
+ false
143
+ end
144
+
145
+ def stack_frame(regexp, stackframe)
146
+ if (match = match_frame(regexp, stackframe))
147
+ return {
148
+ file: match[:file],
149
+ line: (Integer(match[:line]) if match[:line]),
150
+ function: match[:function]
151
+ }
152
+ end
153
+
154
+ logger.error(
155
+ "can't parse '#{stackframe}' (please file an issue so we can fix " \
156
+ "it: https://github.com/airbrake/airbrake-ruby/issues/new)"
157
+ )
158
+ { file: nil, line: nil, function: stackframe }
159
+ end
160
+
161
+ def match_frame(regexp, stackframe)
162
+ match = regexp.match(stackframe)
163
+ return match if match
164
+
165
+ Patterns::GENERIC.match(stackframe)
166
+ end
167
+
168
+ def parse_backtrace(exception)
169
+ regexp = best_regexp_for(exception)
170
+ root_directory = Airbrake::Config.instance.root_directory.to_s
171
+
172
+ exception.backtrace.map.with_index do |stackframe, i|
173
+ frame = stack_frame(regexp, stackframe)
174
+ next(frame) if !Airbrake::Config.instance.code_hunks || frame[:file].nil?
175
+
176
+ if !root_directory.empty?
177
+ populate_code(frame) if frame_in_root?(frame, root_directory)
178
+ elsif i < CODE_FRAME_LIMIT
179
+ populate_code(frame)
180
+ end
181
+
182
+ frame
183
+ end
184
+ end
185
+
186
+ def populate_code(frame)
187
+ code = Airbrake::CodeHunk.new.get(frame[:file], frame[:line])
188
+ frame[:code] = code if code
189
+ end
190
+
191
+ def frame_in_root?(frame, root_directory)
192
+ frame[:file].start_with?(root_directory) && frame[:file] !~ %r{vendor/bundle}
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,39 @@
1
+ module Airbrake
2
+ # Benchmark benchmarks Ruby code.
3
+ #
4
+ # @since v4.2.4
5
+ # @api private
6
+ class Benchmark
7
+ # Measures monotonic time for the given operation.
8
+ #
9
+ # @yieldreturn [void]
10
+ def self.measure
11
+ benchmark = new
12
+
13
+ yield
14
+
15
+ benchmark.stop
16
+ benchmark.duration
17
+ end
18
+
19
+ # @return [Float]
20
+ attr_reader :duration
21
+
22
+ # @since v4.3.0
23
+ def initialize
24
+ @start = MonotonicTime.time_in_ms
25
+ @duration = 0.0
26
+ end
27
+
28
+ # Stops the benchmark and stores `duration`.
29
+ #
30
+ # @since v4.3.0
31
+ # @return [Boolean] true for the first invocation, false in all other cases
32
+ def stop
33
+ return false if @duration > 0.0
34
+
35
+ @duration = MonotonicTime.time_in_ms - @start
36
+ true
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ module Airbrake
2
+ # Represents a small hunk of code consisting of a base line and a couple lines
3
+ # around it
4
+ # @api private
5
+ class CodeHunk
6
+ # @return [Integer] the maximum length of a line
7
+ MAX_LINE_LEN = 200
8
+
9
+ # @return [Integer] how many lines should be read around the base line
10
+ NLINES = 2
11
+
12
+ include Loggable
13
+
14
+ # @param [String] file The file to read
15
+ # @param [Integer] line The base line in the file
16
+ # @return [Hash{Integer=>String}, nil] lines of code around the base line
17
+ def get(file, line)
18
+ return unless File.exist?(file)
19
+ return unless line
20
+
21
+ lines = get_lines(file, [line - NLINES, 1].max, line + NLINES) || {}
22
+ return { 1 => '' } if lines.empty?
23
+
24
+ lines
25
+ end
26
+
27
+ private
28
+
29
+ def get_from_cache(file)
30
+ Airbrake::FileCache[file] ||= File.foreach(file)
31
+ rescue StandardError => ex
32
+ logger.error(
33
+ "#{self.class.name}: can't read code hunk for #{file}: #{ex}"
34
+ )
35
+ nil
36
+ end
37
+
38
+ def get_lines(file, start_line, end_line)
39
+ return unless (cached_file = get_from_cache(file))
40
+
41
+ lines = {}
42
+ cached_file.with_index(1) do |l, i|
43
+ next if i < start_line
44
+ break if i > end_line
45
+
46
+ lines[i] = l[0...MAX_LINE_LEN].rstrip
47
+ end
48
+ lines
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,229 @@
1
+ module Airbrake
2
+ # Represents the Airbrake config. A config contains all the options that you
3
+ # can use to configure an Airbrake instance.
4
+ #
5
+ # @api public
6
+ # @since v1.0.0
7
+ class Config
8
+ # @return [Integer] the project identificator. This value *must* be set.
9
+ # @api public
10
+ attr_accessor :project_id
11
+
12
+ # @return [String] the project key. This value *must* be set.
13
+ # @api public
14
+ attr_accessor :project_key
15
+
16
+ # @return [Hash] the proxy parameters such as (:host, :port, :user and
17
+ # :password)
18
+ # @api public
19
+ attr_accessor :proxy
20
+
21
+ # @return [Logger] the default logger used for debug output
22
+ # @api public
23
+ attr_reader :logger
24
+
25
+ # @return [String] the version of the user's application
26
+ # @api public
27
+ attr_accessor :app_version
28
+
29
+ # @return [Hash{String=>String}] arbitrary versions that your app wants to
30
+ # track
31
+ # @api public
32
+ # @since v2.10.0
33
+ attr_accessor :versions
34
+
35
+ # @return [Integer] the max number of notices that can be queued up
36
+ # @api public
37
+ attr_accessor :queue_size
38
+
39
+ # @return [Integer] the number of worker threads that process the notice
40
+ # queue
41
+ # @api public
42
+ attr_accessor :workers
43
+
44
+ # @return [String] the host, which provides the API endpoint to which
45
+ # exceptions should be sent
46
+ # @api public
47
+ attr_accessor :host
48
+
49
+ # @return [String, Pathname] the working directory of your project
50
+ # @api public
51
+ attr_accessor :root_directory
52
+
53
+ # @return [String, Symbol] the environment the application is running in
54
+ # @api public
55
+ attr_accessor :environment
56
+
57
+ # @return [Array<String,Symbol,Regexp>] the array of environments that
58
+ # forbids sending exceptions when the application is running in them.
59
+ # Other possible environments not listed in the array will allow sending
60
+ # occurring exceptions.
61
+ # @api public
62
+ attr_accessor :ignore_environments
63
+
64
+ # @return [Integer] The HTTP timeout in seconds.
65
+ # @api public
66
+ attr_accessor :timeout
67
+
68
+ # @return [Array<String, Symbol, Regexp>] the keys, which should be
69
+ # filtered
70
+ # @api public
71
+ # @since v1.2.0
72
+ attr_accessor :blacklist_keys
73
+
74
+ # @return [Array<String, Symbol, Regexp>] the keys, which shouldn't be
75
+ # filtered
76
+ # @api public
77
+ # @since v1.2.0
78
+ attr_accessor :whitelist_keys
79
+
80
+ # @return [Boolean] true if the library should attach code hunks to each
81
+ # frame in a backtrace, false otherwise
82
+ # @api public
83
+ # @since v2.5.0
84
+ attr_accessor :code_hunks
85
+
86
+ # @return [Boolean] true if the library should send route performance stats
87
+ # to Airbrake, false otherwise
88
+ # @api public
89
+ # @since v3.2.0
90
+ attr_accessor :performance_stats
91
+
92
+ # @return [Integer] how many seconds to wait before sending collected route
93
+ # stats
94
+ # @api private
95
+ # @since v3.2.0
96
+ attr_accessor :performance_stats_flush_period
97
+
98
+ # @return [Boolean] true if the library should send SQL stats to Airbrake,
99
+ # false otherwise
100
+ # @api public
101
+ # @since v4.6.0
102
+ attr_accessor :query_stats
103
+
104
+ class << self
105
+ # @return [Config]
106
+ attr_writer :instance
107
+
108
+ # @return [Config]
109
+ def instance
110
+ @instance ||= new
111
+ end
112
+ end
113
+
114
+ # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
115
+ # config
116
+ def initialize(user_config = {})
117
+ self.proxy = {}
118
+ self.queue_size = 100
119
+ self.workers = 1
120
+ self.code_hunks = true
121
+ self.logger = ::Logger.new(File::NULL)
122
+ self.project_id = user_config[:project_id]
123
+ self.project_key = user_config[:project_key]
124
+ self.host = 'https://api.airbrake.io'
125
+
126
+ self.ignore_environments = []
127
+
128
+ self.timeout = user_config[:timeout]
129
+
130
+ self.blacklist_keys = []
131
+ self.whitelist_keys = []
132
+
133
+ self.root_directory = File.realpath(
134
+ (defined?(Bundler) && Bundler.root) ||
135
+ Dir.pwd
136
+ )
137
+
138
+ self.versions = {}
139
+ self.performance_stats = true
140
+ self.performance_stats_flush_period = 15
141
+ self.query_stats = false
142
+
143
+ merge(user_config)
144
+ end
145
+
146
+ # The full URL to the Airbrake Notice API. Based on the +:host+ option.
147
+ # @return [URI] the endpoint address
148
+ def endpoint
149
+ @endpoint ||=
150
+ begin
151
+ self.host = ('https://' << host) if host !~ %r{\Ahttps?://}
152
+ api = "api/v3/projects/#{project_id}/notices"
153
+ URI.join(host, api)
154
+ end
155
+ end
156
+
157
+ # Sets the logger. Never allows to assign `nil` as the logger.
158
+ # @return [Logger] the logger
159
+ def logger=(logger)
160
+ @logger = logger || @logger
161
+ end
162
+
163
+ # Merges the given +config_hash+ with itself.
164
+ #
165
+ # @example
166
+ # config.merge(host: 'localhost:8080')
167
+ #
168
+ # @return [self] the merged config
169
+ def merge(config_hash)
170
+ config_hash.each_pair { |option, value| set_option(option, value) }
171
+ self
172
+ end
173
+
174
+ # @return [Boolean] true if the config meets the requirements, false
175
+ # otherwise
176
+ def valid?
177
+ validate.resolved?
178
+ end
179
+
180
+ # @return [Promise]
181
+ # @see Validator.validate
182
+ def validate
183
+ Validator.validate(self)
184
+ end
185
+
186
+ # @return [Promise]
187
+ # @see Validator.check_notify_ability
188
+ def check_notify_ability
189
+ Validator.check_notify_ability(self)
190
+ end
191
+
192
+ # @return [Boolean] true if the config ignores current environment, false
193
+ # otherwise
194
+ def ignored_environment?
195
+ check_notify_ability.rejected?
196
+ end
197
+
198
+ # @return [Promise] resolved promise if config is valid & can notify,
199
+ # rejected otherwise
200
+ def check_configuration
201
+ promise = validate
202
+ return promise if promise.rejected?
203
+
204
+ check_notify_ability
205
+ end
206
+
207
+ # @return [Promise] resolved promise if neither of the performance options
208
+ # reject it, false otherwise
209
+ def check_performance_options(resource)
210
+ promise = Airbrake::Promise.new
211
+
212
+ if !performance_stats
213
+ promise.reject("The Performance Stats feature is disabled")
214
+ elsif resource.is_a?(Airbrake::Query) && !query_stats
215
+ promise.reject("The Query Stats feature is disabled")
216
+ else
217
+ promise
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ def set_option(option, value)
224
+ __send__("#{option}=", value)
225
+ rescue NoMethodError
226
+ raise Airbrake::Error, "unknown option '#{option}'"
227
+ end
228
+ end
229
+ end