airbrake-ruby 4.7.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 (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,80 @@
1
+ module Airbrake
2
+ # Responsible for sending notices to Airbrake asynchronously.
3
+ #
4
+ # @see SyncSender
5
+ # @api private
6
+ # @since v1.0.0
7
+ class AsyncSender
8
+ include Loggable
9
+
10
+ # @return [String]
11
+ WILL_NOT_DELIVER_MSG =
12
+ "%<log_label>s AsyncSender has reached its capacity of %<capacity>s " \
13
+ "and the following notice will not be delivered " \
14
+ "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
15
+
16
+ def initialize(method = :post)
17
+ @config = Airbrake::Config.instance
18
+ @method = method
19
+ end
20
+
21
+ # Asynchronously sends a notice to Airbrake.
22
+ #
23
+ # @param [Airbrake::Notice] notice A notice that was generated by the
24
+ # library
25
+ # @return [Airbrake::Promise]
26
+ def send(notice, promise, endpoint = @config.endpoint)
27
+ unless thread_pool << [notice, promise, endpoint]
28
+ return will_not_deliver(notice, promise)
29
+ end
30
+
31
+ promise
32
+ end
33
+
34
+ # @return [void]
35
+ def close
36
+ thread_pool.close
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def closed?
41
+ thread_pool.closed?
42
+ end
43
+
44
+ # @return [Boolean]
45
+ def has_workers?
46
+ thread_pool.has_workers?
47
+ end
48
+
49
+ private
50
+
51
+ def thread_pool
52
+ @thread_pool ||= begin
53
+ sender = SyncSender.new(@method)
54
+ ThreadPool.new(
55
+ worker_size: @config.workers,
56
+ queue_size: @config.queue_size,
57
+ block: proc { |args| sender.send(*args) }
58
+ )
59
+ end
60
+ end
61
+
62
+ def will_not_deliver(notice, promise)
63
+ error = notice[:errors].first
64
+
65
+ logger.error(
66
+ format(
67
+ WILL_NOT_DELIVER_MSG,
68
+ log_label: LOG_LABEL,
69
+ capacity: @config.queue_size,
70
+ type: error[:type],
71
+ message: error[:message],
72
+ backtrace: error[:backtrace].map do |line|
73
+ "#{line[:file]}:#{line[:line]} in `#{line[:function]}'"
74
+ end.join("\n")
75
+ )
76
+ )
77
+ promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
78
+ end
79
+ end
80
+ 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