celerbrake-ruby 0.1.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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/lib/celerbrake-ruby/async_sender.rb +57 -0
  3. data/lib/celerbrake-ruby/backlog.rb +123 -0
  4. data/lib/celerbrake-ruby/backtrace.rb +197 -0
  5. data/lib/celerbrake-ruby/benchmark.rb +39 -0
  6. data/lib/celerbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/celerbrake-ruby/config/processor.rb +77 -0
  8. data/lib/celerbrake-ruby/config/validator.rb +97 -0
  9. data/lib/celerbrake-ruby/config.rb +291 -0
  10. data/lib/celerbrake-ruby/context.rb +51 -0
  11. data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
  12. data/lib/celerbrake-ruby/file_cache.rb +54 -0
  13. data/lib/celerbrake-ruby/filter_chain.rb +112 -0
  14. data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
  15. data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
  16. data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  17. data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
  18. data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  19. data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
  20. data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
  21. data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
  22. data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
  23. data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
  24. data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
  25. data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
  26. data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
  27. data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
  28. data/lib/celerbrake-ruby/grouppable.rb +12 -0
  29. data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
  30. data/lib/celerbrake-ruby/ignorable.rb +43 -0
  31. data/lib/celerbrake-ruby/inspectable.rb +39 -0
  32. data/lib/celerbrake-ruby/loggable.rb +34 -0
  33. data/lib/celerbrake-ruby/mergeable.rb +12 -0
  34. data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
  35. data/lib/celerbrake-ruby/nested_exception.rb +59 -0
  36. data/lib/celerbrake-ruby/notice.rb +157 -0
  37. data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
  38. data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
  39. data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
  40. data/lib/celerbrake-ruby/promise.rb +110 -0
  41. data/lib/celerbrake-ruby/query.rb +59 -0
  42. data/lib/celerbrake-ruby/queue.rb +65 -0
  43. data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
  44. data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
  45. data/lib/celerbrake-ruby/remote_settings.rb +128 -0
  46. data/lib/celerbrake-ruby/request.rb +48 -0
  47. data/lib/celerbrake-ruby/response.rb +125 -0
  48. data/lib/celerbrake-ruby/stashable.rb +15 -0
  49. data/lib/celerbrake-ruby/stat.rb +66 -0
  50. data/lib/celerbrake-ruby/sync_sender.rb +145 -0
  51. data/lib/celerbrake-ruby/tdigest.rb +379 -0
  52. data/lib/celerbrake-ruby/thread_pool.rb +139 -0
  53. data/lib/celerbrake-ruby/time_truncate.rb +17 -0
  54. data/lib/celerbrake-ruby/timed_trace.rb +56 -0
  55. data/lib/celerbrake-ruby/truncator.rb +121 -0
  56. data/lib/celerbrake-ruby/version.rb +16 -0
  57. data/lib/celerbrake-ruby.rb +592 -0
  58. metadata +251 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 05635c625ddcfffc4ee0d02816587d8ae16eb9114fbb856c32c27c3eb3f934b6
