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
@@ -0,0 +1,93 @@
1
+ module Celerbrake
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/celerbrake/celerbrake-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
+ if (name = th.name)
45
+ thread_info[:name] = name
46
+ end
47
+
48
+ add_thread_info(th, thread_info)
49
+
50
+ notice[:params][:thread] = thread_info
51
+ end
52
+
53
+ private
54
+
55
+ def thread_variables(th)
56
+ th.thread_variables.map.with_object({}) do |var, h|
57
+ next if var.to_s.start_with?(IGNORE_PREFIX)
58
+
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
+
67
+ h[key] = sanitize_value(th[key])
68
+ end
69
+ end
70
+
71
+ def add_thread_info(th, thread_info)
72
+ thread_info[:self] = th.inspect
73
+ thread_info[:group] = th.group.list.map(&:inspect)
74
+ thread_info[:priority] = th.priority
75
+
76
+ thread_info[:safe_level] = th.safe_level if Celerbrake::HAS_SAFE_LEVEL
77
+ end
78
+
79
+ def sanitize_value(value)
80
+ return value if SAFE_CLASSES.any? { |klass| value.is_a?(klass) }
81
+
82
+ case value
83
+ when Array
84
+ value = value.map { |elem| sanitize_value(elem) }
85
+ when Hash
86
+ value.transform_values { |v| sanitize_value(v) }
87
+ else
88
+ value.to_s
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,12 @@
1
+ module Celerbrake
2
+ # Grouppable adds the `#groups` method, so that we don't need to define it in
3
+ # all of performance models every time we add a model without groups.
4
+ #
5
+ # @since v4.9.0
6
+ # @api private
7
+ module Grouppable
8
+ def groups
9
+ {}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ module Celerbrake
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 Celerbrake::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,43 @@
1
+ module Celerbrake
2
+ # Ignorable contains methods that allow the includee to be ignored.
3
+ #
4
+ # @example
5
+ # class A
6
+ # include Celerbrake::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
+ def ignored?
22
+ !!ignored
23
+ end
24
+
25
+ # Ignores an instance. Ignored instances must never reach the Celerbrake
26
+ # dashboard.
27
+ # @return [void]
28
+ # @see #ignored?
29
+ def ignore!
30
+ self.ignored = true
31
+ end
32
+
33
+ private
34
+
35
+ # A method that is meant to be used as a guard.
36
+ # @raise [Celerbrake::Error] when instance is ignored
37
+ def raise_if_ignored
38
+ return unless ignored?
39
+
40
+ raise Celerbrake::Error, "cannot access ignored #{self.class}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module Celerbrake
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 Celerbrake
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).tap { |l| l.level = ::Logger::WARN }
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,12 @@
1
+ module Celerbrake
2
+ # Mergeable adds the `#merge` method, so that we don't need to define it in
3
+ # all of performance models every time we add a model.
4
+ #
5
+ # @since v4.9.0
6
+ # @api private
7
+ module Mergeable
8
+ def merge(_other)
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ module Celerbrake
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
+ # @return [Integer] current monotonic time in seconds
20
+ def time_in_s
21
+ time_in_nanoseconds / (10.0**9)
22
+ end
23
+
24
+ private
25
+
26
+ if defined?(Process::CLOCK_MONOTONIC)
27
+
28
+ def time_in_nanoseconds
29
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
30
+ end
31
+
32
+ elsif RUBY_ENGINE == 'jruby'
33
+
34
+ def time_in_nanoseconds
35
+ java.lang.System.nanoTime
36
+ end
37
+
38
+ else
39
+
40
+ def time_in_nanoseconds
41
+ time = Time.now
42
+ (time.to_i * (10**9)) + time.nsec
43
+ end
44
+
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ module Celerbrake
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
+ # On Ruby 3.1+, the error highlighting gem can produce messages that can
13
+ # span multiple lines. We don't display multiline error messages in the
14
+ # title of the notice in the Celerbrake dashboard. Therefore, we want to strip
15
+ # out the higlighting part so that the errors look consistent. The full
16
+ # message with the exception will be attached to the notice body.
17
+ #
18
+ # @return [String]
19
+ RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n".freeze
20
+
21
+ # @return [Hash] the options for +String#encode+
22
+ ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
23
+
24
+ def initialize(exception)
25
+ @exception = exception
26
+ end
27
+
28
+ def as_json
29
+ unwind_exceptions.map do |exception|
30
+ { type: exception.class.name,
31
+ message: message(exception),
32
+ backtrace: Backtrace.parse(exception) }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def unwind_exceptions
39
+ exception_list = []
40
+ exception = @exception
41
+
42
+ while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
43
+ exception_list << exception
44
+ exception = (exception.cause if exception.respond_to?(:cause))
45
+ end
46
+
47
+ exception_list
48
+ end
49
+
50
+ def message(exception)
51
+ return unless (msg = exception.message)
52
+
53
+ msg
54
+ .encode(Encoding::UTF_8, **ENCODING_OPTIONS)
55
+ .split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
56
+ .first
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,157 @@
1
+ module Celerbrake
2
+ # Represents a chunk of information that is meant to be either sent to
3
+ # Celerbrake or ignored completely.
4
+ #
5
+ # @since v1.0.0
6
+ class Notice
7
+ # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
8
+ # Context tab in the dashboard
9
+ CONTEXT = {
10
+ os: RUBY_PLATFORM,
11
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
12
+ notifier: Celerbrake::NOTIFIER_INFO,
13
+ }.freeze
14
+
15
+ # @return [Integer] the maxium size of the JSON payload in bytes
16
+ MAX_NOTICE_SIZE = 64000
17
+
18
+ # @return [Integer] the maximum size of hashes, arrays and strings in the
19
+ # notice.
20
+ PAYLOAD_MAX_SIZE = 10000
21
+
22
+ # @return [Array<StandardError>] the list of possible exceptions that might
23
+ # be raised when an object is converted to JSON
24
+ JSON_EXCEPTIONS = [
25
+ IOError,
26
+ NotImplementedError,
27
+ JSON::GeneratorError,
28
+ Encoding::UndefinedConversionError,
29
+ ].freeze
30
+
31
+ # @return [Array<Symbol>] the list of keys that can be be overwritten with
32
+ # {Celerbrake::Notice#[]=}
33
+ WRITABLE_KEYS = %i[notifier context environment session params].freeze
34
+
35
+ # @return [Array<Symbol>] parts of a Notice's payload that can be modified
36
+ # by the truncator
37
+ TRUNCATABLE_KEYS = %i[errors environment session params].freeze
38
+
39
+ # @return [String] the name of the host machine
40
+ HOSTNAME = Socket.gethostname.freeze
41
+
42
+ # @return [String]
43
+ DEFAULT_SEVERITY = 'error'.freeze
44
+
45
+ include Ignorable
46
+ include Loggable
47
+ include Stashable
48
+
49
+ # @api private
50
+ def initialize(exception, params = {})
51
+ @config = Celerbrake::Config.instance
52
+ @truncator = Celerbrake::Truncator.new(PAYLOAD_MAX_SIZE)
53
+
54
+ @payload = {
55
+ errors: NestedException.new(exception).as_json,
56
+ context: context(exception),
57
+ environment: {
58
+ program_name: $PROGRAM_NAME,
59
+ },
60
+ session: {},
61
+ params: params,
62
+ }
63
+
64
+ stash[:exception] = exception
65
+ end
66
+
67
+ # Converts the notice to JSON. Calls +to_json+ on each object inside
68
+ # notice's payload. Truncates notices, JSON representation of which is
69
+ # bigger than {MAX_NOTICE_SIZE}.
70
+ #
71
+ # @return [Hash{String=>String}, nil]
72
+ # @api private
73
+ def to_json(*_args)
74
+ loop do
75
+ begin
76
+ json = @payload.to_json
77
+ rescue *JSON_EXCEPTIONS => ex
78
+ logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
79
+ else
80
+ return json if json && json.bytesize <= MAX_NOTICE_SIZE
81
+ end
82
+
83
+ break if truncate == 0
84
+ end
85
+ end
86
+
87
+ # Reads a value from notice's payload.
88
+ #
89
+ # @return [Object]
90
+ # @raise [Celerbrake::Error] if the notice is ignored
91
+ def [](key)
92
+ raise_if_ignored
93
+ @payload[key]
94
+ end
95
+
96
+ # Writes a value to the payload hash. Restricts unrecognized writes.
97
+ #
98
+ # @example
99
+ # notice[:params][:my_param] = 'foobar'
100
+ #
101
+ # @return [void]
102
+ # @raise [Celerbrake::Error] if the notice is ignored
103
+ # @raise [Celerbrake::Error] if the +key+ is not recognized
104
+ # @raise [Celerbrake::Error] if the root value is not a Hash
105
+ def []=(key, value)
106
+ raise_if_ignored
107
+
108
+ unless WRITABLE_KEYS.include?(key)
109
+ raise Celerbrake::Error,
110
+ ":#{key} is not recognized among #{WRITABLE_KEYS}"
111
+ end
112
+
113
+ unless value.respond_to?(:to_hash)
114
+ raise Celerbrake::Error, "Got #{value.class} value, wanted a Hash"
115
+ end
116
+
117
+ @payload[key] = value.to_hash
118
+ end
119
+
120
+ private
121
+
122
+ def context(exception)
123
+ {
124
+ version: @config.app_version,
125
+ versions: @config.versions,
126
+ # We ensure that root_directory is always a String, so it can always be
127
+ # converted to JSON in a predictable manner (when it's a Pathname and in
128
+ # Rails environment, it converts to unexpected JSON).
129
+ rootDirectory: @config.root_directory.to_s,
130
+ environment: @config.environment,
131
+
132
+ # Make sure we always send hostname.
133
+ hostname: HOSTNAME,
134
+
135
+ severity: DEFAULT_SEVERITY,
136
+ error_message: @truncator.truncate(exception.message),
137
+ }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
138
+ end
139
+
140
+ def truncate
141
+ TRUNCATABLE_KEYS.each do |key|
142
+ @payload[key] = @truncator.truncate(@payload[key])
143
+ end
144
+
145
+ new_max_size = @truncator.reduce_max_size
146
+ if new_max_size == 0
147
+ logger.error(
148
+ "#{LOG_LABEL} truncation failed. File an issue at " \
149
+ "https://github.com/celerbrake/celerbrake-ruby " \
150
+ "and attach the following payload: #{@payload}",
151
+ )
152
+ end
153
+
154
+ new_max_size
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,142 @@
1
+ module Celerbrake
2
+ # NoticeNotifier is reponsible for sending notices to Celerbrake. It supports
3
+ # synchronous and asynchronous delivery.
4
+ #
5
+ # @see Celerbrake::Config The list of options
6
+ # @since v1.0.0
7
+ # @api public
8
+ class NoticeNotifier
9
+ # @return [Array<Class>] filters to be executed first
10
+ DEFAULT_FILTERS = [
11
+ Celerbrake::Filters::SystemExitFilter,
12
+ Celerbrake::Filters::GemRootFilter,
13
+
14
+ # Optional filters (must be included by users):
15
+ # Celerbrake::Filters::ThreadFilter
16
+ ].freeze
17
+
18
+ include Inspectable
19
+ include Loggable
20
+
21
+ def initialize
22
+ @config = Celerbrake::Config.instance
23
+ @filter_chain = FilterChain.new
24
+ @async_sender = AsyncSender.new(:post, self.class.name)
25
+ @sync_sender = SyncSender.new
26
+
27
+ DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
28
+
29
+ add_filter(Celerbrake::Filters::ContextFilter.new)
30
+ add_filter(Celerbrake::Filters::ExceptionAttributesFilter.new)
31
+ end
32
+
33
+ # @see Celerbrake.notify
34
+ def notify(exception, params = {}, &block)
35
+ send_notice(exception, params, default_sender, &block)
36
+ end
37
+
38
+ # @see Celerbrake.notify_sync
39
+ def notify_sync(exception, params = {}, &block)
40
+ send_notice(exception, params, @sync_sender, &block).value
41
+ end
42
+
43
+ # @see Celerbrake.add_filte
44
+ def add_filter(filter = nil, &block)
45
+ @filter_chain.add_filter(block_given? ? block : filter)
46
+ end
47
+
48
+ # @see Celerbrake.delete_filter
49
+ def delete_filter(filter_class)
50
+ @filter_chain.delete_filter(filter_class)
51
+ end
52
+
53
+ # @see Celerbrake.build_notice
54
+ def build_notice(exception, params = {})
55
+ if @async_sender.closed?
56
+ raise Celerbrake::Error,
57
+ "Celerbrake is closed; can't build exception: " \
58
+ "#{exception.class}: #{exception}"
59
+ end
60
+
61
+ if exception.is_a?(Celerbrake::Notice)
62
+ exception[:params].merge!(params)
63
+ exception
64
+ else
65
+ Notice.new(convert_to_exception(exception), params.dup)
66
+ end
67
+ end
68
+
69
+ # @see Celerbrake.close
70
+ def close
71
+ @sync_sender.close
72
+ @async_sender.close
73
+ end
74
+
75
+ # @see Celerbrake.configured?
76
+ def configured?
77
+ @config.valid?
78
+ end
79
+
80
+ # @see Celerbrake.merge_context
81
+ def merge_context(context)
82
+ Celerbrake::Context.current.merge!(context)
83
+ end
84
+
85
+ # @return [Boolean]
86
+ # @since v4.14.0
87
+ def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
88
+ @filter_chain.includes?(filter_class)
89
+ end
90
+
91
+ private
92
+
93
+ def convert_to_exception(ex)
94
+ if ex.is_a?(Exception) || Backtrace.java_exception?(ex)
95
+ # Manually created exceptions don't have backtraces, so we create a fake
96
+ # one, whose first frame points to the place where Celerbrake was called
97
+ # (normally via `notify`).
98
+ ex.set_backtrace(clean_backtrace) unless ex.backtrace
99
+ return ex
100
+ end
101
+
102
+ e = RuntimeError.new(ex.to_s)
103
+ e.set_backtrace(clean_backtrace)
104
+ e
105
+ end
106
+
107
+ def send_notice(exception, params, sender)
108
+ promise = @config.check_configuration
109
+ return promise if promise.rejected?
110
+
111
+ notice = build_notice(exception, params)
112
+ yield notice if block_given?
113
+ @filter_chain.refine(notice)
114
+
115
+ promise = Celerbrake::Promise.new
116
+ return promise.reject("#{notice} was marked as ignored") if notice.ignored?
117
+
118
+ sender.send(notice, promise)
119
+ end
120
+
121
+ def default_sender
122
+ return @async_sender if @async_sender.has_workers?
123
+
124
+ logger.warn(
125
+ "#{LOG_LABEL} falling back to sync delivery because there are no " \
126
+ "running async workers",
127
+ )
128
+ @sync_sender
129
+ end
130
+
131
+ def clean_backtrace
132
+ caller_copy = Kernel.caller
133
+ clean_bt = caller_copy.drop_while { |frame| frame.include?('/lib/celerbrake') }
134
+
135
+ # If true, then it's likely an internal library error. In this case return
136
+ # at least some backtrace to simplify debugging.
137
+ return caller_copy if clean_bt.empty?
138
+
139
+ clean_bt
140
+ end
141
+ end
142
+ end