Module: ErrorRadar::Tracking

Defined in:
lib/error_radar/tracking.rb

Overview

The capture facade. Auto-classifies common exception types, lets the host app plug in custom rules, then persists/rolls-up an ErrorLog. Never raises.

Class Method Summary collapse

Class Method Details

.capture(exception, source: nil, category: nil, severity: nil, context: {}) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/error_radar/tracking.rb', line 9

def capture(exception, source: nil, category: nil, severity: nil, context: {})
  return nil unless ErrorRadar.config.enabled

  category ||= categorize(exception)
  severity ||= default_severity(exception, category)

  attrs = {
    category: category,
    severity: severity,
    error_class: exception.class.name,
    source: source || infer_source(exception),
    message: exception.message,
    backtrace: Array(exception.backtrace).first(ErrorRadar.config.backtrace_lines),
    context: context
  }

  ErrorRadar.config.detail_extractors.each do |extractor|
    extra = safe_call(extractor, exception)
    attrs.merge!(extra.compact) if extra.is_a?(Hash)
  end

  ErrorRadar::ErrorLog.record(**attrs)
rescue StandardError => e
  warn_internal("capture failed: #{e.class}: #{e.message}")
  nil
end

.categorize(exception) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/error_radar/tracking.rb', line 56

def categorize(exception)
  ErrorRadar.config.categorizers.each do |rule|
    cat = safe_call(rule, exception)
    return cat if cat
  end

  if defined?(ActiveRecord::ActiveRecordError) && exception.is_a?(ActiveRecord::ActiveRecordError)
    :database
  elsif exception.is_a?(SyntaxError) || exception.is_a?(NameError) ||
        exception.is_a?(ArgumentError) || exception.is_a?(TypeError)
    :syntax
  elsif network_error?(exception)
    :network
  else
    :application
  end
end

.default_severity(_exception, category) ⇒ Object



89
90
91
92
93
94
95
# File 'lib/error_radar/tracking.rb', line 89

def default_severity(_exception, category)
  case category.to_sym
  when :syntax, :database then :critical
  when :network, :external_api then :warning
  else :error
  end
end

.infer_source(exception) ⇒ Object



97
98
99
100
101
102
# File 'lib/error_radar/tracking.rb', line 97

def infer_source(exception)
  line = Array(exception.backtrace).find { |l| l.include?('/app/') } || Array(exception.backtrace).first
  return 'unknown' if line.nil? || line.empty?

  line.split(':').first.to_s.split('/app/').last || line
end

.monitor(source, category: nil, severity: nil, context: {}) ⇒ Object

Wrap a block (rake task, cron script, manual maintenance) so any exception is captured and then re-raised. Use at boundaries the web/Sidekiq handlers don’t cover.



39
40
41
42
43
44
# File 'lib/error_radar/tracking.rb', line 39

def monitor(source, category: nil, severity: nil, context: {})
  yield
rescue StandardError => e
  capture(e, source: source, category: category, severity: severity, context: context)
  raise
end

.network_error?(exception) ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/error_radar/tracking.rb', line 74

def network_error?(exception)
  names = %w[
    Net::OpenTimeout Net::ReadTimeout Errno::ECONNREFUSED Errno::ECONNRESET
    Errno::EHOSTUNREACH Errno::ETIMEDOUT SocketError Timeout::Error
    OpenSSL::SSL::SSLError EOFError Faraday::ConnectionFailed Faraday::TimeoutError
  ]
  klass = exception.class
  while klass
    return true if names.include?(klass.name)

    klass = klass.superclass
  end
  false
end

.notify(message, category: :application, severity: :error, source: nil, context: {}) ⇒ Object

Log a problem without an exception object.



47
48
49
50
51
52
53
54
# File 'lib/error_radar/tracking.rb', line 47

def notify(message, category: :application, severity: :error, source: nil, context: {})
  return nil unless ErrorRadar.config.enabled

  ErrorRadar::ErrorLog.record(category: category, severity: severity, message: message, source: source, context: context)
rescue StandardError => e
  warn_internal("notify failed: #{e.class}: #{e.message}")
  nil
end

.safe_call(callable, *args) ⇒ Object



104
105
106
107
108
109
# File 'lib/error_radar/tracking.rb', line 104

def safe_call(callable, *args)
  callable.call(*args)
rescue StandardError => e
  warn_internal("custom rule failed: #{e.class}: #{e.message}")
  nil
end

.warn_internal(message) ⇒ Object



111
112
113
114
# File 'lib/error_radar/tracking.rb', line 111

def warn_internal(message)
  logger = defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
  logger ? logger.error("[ErrorRadar] #{message}") : warn("[ErrorRadar] #{message}")
end