4
+ data.tar.gz: 592b95064454a6c385583aa5ba60baceaf27731912f77c6e9d7310c7e3afc8c5
5
+ SHA512:
6
+ metadata.gz: d8f415e4ae3c11ed67984395dfe11371f3660d17e160e472e36cbad94718f6a7f76579d976bd7e63eb87924bedf9333daa4d60b5ac10f515f22e666ff2f26cb5
7
+ data.tar.gz: '05581f19888a99ebbd515213165caeb25787f5b373c6ea4537a284b8ba09802d32c177a971e39a7ce92a4238d32ac958cce00a3527ffe87a998d4fa3511ed32c'
@@ -0,0 +1,57 @@
1
+ module Celerbrake
2
+ # Responsible for sending notices to Celerbrake asynchronously.
3
+ #
4
+ # @see SyncSender
5
+ # @api private
6
+ # @since v1.0.0
7
+ class AsyncSender
8
+ def initialize(method = :post, name = 'async-sender')
9
+ @config = Celerbrake::Config.instance
10
+ @sync_sender = SyncSender.new(method)
11
+ @name = name
12
+ end
13
+
14
+ # Asynchronously sends a notice to Celerbrake.
15
+ #
16
+ # @param [Celerbrake::Notice] data Whatever needs to be sent
17
+ # @param [Celerbrake::Promise] promise
18
+ # @param [URI] endpoint Where to send +data+
19
+ # @return [Celerbrake::Promise]
20
+ def send(data, promise, endpoint = @config.error_endpoint)
21
+ unless thread_pool << [data, promise, endpoint]
22
+ return promise.reject(
23
+ "AsyncSender has reached its capacity of #{@config.queue_size}",
24
+ )
25
+ end
26
+
27
+ promise
28
+ end
29
+
30
+ # @return [void]
31
+ def close
32
+ @sync_sender.close
33
+ thread_pool.close
34
+ end
35
+
36
+ # @return [Boolean]
37
+ def closed?
38
+ thread_pool.closed?
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def has_workers?
43
+ thread_pool.has_workers?
44
+ end
45
+
46
+ private
47
+
48
+ def thread_pool
49
+ @thread_pool ||= ThreadPool.new(
50
+ name: @name,
51
+ worker_size: @config.workers,
52
+ queue_size: @config.queue_size,
53
+ block: proc { |args| @sync_sender.send(*args) },
54
+ )
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,123 @@
1
+ module Celerbrake
2
+ # Backlog accepts notices and APM events and synchronously sends them in the
3
+ # background at regular intervals. The backlog is a queue of data that failed
4
+ # to be sent due to some error. In a nutshell, it's a retry mechanism.
5
+ #
6
+ # @api private
7
+ # @since v6.2.0
8
+ class Backlog
9
+ include Loggable
10
+
11
+ # @return [Integer] how many records to keep in the backlog
12
+ BACKLOG_SIZE = 100
13
+
14
+ # @return [Integer] flush period in seconds
15
+ TWO_MINUTES = 60 * 2
16
+
17
+ def initialize(sync_sender, flush_period = TWO_MINUTES)
18
+ @sync_sender = sync_sender
19
+ @flush_period = flush_period
20
+ @queue = SizedQueue.new(BACKLOG_SIZE).extend(MonitorMixin)
21
+ @has_backlog_data = @queue.new_cond
22
+ @schedule_flush = nil
23
+
24
+ @seen = Set.new
25
+ end
26
+
27
+ # Appends data to the backlog. Once appended, the flush schedule will
28
+ # start. Chainable.
29
+ #
30
+ # @example
31
+ # backlog << [{ 'data' => 1 }, 'https://celerbrake.com/api']
32
+ #
33
+ # @param [Array<#to_json, String>] data An array of two elements, where the
34
+ # first element is the data we are sending and the second element is the
35
+ # URL that we are sending to
36
+ # @return [self]
37
+ def <<(data)
38
+ @queue.synchronize do
39
+ return self if @seen.include?(data)
40
+
41
+ @seen << data
42
+
43
+ begin
44
+ @queue.push(data, true)
45
+ rescue ThreadError
46
+ logger.error("#{LOG_LABEL} Celerbrake::Backlog full")
47
+ return self
48
+ end
49
+
50
+ @has_backlog_data.signal
51
+ schedule_flush
52
+
53
+ self
54
+ end
55
+ end
56
+
57
+ # Closes all the resources that this sender has allocated.
58
+ #
59
+ # @return [void]
60
+ # @since v6.2.0
61
+ def close
62
+ @queue.synchronize do
63
+ if @schedule_flush
64
+ @schedule_flush.kill
65
+ logger.debug("#{LOG_LABEL} Celerbrake::Backlog closed")
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def schedule_flush
73
+ @schedule_flush ||= Thread.new do
74
+ loop do
75
+ @queue.synchronize do
76
+ wait
77
+ next if @queue.empty?
78
+
79
+ flush
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def wait
86
+ @has_backlog_data.wait(@flush_period) while time_elapsed < @flush_period
87
+ @last_flush = nil
88
+ end
89
+
90
+ def time_elapsed
91
+ MonotonicTime.time_in_s - last_flush
92
+ end
93
+
94
+ def last_flush
95
+ @last_flush ||= MonotonicTime.time_in_s
96
+ end
97
+
98
+ def flush
99
+ unless @queue.empty?
100
+ logger.debug(
101
+ "#{LOG_LABEL} Celerbrake::Backlog flushing #{@queue.size} messages",
102
+ )
103
+ end
104
+
105
+ failed = 0
106
+
107
+ until @queue.empty?
108
+ data, endpoint = @queue.pop
109
+ promise = Celerbrake::Promise.new
110
+ @sync_sender.send(data, promise, endpoint)
111
+ failed += 1 if promise.rejected?
112
+ end
113
+
114
+ if failed > 0
115
+ logger.debug(
116
+ "#{LOG_LABEL} Celerbrake::Backlog #{failed} messages were not flushed",
117
+ )
118
+ end
119
+
120
+ @seen.clear
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,197 @@
1
+ module Celerbrake
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 Celerbrake 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.freeze
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.freeze
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.freeze
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.freeze
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.freeze
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
+
99
+ parse_backtrace(exception)
100
+ end
101
+
102
+ # Checks whether the given exception was generated by JRuby's VM.
103
+ #
104
+ # @param [Exception] exception
105
+ # @return [Boolean]
106
+ def self.java_exception?(exception)
107
+ if defined?(Java::JavaLang::Throwable) &&
108
+ exception.is_a?(Java::JavaLang::Throwable)
109
+ return true
110
+ end
111
+
112
+ return false unless exception.respond_to?(:backtrace)
113
+
114
+ (Patterns::JAVA =~ exception.backtrace.first) != nil
115
+ end
116
+
117
+ class << self
118
+ include Loggable
119
+
120
+ private
121
+
122
+ def best_regexp_for(exception)
123
+ if java_exception?(exception)
124
+ Patterns::JAVA
125
+ elsif oci_exception?(exception)
126
+ Patterns::OCI
127
+ elsif execjs_exception?(exception)
128
+ Patterns::EXECJS
129
+ else
130
+ Patterns::RUBY
131
+ end
132
+ end
133
+
134
+ def oci_exception?(exception)
135
+ defined?(OCIError) && exception.is_a?(OCIError)
136
+ end
137
+
138
+ def execjs_exception?(exception)
139
+ return false unless defined?(ExecJS::RuntimeError)
140
+ return true if exception.is_a?(ExecJS::RuntimeError)
141
+ return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
142
+
143
+ false
144
+ end
145
+
146
+ def stack_frame(regexp, stackframe)
147
+ if (match = match_frame(regexp, stackframe))
148
+ return {
149
+ file: match[:file],
150
+ line: (Integer(match[:line]) if match[:line]),
151
+ function: match[:function],
152
+ }
153
+ end
154
+
155
+ logger.error(
156
+ "can't parse '#{stackframe}' (please file an issue so we can fix " \
157
+ "it: https://github.com/celerbrake/celerbrake-ruby/issues/new)",
158
+ )
159
+ { file: nil, line: nil, function: stackframe }
160
+ end
161
+
162
+ def match_frame(regexp, stackframe)
163
+ match = regexp.match(stackframe)
164
+ return match if match
165
+
166
+ Patterns::GENERIC.match(stackframe)
167
+ end
168
+
169
+ def parse_backtrace(exception)
170
+ regexp = best_regexp_for(exception)
171
+ root_directory = Celerbrake::Config.instance.root_directory.to_s
172
+
173
+ exception.backtrace.map.with_index do |stackframe, i|
174
+ frame = stack_frame(regexp, stackframe)
175
+ next(frame) if !Celerbrake::Config.instance.code_hunks || frame[:file].nil?
176
+
177
+ if !root_directory.empty?
178
+ populate_code(frame) if frame_in_root?(frame, root_directory)
179
+ elsif i < CODE_FRAME_LIMIT
180
+ populate_code(frame)
181
+ end
182
+
183
+ frame
184
+ end
185
+ end
186
+
187
+ def populate_code(frame)
188
+ code = Celerbrake::CodeHunk.new.get(frame[:file], frame[:line])
189
+ frame[:code] = code if code
190
+ end
191
+
192
+ def frame_in_root?(frame, root_directory)
193
+ frame[:file].start_with?(root_directory) && frame[:file] !~ %r{vendor/bundle}
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,39 @@
1
+ module Celerbrake
2
+ # Benchmark benchmarks Ruby code.
3
+ #
4
+ # @since v4.2.4
5
+ # @api public
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 Celerbrake
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
+ Celerbrake::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,77 @@
1
+ module Celerbrake
2
+ class Config
3
+ # Processor is a helper class, which is responsible for setting default
4
+ # config values, default notifier filters and remote configuration changes.
5
+ #
6
+ # @since v5.0.0
7
+ # @api private
8
+ class Processor
9
+ # @param [Celerbrake::Config] config
10
+ # @return [Celerbrake::Config::Processor]
11
+ def self.process(config)
12
+ new(config).process
13
+ end
14
+
15
+ # @param [Celerbrake::Config] config
16
+ def initialize(config)
17
+ @config = config
18
+ @blocklist_keys = @config.blocklist_keys
19
+ @allowlist_keys = @config.allowlist_keys
20
+ @project_id = @config.project_id
21
+ @poll_callback = Celerbrake::RemoteSettings::Callback.new(config)
22
+ end
23
+
24
+ # @param [Celerbrake::NoticeNotifier] notifier
25
+ # @return [void]
26
+ def process_blocklist(notifier)
27
+ return if @blocklist_keys.none?
28
+
29
+ blocklist = Celerbrake::Filters::KeysBlocklist.new(@blocklist_keys)
30
+ notifier.add_filter(blocklist)
31
+ end
32
+
33
+ # @param [Celerbrake::NoticeNotifier] notifier
34
+ # @return [void]
35
+ def process_allowlist(notifier)
36
+ return if @allowlist_keys.none?
37
+
38
+ allowlist = Celerbrake::Filters::KeysAllowlist.new(@allowlist_keys)
39
+ notifier.add_filter(allowlist)
40
+ end
41
+
42
+ # @return [Celerbrake::RemoteSettings]
43
+ def process_remote_configuration
44
+ return unless @config.remote_config
45
+ return unless @project_id
46
+
47
+ # Never poll remote configuration in the test environment.
48
+ return if @config.environment == 'test'
49
+
50
+ # If the current environment is ignored, don't try to poll remote
51
+ # configuration.
52
+ return if @config.ignore_environments.include?(@config.environment)
53
+
54
+ RemoteSettings.poll(@project_id, @config.remote_config_host) do |data|
55
+ @poll_callback.call(data)
56
+ end
57
+ end
58
+
59
+ # @param [Celerbrake::NoticeNotifier] notifier
60
+ # @return [void]
61
+ def add_filters(notifier)
62
+ return unless @config.root_directory
63
+
64
+ [
65
+ Celerbrake::Filters::RootDirectoryFilter,
66
+ Celerbrake::Filters::GitRevisionFilter,
67
+ Celerbrake::Filters::GitRepositoryFilter,
68
+ Celerbrake::Filters::GitLastCheckoutFilter,
69
+ ].each do |filter|
70
+ next if notifier.has_filter?(filter)
71
+
72
+ notifier.add_filter(filter.new(@config.root_directory))
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,97 @@
1
+ module Celerbrake
2
+ class Config
3
+ # Validator validates values of {Celerbrake::Config} options. A valid config
4
+ # is a config that guarantees that data can be sent to Celerbrake given its
5
+ # configuration.
6
+ #
7
+ # @api private
8
+ # @since v1.5.0
9
+ class Validator
10
+ # @return [Array<Class>] the list of allowed types to configure the
11
+ # environment option
12
+ VALID_ENV_TYPES = [NilClass, String, Symbol].freeze
13
+
14
+ class << self
15
+ # @param [Celerbrake::Config] config
16
+ # @since v4.1.0
17
+ def validate(config)
18
+ promise = Celerbrake::Promise.new
19
+
20
+ unless valid_project_id?(config)
21
+ return promise.reject(':project_id is required')
22
+ end
23
+
24
+ unless valid_project_key?(config)
25
+ return promise.reject(':project_key is required')
26
+ end
27
+
28
+ unless valid_environment?(config)
29
+ return promise.reject(
30
+ "the 'environment' option must be configured " \
31
+ "with a Symbol (or String), but '#{config.environment.class}' was " \
32
+ "provided: #{config.environment}",
33
+ )
34
+ end
35
+
36
+ promise.resolve(:ok)
37
+ end
38
+
39
+ # Whether the given +config+ allows sending data to Celerbrake. It doesn't
40
+ # matter if it's valid or invalid.
41
+ #
42
+ # @param [Celerbrake::Config] config
43
+ # @since v4.1.0
44
+ def check_notify_ability(config)
45
+ promise = Celerbrake::Promise.new
46
+
47
+ unless config.error_notifications
48
+ return promise.reject('error notifications are disabled')
49
+ end
50
+
51
+ if ignored_environment?(config)
52
+ return promise.reject(
53
+ "current environment '#{config.environment}' is ignored",
54
+ )
55
+ end
56
+
57
+ promise.resolve(:ok)
58
+ end
59
+
60
+ private
61
+
62
+ def valid_project_id?(config)
63
+ return true if config.project_id.to_i > 0
64
+
65
+ false
66
+ end
67
+
68
+ def valid_project_key?(config)
69
+ return false unless config.project_key.is_a?(String)
70
+ return false if config.project_key.empty?
71
+
72
+ true
73
+ end
74
+
75
+ def valid_environment?(config)
76
+ VALID_ENV_TYPES.any? { |type| config.environment.is_a?(type) }
77
+ end
78
+
79
+ def ignored_environment?(config)
80
+ if config.ignore_environments.any? && config.environment.nil?
81
+ config.logger.warn(
82
+ "#{LOG_LABEL} the 'environment' option is not set, " \
83
+ "'ignore_environments' has no effect",
84
+ )
85
+ end
86
+
87
+ return false if config.ignore_environments.none? || !config.environment
88
+
89
+ env = config.environment.to_s
90
+ config.ignore_environments.any? do |pattern|
91
+ pattern.is_a?(Regexp) ? env.match(pattern) : env == pattern.to_s
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end