snitch_reporting 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +23 -3
- data/app/assets/javascripts/snitch_reporting/snitch_report.js +60 -0
- data/app/assets/stylesheets/snitch_reporting/_variables.scss +5 -0
- data/app/assets/stylesheets/snitch_reporting/components.scss +120 -0
- data/app/assets/stylesheets/snitch_reporting/containers.scss +6 -0
- data/app/assets/stylesheets/snitch_reporting/default.scss +18 -0
- data/app/assets/stylesheets/snitch_reporting/forms.scss +36 -0
- data/app/assets/stylesheets/snitch_reporting/navigation.scss +37 -0
- data/app/assets/stylesheets/snitch_reporting/snitch_report.scss +1 -0
- data/app/assets/stylesheets/snitch_reporting/tables.scss +127 -0
- data/app/controllers/snitch_reporting/application_controller.rb +4 -0
- data/app/controllers/snitch_reporting/snitch_reports_controller.rb +167 -0
- data/app/helpers/snitch_reporting/params_helper.rb +38 -0
- data/app/helpers/snitch_reporting/snitch_report_helper.rb +2 -0
- data/app/models/snitch_reporting/service/json_wrapper.rb +5 -0
- data/app/models/snitch_reporting/snitch_comment.rb +7 -0
- data/app/models/snitch_reporting/snitch_history.rb +7 -0
- data/app/models/snitch_reporting/snitch_occurrence.rb +183 -0
- data/app/models/snitch_reporting/snitch_report.rb +301 -0
- data/app/models/snitch_reporting/snitch_tracker.rb +17 -0
- data/app/views/snitch_reporting/snitch_reports/_filters.html.erb +16 -0
- data/app/views/snitch_reporting/snitch_reports/_navigation.html.erb +0 -0
- data/app/views/snitch_reporting/snitch_reports/edit.html.erb +19 -0
- data/app/views/snitch_reporting/snitch_reports/index.html.erb +75 -0
- data/app/views/snitch_reporting/snitch_reports/show.html.erb +189 -0
- data/config/routes.rb +4 -1
- data/lib/generators/snitch_reporting/install/install_generator.rb +24 -0
- data/lib/generators/snitch_reporting/install/templates/install_snitch_reporting.rb +62 -0
- data/lib/snitch_reporting/engine.rb +3 -0
- data/lib/snitch_reporting/rack.rb +29 -0
- data/lib/snitch_reporting/version.rb +1 -1
- data/lib/snitch_reporting.rb +3 -1
- metadata +58 -3
@@ -0,0 +1,167 @@
|
|
1
|
+
require_dependency "snitch_reporting/application_controller"
|
2
|
+
|
3
|
+
class ::SnitchReporting::SnitchReportsController < ApplicationController
|
4
|
+
include ::SnitchReporting::ParamsHelper
|
5
|
+
helper ::SnitchReporting::SnitchReportHelper
|
6
|
+
|
7
|
+
layout "application"
|
8
|
+
|
9
|
+
def index
|
10
|
+
@reports = ::SnitchReporting::SnitchReport.order("last_occurrence_at DESC NULLS LAST").page(params[:page]).per(params[:per] || 25)
|
11
|
+
|
12
|
+
# set_report_preferences
|
13
|
+
filter_reports
|
14
|
+
# sort_reports
|
15
|
+
end
|
16
|
+
|
17
|
+
def show
|
18
|
+
@report = ::SnitchReporting::SnitchReport.find(params[:id])
|
19
|
+
occurrences = @report.occurrences.order(created_at: :asc)
|
20
|
+
@occurrence = occurrences.find_by(id: params[:occurrence]) || occurrences.last
|
21
|
+
occurrence_ids = occurrences.ids
|
22
|
+
occurrence_idx = occurrence_ids.index(@occurrence.id)
|
23
|
+
@paged_ids = {
|
24
|
+
first: occurrence_idx == 0 ? nil : occurrence_ids.first,
|
25
|
+
prev: occurrence_idx == 0 ? nil : occurrence_ids[occurrence_idx - 1],
|
26
|
+
next: occurrence_idx == occurrence_ids.length - 1 ? nil : occurrence_ids[occurrence_idx + 1],
|
27
|
+
last: occurrence_idx == occurrence_ids.length - 1 ? nil : occurrence_ids.last,
|
28
|
+
}
|
29
|
+
# @formatted_occurrence_data = occurrences.staggered_occurrence_data
|
30
|
+
# @comments = @report.comments.order(created_at: :desc)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def update
|
35
|
+
@report = ::SnitchReporting::SnitchReport.find(params[:id])
|
36
|
+
# @report.acting_user = current_user
|
37
|
+
@report.update(report_params)
|
38
|
+
|
39
|
+
respond_to do |format|
|
40
|
+
format.html { redirect_to @report }
|
41
|
+
format.json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
#
|
45
|
+
# def comment
|
46
|
+
# if @report.comments.create(comment_params.merge(author: current_credential).merge(params.permit(:resolved, :ignored)))
|
47
|
+
# update_report_for(:resolved, comment: false)
|
48
|
+
# update_report_for(:ignored, comment: false)
|
49
|
+
#
|
50
|
+
# redirect_to snitch_report_path(@report)
|
51
|
+
# else
|
52
|
+
# redirect_to snitch_report_path(@report), alert: "Failed to comment. Please try again."
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
private
|
57
|
+
|
58
|
+
def report_params
|
59
|
+
params.require(:snitch_report).permit(
|
60
|
+
:ignored,
|
61
|
+
:resolved
|
62
|
+
)
|
63
|
+
end
|
64
|
+
#
|
65
|
+
# def current_snitch_report
|
66
|
+
# @report = SnitchReporting::SnitchReport.find(params[:id])
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# def update_report_for(status, comment: true)
|
70
|
+
# raise "Value not allowed: #{status}" unless status.in?([:resolved, :ignored])
|
71
|
+
#
|
72
|
+
# if true_param?(status)
|
73
|
+
# @report.update("#{status}_at": Time.current, "#{status}_by": current_credential)
|
74
|
+
# # @report.comments.create(author: current_credential, message: ">>> Marked as #{status} <<<", skip_notify: true, status => true) if comment
|
75
|
+
# elsif params[status].present?
|
76
|
+
# # @report.comments.create(author: current_credential, message: ">>> Marked as un#{status} <<<", skip_notify: true, status => false) if comment
|
77
|
+
# @report.update("#{status}_at": nil, "#{status}_by": nil)
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# def report_params
|
82
|
+
# params.require(:snitch_report).permit(
|
83
|
+
# :source,
|
84
|
+
# :severity,
|
85
|
+
# :assigned_to_id,
|
86
|
+
# :title,
|
87
|
+
# :custom_details
|
88
|
+
# )
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# def comment_params
|
92
|
+
# params.require(:snitch_comment).permit(:message)
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# def set_report_preferences
|
96
|
+
# @report_preferences = begin
|
97
|
+
# preferences = JSON.parse(session[:report_preferences].presence || "{}").symbolize_keys
|
98
|
+
#
|
99
|
+
# available_preferences = [:level_tags, :severity_tags, :source_tags, :resolved, :ignored]
|
100
|
+
# available_preferences.each do |pref_key|
|
101
|
+
# pref_val = params[pref_key]
|
102
|
+
# preferences[pref_key] = pref_val if pref_val.present?
|
103
|
+
# preferences.delete(pref_key) if pref_val == "all"
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# session[:report_preferences] = preferences.to_json
|
107
|
+
# preferences
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
def set_filters
|
111
|
+
@filter_sets = {
|
112
|
+
status: {
|
113
|
+
default: :unresolved,
|
114
|
+
values: [:all, :resolved, :unresolved]
|
115
|
+
},
|
116
|
+
# assignee: {
|
117
|
+
# default: :any,
|
118
|
+
# values: [:any, :me, :not_me, :not_assigned]
|
119
|
+
# },
|
120
|
+
# log_level: {
|
121
|
+
# default: :any,
|
122
|
+
# values: [:any] + ::SnitchReporting::SnitchReport.log_levels.keys.map(&:to_sym)
|
123
|
+
# },
|
124
|
+
# ignored: {
|
125
|
+
# default: :not_ignored,
|
126
|
+
# values: [:all, :ignored, :not_ignored]
|
127
|
+
# }
|
128
|
+
}
|
129
|
+
|
130
|
+
@filters = @filter_sets.each_with_object({set_filters: {}}) do |(filter_name, filter_set), filters|
|
131
|
+
filters[filter_name] = filter_set[:default]
|
132
|
+
filter_in_param = params[filter_name].try(:to_sym)
|
133
|
+
next unless filter_in_param && filter_set[:values].include?(filter_in_param)
|
134
|
+
filters[filter_name] = filter_in_param
|
135
|
+
filters[:set_filters][filter_name] = filter_in_param
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def filter_reports
|
140
|
+
set_filters
|
141
|
+
|
142
|
+
@reports = @reports.resolved if @filters[:status] == :resolved
|
143
|
+
@reports = @reports.unresolved if @filters[:status] == :unresolved
|
144
|
+
# @reports = @reports.search(@report_preferences[:by_fuzzy_text]) if @report_preferences[:by_fuzzy_text].present?
|
145
|
+
#
|
146
|
+
# @reports = @reports.by_level(@report_preferences[:level_tags]) if @report_preferences[:level_tags].present?
|
147
|
+
# @reports = @reports.by_severity(@report_preferences[:severity_tags]) if @report_preferences[:severity_tags].present?
|
148
|
+
# @reports = @reports.by_source(@report_preferences[:source_tags]) if @report_preferences[:source_tags].present?
|
149
|
+
#
|
150
|
+
# @reports = @report_preferences[:resolved].present? && truthy?(@report_preferences[:resolved]) ? @reports.resolved : @reports.unresolved
|
151
|
+
# @reports = @report_preferences[:ignored].present? && truthy?(@report_preferences[:ignored]) ? @reports.ignored : @reports.unignored
|
152
|
+
end
|
153
|
+
#
|
154
|
+
# def sort_reports
|
155
|
+
# order = sort_order || "desc"
|
156
|
+
# @reports =
|
157
|
+
# case params[:sort]
|
158
|
+
# when "count"
|
159
|
+
# @reports.order("snitch_reporting_snitch_reports.occurrence_count #{order} NULLS LAST, snitch_reporting_snitch_reports.last_occurrence_at DESC NULLS LAST")
|
160
|
+
# when "last"
|
161
|
+
# @reports.order("snitch_reporting_snitch_reports.last_occurrence_at #{order} NULLS LAST")
|
162
|
+
# else
|
163
|
+
# @reports.order("snitch_reporting_snitch_reports.last_occurrence_at DESC NULLS LAST")
|
164
|
+
# end
|
165
|
+
# @reports = @reports.page(params[:page]).per(params[:per])
|
166
|
+
# end
|
167
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ::SnitchReporting::ParamsHelper
|
2
|
+
def sort_order
|
3
|
+
params[:order] == "desc" ? "desc" : "asc"
|
4
|
+
end
|
5
|
+
|
6
|
+
def toggled_sort_order
|
7
|
+
params[:order] == "desc" ? "asc" : "desc"
|
8
|
+
end
|
9
|
+
|
10
|
+
def truthy?(val)
|
11
|
+
val.to_s.downcase.in?(["true", "t", "1"])
|
12
|
+
end
|
13
|
+
|
14
|
+
def true_param?(*param_keys)
|
15
|
+
truthy?(params&.dig(*param_keys))
|
16
|
+
end
|
17
|
+
|
18
|
+
def svg(img, opts={})
|
19
|
+
""
|
20
|
+
end
|
21
|
+
|
22
|
+
def sortable(link_text, param_string, options={})
|
23
|
+
if param_string.nil?
|
24
|
+
return link_to link_text.html_safe, options[:additional_params] || {}, class: "sortable #{options[:class]}" # rubocop:disable Rails/OutputSafety - We're certain this is safe because we're in control of all of the elements.
|
25
|
+
end
|
26
|
+
if params[:sort] == param_string
|
27
|
+
sorted_class = "sorted sortable-#{toggled_sort_order}"
|
28
|
+
default_order = toggled_sort_order
|
29
|
+
end
|
30
|
+
|
31
|
+
additional_params = options[:additional_params] || {}
|
32
|
+
additional_params = additional_params.permit!.to_h if additional_params.is_a?(ActionController::Parameters)
|
33
|
+
# rubocop:disable Rails/OutputSafety - We're certain this is safe because we're in control of all of the elements.
|
34
|
+
link_html = "#{link_text}<div class=\"sortable-arrows\">#{svg('icons/UpChevron.svg', class: 'desc')}#{svg('icons/DownChevron.svg', class: 'asc')}</div>".html_safe
|
35
|
+
# rubocop:enable Rails/OutputSafety
|
36
|
+
link_to link_html, { sort: param_string, order: default_order || "desc" }.reverse_merge(additional_params), class: "sortable #{sorted_class} #{options[:class]}"
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# belongs_to :snitch_report
|
2
|
+
# string :http_method
|
3
|
+
# string :url
|
4
|
+
# text :user_agent
|
5
|
+
# text :backtrace
|
6
|
+
# text :context
|
7
|
+
# text :params
|
8
|
+
# text :headers
|
9
|
+
|
10
|
+
class SnitchReporting::SnitchOccurrence < ApplicationRecord
|
11
|
+
attr_accessor :always_notify, :acting_user, :should_notify
|
12
|
+
|
13
|
+
belongs_to :report, class_name: "SnitchReporting::SnitchReport"
|
14
|
+
|
15
|
+
after_create :mark_occurrence
|
16
|
+
|
17
|
+
serialize :backtrace_data, ::SnitchReporting::Service::JSONWrapper
|
18
|
+
serialize :backtrace, ::SnitchReporting::Service::JSONWrapper
|
19
|
+
serialize :context, ::SnitchReporting::Service::JSONWrapper
|
20
|
+
serialize :params, ::SnitchReporting::Service::JSONWrapper
|
21
|
+
serialize :headers, ::SnitchReporting::Service::JSONWrapper
|
22
|
+
|
23
|
+
# def self.staggered_occurrence_data
|
24
|
+
# data = {}
|
25
|
+
# occurrence_data_keys = all.map { |occurrence| occurrence.details.try(:keys) }.compact
|
26
|
+
# longest_key_array = occurrence_data_keys.max_by(&:length) # Need the longest because `zip` removes any objects longer than the initial.
|
27
|
+
# staggered_keys = longest_key_array&.zip(*occurrence_data_keys)&.flatten(1)&.compact&.uniq || []
|
28
|
+
#
|
29
|
+
# data[:keys] = staggered_keys
|
30
|
+
# data[:details] = all.map do |occurrence|
|
31
|
+
# detail_hash = {
|
32
|
+
# id: occurrence.id,
|
33
|
+
# details: []
|
34
|
+
# }
|
35
|
+
# occurrence_details = occurrence.details
|
36
|
+
# data[:keys].each_with_index do |detail_key, idx|
|
37
|
+
# occurrence_detail = occurrence_details[detail_key]
|
38
|
+
# detail_hash[:details][idx] =
|
39
|
+
# if occurrence_detail.is_a?(Array)
|
40
|
+
# occurrence_detail.join("\n")
|
41
|
+
# else
|
42
|
+
# occurrence_detail.to_s
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# detail_hash
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# data
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def from_data=(details_hash)
|
52
|
+
# self.details = flatten_hash(details_hash)
|
53
|
+
# end
|
54
|
+
|
55
|
+
def backtrace=(trace_lines)
|
56
|
+
already_traced = []
|
57
|
+
self.backtrace_data = trace_lines.map do |trace_line|
|
58
|
+
next unless trace_line.include?("/app/") # && trace_line.exclude?("app/models/snitch_reporting")
|
59
|
+
|
60
|
+
joined_path = file_lines_from_backtrace(trace_line)
|
61
|
+
next if joined_path.include?(joined_path)
|
62
|
+
already_traced << joined_path
|
63
|
+
|
64
|
+
file_path, line_number = joined_path.split(":", 2)
|
65
|
+
{
|
66
|
+
file_path: remove_project_root(file_path),
|
67
|
+
line_number: line_number,
|
68
|
+
snippet: snippet_for_line(trace_line)
|
69
|
+
}
|
70
|
+
end.compact
|
71
|
+
super(trace_lines.map { |trace_line| remove_project_root(trace_line) })
|
72
|
+
end
|
73
|
+
|
74
|
+
def filtered_backtrace
|
75
|
+
backtrace_data&.map { |data| data&.dig(:file_path) }.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove_project_root(row)
|
79
|
+
row.gsub(Rails.root.to_s, "[PROJECT_ROOT]")
|
80
|
+
end
|
81
|
+
|
82
|
+
def file_lines_from_backtrace(backtrace_line)
|
83
|
+
backtrace_line[/(\/?\w+\/?)+(\.\w+)+:?\d*/]
|
84
|
+
end
|
85
|
+
|
86
|
+
def snippet_for_line(backtrace_line, line_count: 5)
|
87
|
+
file, num = backtrace_line.split(":")
|
88
|
+
first_line_number = num.to_i
|
89
|
+
offset_line_numbers = (line_count - 1) / 2
|
90
|
+
line_numbers = (first_line_number - offset_line_numbers - 1)..(first_line_number + offset_line_numbers - 1)
|
91
|
+
|
92
|
+
lines = File.open(file).read.split("\n").map.with_index { |line, idx| [line, idx + 1] }[line_numbers] rescue nil
|
93
|
+
|
94
|
+
whitespace_count = lines.map { |line, _idx| line[/\s*/].length }.min
|
95
|
+
|
96
|
+
lines.each_with_object({}) do |(line, idx), data|
|
97
|
+
data[idx] = line[whitespace_count..-1]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
#
|
101
|
+
# def title
|
102
|
+
# super.presence || "#{report.title} | Occurrence ##{id}"
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# def details
|
106
|
+
# temp_details = super
|
107
|
+
#
|
108
|
+
# temp_details.each do |detail_key, detail_val|
|
109
|
+
# next unless detail_val.to_s.first.in?(["[", "{"])
|
110
|
+
#
|
111
|
+
# begin
|
112
|
+
# temp_details[detail_key] = JSON.parse(detail_val)
|
113
|
+
# rescue JSON::ParserError
|
114
|
+
# nil
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# temp_details
|
119
|
+
# end
|
120
|
+
|
121
|
+
def notify?
|
122
|
+
@should_notify ||= always_notify || report.resolved? || report.occurrence_count == 1
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def mark_occurrence
|
128
|
+
notify? # Store ivar
|
129
|
+
report_updates = { last_occurrence_at: Time.current, resolved_at: nil }
|
130
|
+
report_updates[:first_occurrence_at] = Time.current if report.first_occurrence_at.nil?
|
131
|
+
report_updates[:occurrence_count] = report.occurrence_count.to_i + 1
|
132
|
+
|
133
|
+
report.update(report_updates)
|
134
|
+
|
135
|
+
tracker = report.tracker_for_date
|
136
|
+
now = Time.current
|
137
|
+
todays_occurrences = report.occurrences.where(created_at: now.beginning_of_day..now.end_of_day)
|
138
|
+
tracker.update(count: todays_occurrences.count + 1)
|
139
|
+
end
|
140
|
+
|
141
|
+
# def to_slack_attachment
|
142
|
+
# details ||= {}
|
143
|
+
# description = details[:description] || details[:error_description] || details[:error] || details[:explanation]
|
144
|
+
# source = report.source.to_s.titleize
|
145
|
+
# level = report.log_level.to_sym
|
146
|
+
#
|
147
|
+
# color =
|
148
|
+
# case level
|
149
|
+
# when :debug then "#F5F5F5"
|
150
|
+
# when :info then "#209CEE"
|
151
|
+
# when :warn then "#FFDD57"
|
152
|
+
# when :error then "#FF945B"
|
153
|
+
# when :fatal then "#FF1F35"
|
154
|
+
# when :unknown then "#000000"
|
155
|
+
# end
|
156
|
+
# fields = []
|
157
|
+
# fields << { title: "Level", value: level, short: true } if level.present?
|
158
|
+
# fields << { title: "Source", value: source, short: true } if source.present?
|
159
|
+
# fields << { title: "Description", value: description, short: false } if description.present?
|
160
|
+
#
|
161
|
+
# {
|
162
|
+
# pretext: report.resolved? ? "A previously resolved #{level} level error has occurred again:" : "A new #{level} level error has occurred:",
|
163
|
+
# title: title,
|
164
|
+
# fallback: "```#{filtered_backtrace.present? ? filtered_backtrace.first(5).join("\n") : description}```",
|
165
|
+
# color: color,
|
166
|
+
# title_link: custom_url_for(:helix, :bug_report_path, report_id),
|
167
|
+
# text: filtered_backtrace.present? ? filtered_backtrace.first(10).join("\n") : description,
|
168
|
+
# fields: fields,
|
169
|
+
# footer: "Last occurred:",
|
170
|
+
# ts: created_at.to_i
|
171
|
+
# }
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# def flatten_hash(hash_to_flatten, hash_memo={}, ancestor_key_str=nil)
|
175
|
+
# hash_to_flatten = hash_to_flatten[0] if hash_to_flatten.is_a?(Array) && hash_to_flatten.first.is_a?(Hash)
|
176
|
+
# return hash_memo.merge!(ancestor_key_str => hash_to_flatten.to_s) unless hash_to_flatten.is_a?(Hash)
|
177
|
+
# # If the current object is not a hash, we don't need to continue nesting, so set the current ancestor string as the final key.
|
178
|
+
# hash_to_flatten.each { |key, hash_values| flatten_hash(hash_values, hash_memo, [ancestor_key_str, key].compact.join(".")) }
|
179
|
+
# # Iterate through the hash, and pass each key as the ancestor into the current method to flatten lower level hashes.
|
180
|
+
# hash_memo
|
181
|
+
# # Return the current hash for memoization.
|
182
|
+
# end
|
183
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# text :error
|
2
|
+
# text :message
|
3
|
+
# integer :log_level
|
4
|
+
# string :klass
|
5
|
+
# string :action
|
6
|
+
# text :tags
|
7
|
+
# datetime :first_occurrence_at
|
8
|
+
# datetime :last_occurrence_at
|
9
|
+
# bigint :occurrence_count
|
10
|
+
# belongs_to :assigned_to
|
11
|
+
# datetime :resolved_at
|
12
|
+
# belongs_to :resolved_by
|
13
|
+
# datetime :ignored_at
|
14
|
+
# belongs_to :ignored_by
|
15
|
+
|
16
|
+
class SnitchReporting::SnitchReport < ApplicationRecord
|
17
|
+
attr_accessor :acting_user
|
18
|
+
has_many :occurrences, class_name: "SnitchReporting::SnitchOccurrence", foreign_key: :report_id
|
19
|
+
has_many :trackers, class_name: "SnitchReporting::SnitchTracker", foreign_key: :report_id
|
20
|
+
has_many :histories, class_name: "SnitchReporting::SnitchHistory", foreign_key: :report_id
|
21
|
+
|
22
|
+
# belongs_to :assigned_to
|
23
|
+
def assigned_to; end
|
24
|
+
# belongs_to :resolved_by
|
25
|
+
# belongs_to :ignored_by
|
26
|
+
|
27
|
+
scope :resolved, -> { where.not(resolved_at: nil) }
|
28
|
+
scope :unresolved, -> { where(resolved_at: nil) }
|
29
|
+
scope :ignored, -> { where.not(ignored_at: nil) }
|
30
|
+
scope :unignored, -> { where(ignored_at: nil) }
|
31
|
+
scope :by_level, ->(*level_tags) { where(log_level: level_tags) }
|
32
|
+
|
33
|
+
enum log_level: {
|
34
|
+
debug: 1,
|
35
|
+
info: 2,
|
36
|
+
warn: 3,
|
37
|
+
error: 4,
|
38
|
+
fatal: 5,
|
39
|
+
unknown: 6
|
40
|
+
}
|
41
|
+
|
42
|
+
class << self
|
43
|
+
def debug(*args); report(:debug, args); end
|
44
|
+
def info(*args); report(:info, args); end
|
45
|
+
def warn(*args); report(:warn, args); end
|
46
|
+
def error(*args); report(:error, args); end
|
47
|
+
def fatal(*args); report(:fatal, args); end
|
48
|
+
def unknown(*args); report(:unknown, args); end
|
49
|
+
|
50
|
+
def report(log_level, *args)
|
51
|
+
exceptions, arg_hash, arg_values = format_args(args)
|
52
|
+
env, klass, base_exception = extract_base_variables(exceptions, arg_hash, arg_values)
|
53
|
+
always_notify = arg_hash.delete(:always_notify)
|
54
|
+
|
55
|
+
report_title = retrieve_report_title(base_exception, arg_hash)
|
56
|
+
report = retrieve_or_create_existing_report(log_level, santize_title(report_title), env, base_exception, arg_hash)
|
57
|
+
return SnitchReporting::SnitchReport.error("Failed to save report.", report.errors.full_messages) unless report.persisted?
|
58
|
+
|
59
|
+
report_data = gather_report_data(env, exceptions, arg_hash, arg_values)
|
60
|
+
|
61
|
+
occurrence = report.occurrences.create(
|
62
|
+
http_method: env[:REQUEST_METHOD],
|
63
|
+
url: env[:REQUEST_URI],
|
64
|
+
user_agent: env[:HTTP_USER_AGENT],
|
65
|
+
backtrace: trace_from_exception(base_exception),
|
66
|
+
context: report_data,
|
67
|
+
params: env&.dig(:"action_controller.instance")&.params&.permit!&.to_h&.except(:action, :controller),
|
68
|
+
headers: env&.reject { |k, v| k.to_s.include?(".") || !v.is_a?(String) },
|
69
|
+
always_notify: always_notify
|
70
|
+
)
|
71
|
+
return SnitchReporting::SnitchReport.error("Failed to save occurrence.", occurrence.errors.full_messages) unless occurrence.persisted?
|
72
|
+
occurrence
|
73
|
+
rescue StandardError => ex
|
74
|
+
env ||= {}
|
75
|
+
SnitchReporting::SnitchReport.fatal("Failed to create report. (#{ex.class})", env, ex)
|
76
|
+
end
|
77
|
+
|
78
|
+
def format_args(args)
|
79
|
+
args = [args].flatten.compact
|
80
|
+
|
81
|
+
exceptions = args.select { |arg| arg.is_a?(Exception) }
|
82
|
+
reduced_arrays = args.select { |arg| arg.is_a?(Array) }.reduce([], :concat)
|
83
|
+
reduced_values = args.select { |arg| [Array, Exception, Hash].all? { |klass| !arg.is_a?(klass) } }
|
84
|
+
arg_values = reduced_arrays + reduced_values
|
85
|
+
|
86
|
+
arg_hash = (args.select { |arg| arg.is_a?(Hash) }.inject(&:merge) || {}).deep_symbolize_keys
|
87
|
+
# This will remove duplicate keys in the case there are multiple hashes
|
88
|
+
# passed in for some reason. I don't foresee this being an issue, but
|
89
|
+
# if it ever proves to be, this is the spot to refactor.
|
90
|
+
[exceptions, arg_hash, arg_values]
|
91
|
+
end
|
92
|
+
|
93
|
+
def extract_base_variables(exceptions, arg_hash, _arg_values)
|
94
|
+
exceptions << arg_hash.delete(:exception) if arg_hash[:exception].present?
|
95
|
+
base_exception = exceptions.first || {} # TODO: Deal with other exceptions here
|
96
|
+
klass = arg_hash[:klass] || arg_hash[:class]
|
97
|
+
env = arg_hash.delete(:env) || {}
|
98
|
+
|
99
|
+
[env, klass, base_exception]
|
100
|
+
end
|
101
|
+
|
102
|
+
def trace_from_exception(exception)
|
103
|
+
trace = exception.try(:backtrace).presence
|
104
|
+
return trace if trace.present?
|
105
|
+
trace = caller.dup
|
106
|
+
trace.shift until trace.first.exclude?("snitch_reporting")
|
107
|
+
trace
|
108
|
+
end
|
109
|
+
|
110
|
+
def extract_relevant_ivars(report_data, kontroller)
|
111
|
+
set_user_vars_from_source(report_data, kontroller)
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_user_vars_from_source(report_data, source)
|
115
|
+
user_vars = source.try(:instance_variables)&.select { |ivar_key| ivar_key.to_s.starts_with?("@current_") } || []
|
116
|
+
user_vars.each do |user_var|
|
117
|
+
begin
|
118
|
+
ivar = source.instance_variable_get(user_var)
|
119
|
+
next if ivar.blank?
|
120
|
+
|
121
|
+
report_data[user_var] = get_details_from_ivar(ivar)
|
122
|
+
rescue StandardError => ex
|
123
|
+
report_data[user_var] = "!-- Failed to retrieve data from user #{user_var}: #{ivar.try(:class).try(:name)} (#{ex.class}) --!"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_details_from_ivar(ivar)
|
129
|
+
return ivar.class.name if ivar.class.name.include?("Abilit")
|
130
|
+
case ivar.class.name
|
131
|
+
when "String", "Array", "Hash" then ivar
|
132
|
+
when "ActionController::Parameters" then ivar.to_json
|
133
|
+
when "ActiveRecord::Relation"
|
134
|
+
"#{ivar.klass} ids: [#{ivar.pluck(:id).join(', ')}]"
|
135
|
+
else
|
136
|
+
ivar.try(:to_data) || ivar.try(:to_json) || ivar.to_s.presence
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def add_controller_data_to_report(report_data, env)
|
141
|
+
kontroller = env&.dig(:"action_controller.instance")
|
142
|
+
return if kontroller.blank?
|
143
|
+
|
144
|
+
extract_relevant_ivars(report_data, kontroller)
|
145
|
+
|
146
|
+
kontroller.instance_variables.each do |ivar_key|
|
147
|
+
begin
|
148
|
+
next if ivar_key.to_s.starts_with?("@current_") # Already extracted these in the above method
|
149
|
+
next if ivar_key.in?(ignored_kontroller_ivars)
|
150
|
+
|
151
|
+
ivar = kontroller.instance_variable_get(ivar_key)
|
152
|
+
next if ivar.blank?
|
153
|
+
|
154
|
+
report_data[ivar_key] = get_details_from_ivar(ivar)
|
155
|
+
rescue StandardError => ex
|
156
|
+
report_data[ivar_key] = "!-- Failed to retrieve data from variable #{ivar_key}: #{ivar.try(:class).try(:name)} (#{ex.class}) --!"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def retrieve_report_title(exception, arg_hash)
|
162
|
+
report_title = arg_hash[:title].presence
|
163
|
+
report_title ||= exception.try(:message).presence
|
164
|
+
report_title ||= exception.try(:body).presence
|
165
|
+
report_title ||= exception.try(:class).presence
|
166
|
+
report_title ||= (arg_hash[:klass] || arg_hash[:class]).presence
|
167
|
+
report_title ||= trace_from_exception(exception).find { |row| row.include?("/app/") }
|
168
|
+
report_title
|
169
|
+
end
|
170
|
+
|
171
|
+
def add_leftover_objects_to_report_data(report_data, exceptions, arg_hash, arg_values)
|
172
|
+
report_data[:exceptions] = exceptions.map { |ex| "#{ex.try(:class)}: #{ex.try(:message)}" } if exceptions.present?
|
173
|
+
report_data.merge!(arg_hash)
|
174
|
+
report_data[:details] = arg_values if arg_values.present?
|
175
|
+
end
|
176
|
+
|
177
|
+
def add_sanitized_env_information_to_report_data(report_data, env)
|
178
|
+
report_data[:env] = env.slice(*relevant_env_keys) if env.present?
|
179
|
+
end
|
180
|
+
|
181
|
+
def santize_title(report_title)
|
182
|
+
regex_find_numbers_and_words_with_numbers = /\w*\d[\d\w]*/
|
183
|
+
# We remove all numbers and words that have numbers in them so that we can
|
184
|
+
# more easily group similar errors together, but often times errors have
|
185
|
+
# unique ids, so we strip those out.
|
186
|
+
report_title.to_s.gsub(regex_find_numbers_and_words_with_numbers, "").presence
|
187
|
+
end
|
188
|
+
|
189
|
+
def retrieve_or_create_existing_report(log_level, sanitized_title, env, exception, arg_hash)
|
190
|
+
report_identifiable_data = {
|
191
|
+
error: (exception.try(:class) || sanitized_title.presence).to_s,
|
192
|
+
message: sanitized_title.presence,
|
193
|
+
log_level: log_level,
|
194
|
+
klass: env&.dig(:"action_controller.instance").try(:class).to_s.split("::").last&.gsub("Controller", ""),
|
195
|
+
action: env&.dig(:"action_controller.instance").try(:action_name)
|
196
|
+
}
|
197
|
+
|
198
|
+
report = find_by(report_identifiable_data) if sanitized_title.present?
|
199
|
+
# Not using find or create because the slug might be `nil`- in these
|
200
|
+
# cases, we want to create a new report so that we don't falsely group
|
201
|
+
# unrelated errors together.
|
202
|
+
report || create(report_identifiable_data)
|
203
|
+
end
|
204
|
+
|
205
|
+
def gather_report_data(env, exceptions, arg_hash, arg_values)
|
206
|
+
report_data = {}
|
207
|
+
|
208
|
+
add_controller_data_to_report(report_data, env)
|
209
|
+
add_leftover_objects_to_report_data(report_data, exceptions, arg_hash, arg_values)
|
210
|
+
add_sanitized_env_information_to_report_data(report_data, env)
|
211
|
+
|
212
|
+
report_data
|
213
|
+
end
|
214
|
+
|
215
|
+
def ignored_kontroller_ivars
|
216
|
+
[
|
217
|
+
:@_request,
|
218
|
+
:@_response,
|
219
|
+
:@_response,
|
220
|
+
:@_lookup_context,
|
221
|
+
:@_authorized,
|
222
|
+
:@_main_app,
|
223
|
+
:@_view_renderer,
|
224
|
+
:@_view_context_class,
|
225
|
+
:@_db_runtime,
|
226
|
+
:@marked_for_same_origin_verification
|
227
|
+
]
|
228
|
+
end
|
229
|
+
|
230
|
+
def relevant_env_keys
|
231
|
+
[
|
232
|
+
:REQUEST_URI,
|
233
|
+
:REQUEST_METHOD,
|
234
|
+
:HTTP_REFERER,
|
235
|
+
:HTTP_USER_AGENT,
|
236
|
+
:PATH_INFO,
|
237
|
+
:HTTP_CONNECTION,
|
238
|
+
:REMOTE_USER,
|
239
|
+
:SERVER_NAME,
|
240
|
+
:QUERY_STRING,
|
241
|
+
:REMOTE_HOST,
|
242
|
+
:SERVER_PORT,
|
243
|
+
:HTTP_ACCEPT_ENCODING,
|
244
|
+
:HTTP_USER_AGENT,
|
245
|
+
:SERVER_PROTOCOL,
|
246
|
+
:HTTP_CACHE_CONTROL,
|
247
|
+
:HTTP_ACCEPT_LANGUAGE,
|
248
|
+
:HTTP_HOST,
|
249
|
+
:REMOTE_ADDR,
|
250
|
+
:SERVER_SOFTWARE,
|
251
|
+
:HTTP_KEEP_ALIVE,
|
252
|
+
:HTTP_REFERER,
|
253
|
+
:HTTP_ACCEPT_CHARSET,
|
254
|
+
:GATEWAY_INTERFACE,
|
255
|
+
:HTTP_ACCEPT,
|
256
|
+
:HTTP_COOKIE
|
257
|
+
]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def tracker_for_date(date=Date.today)
|
262
|
+
trackers.tracker_for_date(date)
|
263
|
+
end
|
264
|
+
|
265
|
+
def resolved?; resolved_at?; end
|
266
|
+
def ignored?; ignored_at?; end
|
267
|
+
|
268
|
+
def ignored=(bool)
|
269
|
+
if bool.to_s.downcase.in?(["t", "true", "1", "y", "yes"])
|
270
|
+
self.ignored_at ||= Time.current
|
271
|
+
# self.ignored_by ||= acting_user
|
272
|
+
else
|
273
|
+
self.ignored_at = nil
|
274
|
+
# self.ignored_by = nil
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def resolved=(bool)
|
279
|
+
if bool.to_s.downcase.in?(["t", "true", "1", "y", "yes"])
|
280
|
+
self.resolved_at ||= Time.current
|
281
|
+
# self.resolved_by ||= acting_user
|
282
|
+
else
|
283
|
+
self.resolved_at = nil
|
284
|
+
# self.resolved_by = nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# def title
|
289
|
+
# super.presence || slug.presence || "Report ##{id}"
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# def timeago
|
293
|
+
# "Not implemented"
|
294
|
+
# # timeago_in_words(last_occurrence_at)
|
295
|
+
# end
|
296
|
+
#
|
297
|
+
# def slack_channel
|
298
|
+
# "Not implemented"
|
299
|
+
# # "error-reporting"
|
300
|
+
# end
|
301
|
+
end
|