Class: SnitchReporting::SnitchReport

Inherits:
ApplicationRecord
  • Object
show all
Defined in:
app/models/snitch_reporting/snitch_report.rb

Overview

text :error text :message integer :log_level string :klass string :action text :tags datetime :first_occurrence_at datetime :last_occurrence_at bigint :occurrence_count belongs_to :assigned_to datetime :resolved_at belongs_to :resolved_by datetime :ignored_at belongs_to :ignored_by

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#acting_userObject

Returns the value of attribute acting_user.



17
18
19
# File 'app/models/snitch_reporting/snitch_report.rb', line 17

def acting_user
  @acting_user
end

Class Method Details

.add_controller_data_to_report(report_data, env) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/models/snitch_reporting/snitch_report.rb', line 140

def add_controller_data_to_report(report_data, env)
  kontroller = env&.dig(:"action_controller.instance")
  return if kontroller.blank?

  extract_relevant_ivars(report_data, kontroller)

  kontroller.instance_variables.each do |ivar_key|
    begin
      next if ivar_key.to_s.starts_with?("@current_") # Already extracted these in the above method
      next if ivar_key.in?(ignored_kontroller_ivars)

      ivar = kontroller.instance_variable_get(ivar_key)
      next if ivar.blank?

      report_data[ivar_key] = get_details_from_ivar(ivar)
    rescue StandardError => ex
      report_data[ivar_key] = "!-- Failed to retrieve data from variable #{ivar_key}: #{ivar.try(:class).try(:name)} (#{ex.class}) --!"
    end
  end
end

.add_leftover_objects_to_report_data(report_data, exceptions, arg_hash, arg_values) ⇒ Object



171
172
173
174
175
# File 'app/models/snitch_reporting/snitch_report.rb', line 171

def add_leftover_objects_to_report_data(report_data, exceptions, arg_hash, arg_values)
  report_data[:exceptions] = exceptions.map { |ex| "#{ex.try(:class)}: #{ex.try(:message)}" } if exceptions.present?
  report_data.merge!(arg_hash)
  report_data[:details] = arg_values if arg_values.present?
end

.add_sanitized_env_information_to_report_data(report_data, env) ⇒ Object



177
178
179
# File 'app/models/snitch_reporting/snitch_report.rb', line 177

def add_sanitized_env_information_to_report_data(report_data, env)
  report_data[:env] = env.slice(*relevant_env_keys) if env.present?
end

.debug(*args) ⇒ Object



43
# File 'app/models/snitch_reporting/snitch_report.rb', line 43

def debug(*args);   report(:debug,   args); end

.error(*args) ⇒ Object



46
# File 'app/models/snitch_reporting/snitch_report.rb', line 46

def error(*args);   report(:error,   args); end

.extract_base_variables(exceptions, arg_hash, _arg_values) ⇒ Object



93
94
95
96
97
98
99
100
# File 'app/models/snitch_reporting/snitch_report.rb', line 93

def extract_base_variables(exceptions, arg_hash, _arg_values)
  exceptions << arg_hash.delete(:exception) if arg_hash[:exception].present?
  base_exception = exceptions.first || {} # TODO: Deal with other exceptions here
  klass = arg_hash[:klass] || arg_hash[:class]
  env = arg_hash.delete(:env) || {}

  [env, klass, base_exception]
end

.extract_relevant_ivars(report_data, kontroller) ⇒ Object



110
111
112
# File 'app/models/snitch_reporting/snitch_report.rb', line 110

def extract_relevant_ivars(report_data, kontroller)
  set_user_vars_from_source(report_data, kontroller)
end

.fatal(*args) ⇒ Object



47
# File 'app/models/snitch_reporting/snitch_report.rb', line 47

def fatal(*args);   report(:fatal,   args); end

.format_args(args) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/snitch_reporting/snitch_report.rb', line 78

def format_args(args)
  args = [args].flatten.compact

  exceptions = args.select { |arg| arg.is_a?(Exception) }
  reduced_arrays = args.select { |arg| arg.is_a?(Array) }.reduce([], :concat)
  reduced_values = args.select { |arg| [Array, Exception, Hash].all? { |klass| !arg.is_a?(klass) } }
  arg_values = reduced_arrays + reduced_values

  arg_hash = (args.select { |arg| arg.is_a?(Hash) }.inject(&:merge) || {}).deep_symbolize_keys
  # This will remove duplicate keys in the case there are multiple hashes
  #   passed in for some reason. I don't foresee this being an issue, but
  #   if it ever proves to be, this is the spot to refactor.
  [exceptions, arg_hash, arg_values]
end

