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



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'app/models/snitch_reporting/snitch_report.rb', line 156

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



197
198
199
200
201
# File 'app/models/snitch_reporting/snitch_report.rb', line 197

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



203
204
205
# File 'app/models/snitch_reporting/snitch_report.rb', line 203

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



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

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

.error(*args) ⇒ Object



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

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

.extract_base_variables(exceptions, arg_hash, _arg_values) ⇒ Object



109
110
111
112
113
114
115
116
# File 'app/models/snitch_reporting/snitch_report.rb', line 109

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



126
127
128
# File 'app/models/snitch_reporting/snitch_report.rb', line 126

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

.fatal(*args) ⇒ Object



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

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

.format_args(args) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'app/models/snitch_reporting/snitch_report.rb', line 94

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



233
234
235
236
237
238
239
240
241
# File 'app/models/snitch_reporting/snitch_report.rb', line 233

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



144
145
146
147
148
149
150
151
152
153
154
# File 'app/models/snitch_reporting/snitch_report.rb', line 144

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



243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/models/snitch_reporting/snitch_report.rb', line 243

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



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

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

.relevant_env_keysObject



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'app/models/snitch_reporting/snitch_report.rb', line 258

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



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/models/snitch_reporting/snitch_report.rb', line 64

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, arg_values)
  report_error = retrieve_error_string(base_exception, arg_hash, arg_values)
  report = retrieve_or_create_existing_report(log_level, santize_title(report_title), santize_title(report_error), 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 ||= {}
  binding.pry
  SnitchReporting::SnitchReport.fatal("Failed to create report. (#{ex.class})", env.to_s, ex.to_s)
end

.retrieve_error_string(exception, arg_hash, arg_values) ⇒ Object



188
189
190
191
192
193
194
195
# File 'app/models/snitch_reporting/snitch_report.rb', line 188

def retrieve_error_string(exception, arg_hash, arg_values)
  report_error ||= arg_values&.find { |arg_value| arg_value.is_a?(String) }
  report_error ||= (arg_hash[:klass] || arg_hash[:class]).presence
  report_error ||= arg_hash&.first.to_s.presence
  report_error ||= trace_from_exception(exception).find { |row| row.include?("/app/") }
  report_error ||= exception.try(:class).presence
  report_error
end

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



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

def retrieve_or_create_existing_report(log_level, sanitized_title, sanitized_error_msg, env, exception, arg_hash)
  report_identifiable_data = {
    error:     sanitized_title.presence,
    message:   sanitized_error_msg.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)
  report.add_tags(arg_hash.delete(:tags))
  report
end

.retrieve_report_title(exception, arg_hash, arg_values) ⇒ Object



177
178
179
180
181
182
183
184
185
186
# File 'app/models/snitch_reporting/snitch_report.rb', line 177

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

.santize_title(report_title) ⇒ Object



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

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



130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/models/snitch_reporting/snitch_report.rb', line 130

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



118
119
120
121
122
123
124
# File 'app/models/snitch_reporting/snitch_report.rb', line 118

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



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

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

.warn(*args) ⇒ Object



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

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

Instance Method Details

#add_tags(new_tags) ⇒ Object



301
302
303
304
305
# File 'app/models/snitch_reporting/snitch_report.rb', line 301

def add_tags(new_tags)
  return if new_tags.blank?

  update(tags: (tags + [new_tags]).compact.flatten.uniq)
end

#ignored=(bool) ⇒ Object



310
311
312
313
314
315
316
317
318
# File 'app/models/snitch_reporting/snitch_report.rb', line 310

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



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

def ignored?; ignored_at?; end

#resolved=(bool) ⇒ Object



320
321
322
323
324
325
326
327
328
# File 'app/models/snitch_reporting/snitch_report.rb', line 320

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



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

def resolved?; resolved_at?; end

#tagsObject



293
294
295
# File 'app/models/snitch_reporting/snitch_report.rb', line 293

def tags
  (super || []).uniq
end

#tags=(new_tags) ⇒ Object



297
298
299
# File 'app/models/snitch_reporting/snitch_report.rb', line 297

def tags=(new_tags)
  super((new_tags.try(:to_a) || [new_tags]).compact.flatten.uniq)
end

#tracker_for_date(date = Date.today) ⇒ Object



289
290
291
# File 'app/models/snitch_reporting/snitch_report.rb', line 289

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