.gather_report_data(env, exceptions, arg_hash, arg_values) ⇒ Object



205
206
207
208
209
210
211
212
213
# File 'app/models/snitch_reporting/snitch_report.rb', line 205

def gather_report_data(env, exceptions, arg_hash, arg_values)
  report_data = {}

  add_controller_data_to_report(report_data, env)
  add_leftover_objects_to_report_data(report_data, exceptions, arg_hash, arg_values)
  add_sanitized_env_information_to_report_data(report_data, env)

  report_data
end

.get_details_from_ivar(ivar) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
# File 'app/models/snitch_reporting/snitch_report.rb', line 128

def get_details_from_ivar(ivar)
  return ivar.class.name if ivar.class.name.include?("Abilit")
  case ivar.class.name
  when "String", "Array", "Hash" then ivar
  when "ActionController::Parameters" then ivar.to_json
  when "ActiveRecord::Relation"
    "#{ivar.klass} ids: [#{ivar.pluck(:id).join(', ')}]"
  else
    ivar.try(:to_data) || ivar.try(:to_json) || ivar.to_s.presence
  end
end

.ignored_kontroller_ivarsObject



215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'app/models/snitch_reporting/snitch_report.rb', line 215

def ignored_kontroller_ivars
  [
    :@_request,
    :@_response,
    :@_response,
    :@_lookup_context,
    :@_authorized,
    :@_main_app,
    :@_view_renderer,
    :@_view_context_class,
    :@_db_runtime,
    :@marked_for_same_origin_verification
  ]
end

.info(*args) ⇒ Object



44
# File 'app/models/snitch_reporting/snitch_report.rb', line 44

def info(*args);    report(:info,    args); end

.relevant_env_keysObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'app/models/snitch_reporting/snitch_report.rb', line 230

def relevant_env_keys
  [
    :REQUEST_URI,
    :REQUEST_METHOD,
    :HTTP_REFERER,
    :HTTP_USER_AGENT,
    :PATH_INFO,
    :HTTP_CONNECTION,
    :REMOTE_USER,
    :SERVER_NAME,
    :QUERY_STRING,
    :REMOTE_HOST,
    :SERVER_PORT,
    :HTTP_ACCEPT_ENCODING,
    :HTTP_USER_AGENT,
    :SERVER_PROTOCOL,
    :HTTP_CACHE_CONTROL,
    :HTTP_ACCEPT_LANGUAGE,
    :HTTP_HOST,
    :REMOTE_ADDR,
    :SERVER_SOFTWARE,
    :HTTP_KEEP_ALIVE,
    :HTTP_REFERER,
    :HTTP_ACCEPT_CHARSET,
    :GATEWAY_INTERFACE,
    :HTTP_ACCEPT,
    :HTTP_COOKIE
  ]
end

.report(log_level, *args) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snitch_reporting/snitch_report.rb', line 50

def report(log_level, *args)
  exceptions, arg_hash, arg_values = format_args(args)
  env, klass, base_exception = extract_base_variables(exceptions, arg_hash, arg_values)
  always_notify = arg_hash.delete(:always_notify)

  report_title = retrieve_report_title(base_exception, arg_hash)
  report = retrieve_or_create_existing_report(log_level, santize_title(report_title), env, base_exception, arg_hash)
  return SnitchReporting::SnitchReport.error("Failed to save report.", report.errors.full_messages) unless report.persisted?

  report_data = gather_report_data(env, exceptions, arg_hash, arg_values)

  occurrence = report.occurrences.create(
    http_method: env[:REQUEST_METHOD],
    url:         env[:REQUEST_URI],
    user_agent:  env[:HTTP_USER_AGENT],
    backtrace:   trace_from_exception(base_exception),
    context:     report_data,
    params:      env&.dig(:"action_controller.instance")&.params&.permit!&.to_h&.except(:action, :controller),
    headers:     env&.reject { |k, v| k.to_s.include?(".") || !v.is_a?(String) },
    always_notify: always_notify
  )
  return SnitchReporting::SnitchReport.error("Failed to save occurrence.", occurrence.errors.full_messages) unless occurrence.persisted?
  occurrence
rescue StandardError => ex
  env ||= {}
  SnitchReporting::SnitchReport.fatal("Failed to create report. (#{ex.class})", env, ex)
end

.retrieve_or_create_existing_report(log_level, sanitized_title, env, exception, arg_hash) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'app/models/snitch_reporting/snitch_report.rb', line 189

def retrieve_or_create_existing_report(log_level, sanitized_title, env, exception, arg_hash)
  report_identifiable_data = {
    error:     (exception.try(:class) || sanitized_title.presence).to_s,
    message:   sanitized_title.presence,
    log_level: log_level,
    klass:     env&.dig(:"action_controller.instance").try(:class).to_s.split("::").last&.gsub("Controller", ""),
    action:    env&.dig(:"action_controller.instance").try(:action_name)
  }

  report = find_by(report_identifiable_data) if sanitized_title.present?
  # Not using find or create because the slug might be `nil`- in these
  #   cases, we want to create a new report so that we don't falsely group
  #   unrelated errors together.
  report || create(report_identifiable_data)
end

.retrieve_report_title(exception, arg_hash) ⇒ Object



161
162
163
164
165
166
167
168
169
# File 'app/models/snitch_reporting/snitch_report.rb', line 161

def retrieve_report_title(exception, arg_hash)
  report_title = arg_hash[:title].presence
  report_title ||= exception.try(:message).presence
  report_title ||= exception.try(:body).presence
  report_title ||= exception.try(:class).presence
  report_title ||= (arg_hash[:klass] || arg_hash[:class]).presence
  report_title ||= trace_from_exception(exception).find { |row| row.include?("/app/") }
  report_title
end

.santize_title(report_title) ⇒ Object



181
182
183
184
185
186
187
# File 'app/models/snitch_reporting/snitch_report.rb', line 181

def santize_title(report_title)
  regex_find_numbers_and_words_with_numbers = /\w*\d[\d\w]*/
  # We remove all numbers and words that have numbers in them so that we can
  #   more easily group similar errors together, but often times errors have
  #   unique ids, so we strip those out.
  report_title.to_s.gsub(regex_find_numbers_and_words_with_numbers, "").presence
end

.set_user_vars_from_source(report_data, source) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'app/models/snitch_reporting/snitch_report.rb', line 114

def set_user_vars_from_source(report_data, source)
  user_vars = source.try(:instance_variables)&.select { |ivar_key| ivar_key.to_s.starts_with?("@current_") } || []
  user_vars.each do |user_var|
    begin
      ivar = source.instance_variable_get(user_var)
      next if ivar.blank?

      report_data[user_var] = get_details_from_ivar(ivar)
    rescue StandardError => ex
      report_data[user_var] = "!-- Failed to retrieve data from user #{user_var}: #{ivar.try(:class).try(:name)} (#{ex.class}) --!"
    end
  end
end

.trace_from_exception(exception) ⇒ Object



102
103
104
105
106
107
108
# File 'app/models/snitch_reporting/snitch_report.rb', line 102

def trace_from_exception(exception)
  trace = exception.try(:backtrace).presence
  return trace if trace.present?
  trace = caller.dup
  trace.shift until trace.first.exclude?("snitch_reporting")
  trace
end

.unknown(*args) ⇒ Object



48
# File 'app/models/snitch_reporting/snitch_report.rb', line 48

def unknown(*args); report(:unknown, args); end

.warn(*args) ⇒ Object



45
# File 'app/models/snitch_reporting/snitch_report.rb', line 45

def warn(*args);    report(:warn,    args); end

Instance Method Details

#assigned_toObject

belongs_to :assigned_to



23
# File 'app/models/snitch_reporting/snitch_report.rb', line 23

def assigned_to; end

#ignored=(bool) ⇒ Object



268
269
270
271
272
273
274
275
276
# File 'app/models/snitch_reporting/snitch_report.rb', line 268

def ignored=(bool)
  if bool.to_s.downcase.in?(["t", "true", "1", "y", "yes"])
    self.ignored_at ||= Time.current
    # self.ignored_by ||= acting_user
  else
    self.ignored_at = nil
    # self.ignored_by = nil
  end
end

#ignored?Boolean

Returns:

  • (Boolean)


266
# File 'app/models/snitch_reporting/snitch_report.rb', line 266

def ignored?; ignored_at?; end

#resolved=(bool) ⇒ Object



278
279
280
281
282
283
284
285
286
# File 'app/models/snitch_reporting/snitch_report.rb', line 278

def resolved=(bool)
  if bool.to_s.downcase.in?(["t", "true", "1", "y", "yes"])
    self.resolved_at ||= Time.current
    # self.resolved_by ||= acting_user
  else
    self.resolved_at = nil
    # self.resolved_by = nil
  end
end

#resolved?Boolean

Returns:

  • (Boolean)


265
# File 'app/models/snitch_reporting/snitch_report.rb', line 265

def resolved?; resolved_at?; end

#tracker_for_date(date = Date.today) ⇒ Object



261
262
263
# File 'app/models/snitch_reporting/snitch_report.rb', line 261

def tracker_for_date(date=Date.today)
  trackers.tracker_for_date(date)
end