exceptify 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 +7 -0
- data/CHANGELOG.rdoc +16 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/CONTRIBUTING.md +33 -0
- data/MIT-LICENSE +23 -0
- data/README.md +534 -0
- data/RELEASING.md +51 -0
- data/Rakefile +25 -0
- data/docs/notifiers/custom.md +42 -0
- data/docs/notifiers/datadog.md +51 -0
- data/docs/notifiers/email.md +195 -0
- data/docs/notifiers/slack.md +154 -0
- data/docs/notifiers/sns.md +37 -0
- data/docs/notifiers/teams.md +54 -0
- data/docs/notifiers/webhook.md +60 -0
- data/exceptify.gemspec +48 -0
- data/lib/exceptify/base_notifier.rb +30 -0
- data/lib/exceptify/configuration.rb +184 -0
- data/lib/exceptify/datadog_notifier.rb +160 -0
- data/lib/exceptify/dispatcher.rb +49 -0
- data/lib/exceptify/email_notifier.rb +208 -0
- data/lib/exceptify/modules/backtrace_cleaner.rb +13 -0
- data/lib/exceptify/modules/error_grouping.rb +170 -0
- data/lib/exceptify/modules/formatter.rb +119 -0
- data/lib/exceptify/notification.rb +71 -0
- data/lib/exceptify/notifier.rb +19 -0
- data/lib/exceptify/notifier_registry.rb +55 -0
- data/lib/exceptify/rack.rb +88 -0
- data/lib/exceptify/rails/runner_tie.rb +57 -0
- data/lib/exceptify/rails.rb +29 -0
- data/lib/exceptify/rake.rb +59 -0
- data/lib/exceptify/request_context.rb +35 -0
- data/lib/exceptify/resque.rb +25 -0
- data/lib/exceptify/sidekiq.rb +15 -0
- data/lib/exceptify/slack_notifier.rb +141 -0
- data/lib/exceptify/sns_notifier.rb +98 -0
- data/lib/exceptify/solid_queue.rb +68 -0
- data/lib/exceptify/teams_notifier.rb +209 -0
- data/lib/exceptify/version.rb +5 -0
- data/lib/exceptify/views/exceptify/_backtrace.html.erb +3 -0
- data/lib/exceptify/views/exceptify/_backtrace.text.erb +1 -0
- data/lib/exceptify/views/exceptify/_data.html.erb +6 -0
- data/lib/exceptify/views/exceptify/_data.text.erb +1 -0
- data/lib/exceptify/views/exceptify/_environment.html.erb +10 -0
- data/lib/exceptify/views/exceptify/_environment.text.erb +5 -0
- data/lib/exceptify/views/exceptify/_request.html.erb +36 -0
- data/lib/exceptify/views/exceptify/_request.text.erb +10 -0
- data/lib/exceptify/views/exceptify/_session.html.erb +10 -0
- data/lib/exceptify/views/exceptify/_session.text.erb +2 -0
- data/lib/exceptify/views/exceptify/_title.html.erb +3 -0
- data/lib/exceptify/views/exceptify/_title.text.erb +3 -0
- data/lib/exceptify/views/exceptify/background_exceptify.html.erb +53 -0
- data/lib/exceptify/views/exceptify/background_exceptify.text.erb +14 -0
- data/lib/exceptify/views/exceptify/exceptify.html.erb +52 -0
- data/lib/exceptify/views/exceptify/exceptify.text.erb +24 -0
- data/lib/exceptify/webhook_notifier.rb +63 -0
- data/lib/exceptify.rb +177 -0
- data/lib/generators/exceptify/install_generator.rb +24 -0
- data/lib/generators/exceptify/templates/exceptify.rb.erb +44 -0
- metadata +364 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "resque/failure/base"
|
|
4
|
+
require "exceptify"
|
|
5
|
+
|
|
6
|
+
module Exceptify
|
|
7
|
+
class Resque < Resque::Failure::Base
|
|
8
|
+
def self.count
|
|
9
|
+
::Resque::Stat[:failed]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def save
|
|
13
|
+
data = {
|
|
14
|
+
error_class: exception.class.name,
|
|
15
|
+
error_message: exception.message,
|
|
16
|
+
failed_at: Time.now.to_s,
|
|
17
|
+
payload: payload,
|
|
18
|
+
queue: queue,
|
|
19
|
+
worker: worker.to_s
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Exceptify.notify_exception(exception, data: {resque: data})
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sidekiq"
|
|
4
|
+
require "exceptify"
|
|
5
|
+
|
|
6
|
+
::Sidekiq.configure_server do |config|
|
|
7
|
+
config.error_handlers << proc do |ex, context, config|
|
|
8
|
+
# Before Sidekiq 7.1.5 the config was not passed to the proc
|
|
9
|
+
if config
|
|
10
|
+
Exceptify.notify_exception(ex, data: {sidekiq: {context: context, config: config}})
|
|
11
|
+
else
|
|
12
|
+
Exceptify.notify_exception(ex, data: {sidekiq: context})
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exceptify
|
|
4
|
+
class SlackNotifier < BaseNotifier
|
|
5
|
+
include Exceptify::BacktraceCleaner
|
|
6
|
+
|
|
7
|
+
attr_accessor :notifier
|
|
8
|
+
|
|
9
|
+
def initialize(options)
|
|
10
|
+
options = options.dup
|
|
11
|
+
fail_silently = options.delete(:fail_silently) { false }
|
|
12
|
+
injected_notifier = options.delete(:notifier)
|
|
13
|
+
super()
|
|
14
|
+
self.base_options = options
|
|
15
|
+
|
|
16
|
+
@ignore_data_if = options[:ignore_data_if]
|
|
17
|
+
@backtrace_lines = options.fetch(:backtrace_lines, 10)
|
|
18
|
+
@additional_fields = options[:additional_fields]
|
|
19
|
+
@message_opts = options.fetch(:additional_parameters, {}).dup
|
|
20
|
+
@color = @message_opts.delete(:color) { "danger" }
|
|
21
|
+
|
|
22
|
+
@notifier = injected_notifier || build_notifier(options)
|
|
23
|
+
rescue => e
|
|
24
|
+
raise unless fail_silently
|
|
25
|
+
|
|
26
|
+
log_configuration_error(e)
|
|
27
|
+
@notifier = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(exception, options = {})
|
|
31
|
+
notification = Notification.new(exception, options, backtrace_cleaner: self)
|
|
32
|
+
clean_message = exception.message.tr("`", "'")
|
|
33
|
+
attchs = attchs(notification, clean_message)
|
|
34
|
+
|
|
35
|
+
return unless valid?
|
|
36
|
+
|
|
37
|
+
args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)]
|
|
38
|
+
send_notice(*args) do |_msg, message_opts|
|
|
39
|
+
message_opts[:channel] = options[:channel] if options.key?(:channel)
|
|
40
|
+
|
|
41
|
+
@notifier.ping "", message_opts
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def valid?
|
|
48
|
+
!@notifier.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def deep_reject(hash, block)
|
|
52
|
+
hash.each do |k, v|
|
|
53
|
+
deep_reject(v, block) if v.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
hash.delete(k) if block.call(k, v)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def build_notifier(options)
|
|
62
|
+
webhook_url = options[:webhook_url]
|
|
63
|
+
raise ArgumentError, "You must provide 'webhook_url' option" if blank?(webhook_url)
|
|
64
|
+
unless defined?(::Slack::Notifier)
|
|
65
|
+
raise ArgumentError, "Slack notifier requires the 'slack-notifier' gem"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Slack::Notifier.new(webhook_url, options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def blank?(value)
|
|
72
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def log_configuration_error(error)
|
|
76
|
+
Exceptify.logger&.warn(
|
|
77
|
+
"Slack notifier disabled: #{error.class}: #{error.message}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def attchs(notification, clean_message)
|
|
82
|
+
text, data = information_from_notification(notification)
|
|
83
|
+
backtrace = notification.backtrace
|
|
84
|
+
fields = fields(notification, clean_message, backtrace, data)
|
|
85
|
+
|
|
86
|
+
[color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def information_from_notification(notification)
|
|
90
|
+
errors_count = notification.options[:accumulated_errors_count].to_i
|
|
91
|
+
exception_class = notification.exception.class
|
|
92
|
+
|
|
93
|
+
measure_word = if errors_count > 1
|
|
94
|
+
errors_count
|
|
95
|
+
else
|
|
96
|
+
/^[aeiou]/i.match?(exception_class.to_s) ? "An" : "A"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
exception_name = "*#{measure_word}* `#{exception_class}`"
|
|
100
|
+
env = notification.env
|
|
101
|
+
data = notification.data
|
|
102
|
+
|
|
103
|
+
notification.options[:headers] ||= {}
|
|
104
|
+
notification.options[:headers]["Content-Type"] = "application/json"
|
|
105
|
+
|
|
106
|
+
if env.nil?
|
|
107
|
+
text = "#{exception_name} *occured in background*\n"
|
|
108
|
+
else
|
|
109
|
+
kontroller = env["action_controller.instance"]
|
|
110
|
+
request = "#{env["REQUEST_METHOD"]} <#{env["REQUEST_URI"]}>"
|
|
111
|
+
text = "#{exception_name} *occurred while* `#{request}`"
|
|
112
|
+
text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller
|
|
113
|
+
text += "\n"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
[text, data]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def fields(notification, clean_message, backtrace, data)
|
|
120
|
+
fields = [
|
|
121
|
+
{title: "Exception", value: clean_message},
|
|
122
|
+
{title: "Hostname", value: notification.hostname}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
unless backtrace.empty?
|
|
126
|
+
formatted_backtrace = "```#{backtrace.first(@backtrace_lines).join("\n")}```"
|
|
127
|
+
fields << {title: "Backtrace", value: formatted_backtrace}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless data.empty?
|
|
131
|
+
deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc)
|
|
132
|
+
data_string = data.map { |k, v| "#{k}: #{v}" }.join("\n")
|
|
133
|
+
fields << {title: "Data", value: "```#{data_string}```"}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
fields.concat(@additional_fields) if @additional_fields
|
|
137
|
+
|
|
138
|
+
fields
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exceptify
|
|
4
|
+
class SnsNotifier < BaseNotifier
|
|
5
|
+
def initialize(options)
|
|
6
|
+
options = options.dup
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
@notifier = options.delete(:client) || build_client(options)
|
|
10
|
+
@options = default_options.merge(options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(exception, custom_opts = {})
|
|
14
|
+
custom_options = options.merge(custom_opts)
|
|
15
|
+
notification = Notification.new(exception, custom_options)
|
|
16
|
+
|
|
17
|
+
subject = build_subject(notification, custom_options)
|
|
18
|
+
message = build_message(notification, custom_options)
|
|
19
|
+
|
|
20
|
+
notifier.publish(
|
|
21
|
+
topic_arn: custom_options[:topic_arn],
|
|
22
|
+
message: message,
|
|
23
|
+
subject: subject
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :notifier, :options
|
|
30
|
+
|
|
31
|
+
def build_client(options)
|
|
32
|
+
raise ArgumentError, "You must provide 'region' option" unless options[:region]
|
|
33
|
+
raise ArgumentError, "You must provide 'access_key_id' option" unless options[:access_key_id]
|
|
34
|
+
raise ArgumentError, "You must provide 'secret_access_key' option" unless options[:secret_access_key]
|
|
35
|
+
raise ArgumentError, "SNS notifier requires the 'aws-sdk-sns' gem" unless defined?(::Aws::SNS::Client)
|
|
36
|
+
|
|
37
|
+
Aws::SNS::Client.new(
|
|
38
|
+
region: options[:region],
|
|
39
|
+
access_key_id: options[:access_key_id],
|
|
40
|
+
secret_access_key: options[:secret_access_key]
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_subject(notification, options)
|
|
45
|
+
subject =
|
|
46
|
+
"#{options[:sns_prefix]} - #{accumulated_exception_name(notification, options)} occurred"
|
|
47
|
+
(subject.length > 120) ? subject[0...120] + "..." : subject
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_message(notification, options)
|
|
51
|
+
exception = notification.exception
|
|
52
|
+
exception_name = accumulated_exception_name(notification, options)
|
|
53
|
+
|
|
54
|
+
if notification.env.nil?
|
|
55
|
+
text = "#{exception_name} occured in background\n"
|
|
56
|
+
data = notification.data
|
|
57
|
+
else
|
|
58
|
+
env = notification.env
|
|
59
|
+
|
|
60
|
+
kontroller = env["action_controller.instance"]
|
|
61
|
+
data = notification.data
|
|
62
|
+
request = "#{env["REQUEST_METHOD"]} <#{env["REQUEST_URI"]}>"
|
|
63
|
+
|
|
64
|
+
text = "#{exception_name} occurred while #{request}"
|
|
65
|
+
text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
text += "Exception: #{exception.message}\n"
|
|
69
|
+
text += "Hostname: #{notification.hostname}\n"
|
|
70
|
+
text += "Data: #{data}\n"
|
|
71
|
+
|
|
72
|
+
return text if notification.backtrace.empty?
|
|
73
|
+
|
|
74
|
+
formatted_backtrace = notification.backtrace.first(options[:backtrace_lines]).join("\n").to_s
|
|
75
|
+
text + "Backtrace:\n#{formatted_backtrace}\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def accumulated_exception_name(notification, options)
|
|
79
|
+
errors_count = options[:accumulated_errors_count].to_i
|
|
80
|
+
exception = notification.exception
|
|
81
|
+
|
|
82
|
+
measure_word = if errors_count > 1
|
|
83
|
+
errors_count
|
|
84
|
+
else
|
|
85
|
+
/^[aeiou]/i.match?(exception.class.to_s) ? "An" : "A"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
"#{measure_word} #{exception.class}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_options
|
|
92
|
+
{
|
|
93
|
+
sns_prefix: "[ERROR]",
|
|
94
|
+
backtrace_lines: 10
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require "active_support/notifications"
|
|
5
|
+
require "exceptify"
|
|
6
|
+
|
|
7
|
+
module Exceptify
|
|
8
|
+
module SolidQueue
|
|
9
|
+
ADAPTER_NAME = "solid_queue"
|
|
10
|
+
EVENT_NAME = "perform.active_job"
|
|
11
|
+
JOB_ATTRIBUTES = %i[
|
|
12
|
+
job_id
|
|
13
|
+
provider_job_id
|
|
14
|
+
queue_name
|
|
15
|
+
priority
|
|
16
|
+
arguments
|
|
17
|
+
executions
|
|
18
|
+
exception_executions
|
|
19
|
+
locale
|
|
20
|
+
timezone
|
|
21
|
+
enqueued_at
|
|
22
|
+
scheduled_at
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def install
|
|
27
|
+
return if installed?
|
|
28
|
+
|
|
29
|
+
@subscription = ActiveSupport::Notifications.subscribe(EVENT_NAME) do |*args|
|
|
30
|
+
notify(ActiveSupport::Notifications::Event.new(*args))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def installed?
|
|
35
|
+
!!@subscription
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def notify(event)
|
|
39
|
+
exception = event.payload[:exception_object]
|
|
40
|
+
job = event.payload[:job]
|
|
41
|
+
|
|
42
|
+
return unless exception && solid_queue_job?(job)
|
|
43
|
+
|
|
44
|
+
Exceptify.notify_exception(exception, data: {solid_queue: job_data(job)})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def solid_queue_job?(job)
|
|
50
|
+
queue_adapter_name(job) == ADAPTER_NAME
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def queue_adapter_name(job)
|
|
54
|
+
job.class.queue_adapter_name if job && job.class.respond_to?(:queue_adapter_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def job_data(job)
|
|
58
|
+
{adapter: queue_adapter_name(job), job_class: job.class.name}.tap do |data|
|
|
59
|
+
JOB_ATTRIBUTES.each do |attribute|
|
|
60
|
+
data[attribute] = job.public_send(attribute) if job.respond_to?(attribute)
|
|
61
|
+
end
|
|
62
|
+
end.compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Exceptify::SolidQueue.install
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch"
|
|
4
|
+
require "active_support/core_ext/time"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Exceptify
|
|
8
|
+
class TeamsNotifier < BaseNotifier
|
|
9
|
+
include Exceptify::BacktraceCleaner
|
|
10
|
+
|
|
11
|
+
class MissingController
|
|
12
|
+
def method_missing(*args, &block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def respond_to_missing?(*args)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_accessor :httparty
|
|
20
|
+
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
options = options.dup
|
|
23
|
+
@httparty = options.delete(:http_client) || HTTParty
|
|
24
|
+
super()
|
|
25
|
+
self.base_options = options
|
|
26
|
+
@default_options = options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(exception, options = {})
|
|
30
|
+
@options = options.merge(@default_options)
|
|
31
|
+
@exception = exception
|
|
32
|
+
@backtrace = exception.backtrace ? clean_backtrace(exception) : nil
|
|
33
|
+
|
|
34
|
+
@env = @options.delete(:env)
|
|
35
|
+
|
|
36
|
+
@application_name = @options.delete(:app_name) || rails_app_name
|
|
37
|
+
@gitlab_url = @options.delete(:git_url)
|
|
38
|
+
@jira_url = @options.delete(:jira_url)
|
|
39
|
+
|
|
40
|
+
@webhook_url = @options.delete(:webhook_url)
|
|
41
|
+
raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url
|
|
42
|
+
|
|
43
|
+
if @env.nil?
|
|
44
|
+
@controller = @request_items = nil
|
|
45
|
+
else
|
|
46
|
+
@controller = @env["action_controller.instance"] || MissingController.new
|
|
47
|
+
@additional_exception_data = @env["exceptify.exception_data"]
|
|
48
|
+
request = ActionDispatch::Request.new(@env)
|
|
49
|
+
|
|
50
|
+
@request_items = {url: request.original_url,
|
|
51
|
+
http_method: request.method,
|
|
52
|
+
ip_address: request.remote_ip,
|
|
53
|
+
parameters: request.filtered_parameters,
|
|
54
|
+
timestamp: Time.current}
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
payload = message_text
|
|
59
|
+
|
|
60
|
+
@options[:body] = payload.to_json
|
|
61
|
+
@options[:headers] ||= {}
|
|
62
|
+
@options[:headers]["Content-Type"] = "application/json"
|
|
63
|
+
@options[:debug_output] = $stdout
|
|
64
|
+
|
|
65
|
+
@httparty.post(@webhook_url, @options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def message_text
|
|
71
|
+
text = {
|
|
72
|
+
"@type" => "MessageCard",
|
|
73
|
+
"@context" => "http://schema.org/extensions",
|
|
74
|
+
"summary" => "#{@application_name} Exception Alert",
|
|
75
|
+
"title" => "⚠️ Exception Occurred in #{env_name} ⚠️",
|
|
76
|
+
"sections" => [
|
|
77
|
+
{
|
|
78
|
+
"activityTitle" => activity_title,
|
|
79
|
+
"activitySubtitle" => @exception.message.to_s
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"potentialAction" => []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
text["sections"].push details
|
|
86
|
+
text["potentialAction"].push gitlab_view_link unless @gitlab_url.nil?
|
|
87
|
+
text["potentialAction"].push gitlab_issue_link unless @gitlab_url.nil?
|
|
88
|
+
text["potentialAction"].push jira_issue_link unless @jira_url.nil?
|
|
89
|
+
|
|
90
|
+
text
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def details
|
|
94
|
+
details = {
|
|
95
|
+
"title" => "Details",
|
|
96
|
+
"facts" => []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
details["facts"].push message_request unless @request_items.nil?
|
|
100
|
+
details["facts"].push message_backtrace unless @backtrace.nil?
|
|
101
|
+
details["facts"].push additional_exception_data unless @additional_exception_data.nil?
|
|
102
|
+
details
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def activity_title
|
|
106
|
+
errors_count = @options[:accumulated_errors_count].to_i
|
|
107
|
+
|
|
108
|
+
"#{(errors_count > 1) ? errors_count : "A"} *#{@exception.class}* occurred" +
|
|
109
|
+
(@controller ? " in *#{controller_and_method}*." : ".")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def message_request
|
|
113
|
+
{
|
|
114
|
+
"name" => "Request",
|
|
115
|
+
"value" => "#{hash_presentation(@request_items)}\n "
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def message_backtrace(size = 3)
|
|
120
|
+
text = []
|
|
121
|
+
size = (@backtrace.size < size) ? @backtrace.size : size
|
|
122
|
+
text << "```"
|
|
123
|
+
size.times { |i| text << "* " + @backtrace[i] }
|
|
124
|
+
text << "```"
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
"name" => "Backtrace",
|
|
128
|
+
"value" => text.join(" \n").to_s
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def additional_exception_data
|
|
133
|
+
{
|
|
134
|
+
"name" => "Data",
|
|
135
|
+
"value" => "`#{@additional_exception_data}`\n "
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def gitlab_view_link
|
|
140
|
+
{
|
|
141
|
+
"@type" => "ViewAction",
|
|
142
|
+
"name" => "\u{1F98A} View in GitLab",
|
|
143
|
+
"target" => [
|
|
144
|
+
"#{@gitlab_url}/#{@application_name}"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def gitlab_issue_link
|
|
150
|
+
link = [@gitlab_url, @application_name, "issues", "new"].join("/")
|
|
151
|
+
params = {
|
|
152
|
+
"issue[title]" => ["[BUG] Error 500 :",
|
|
153
|
+
controller_and_method,
|
|
154
|
+
"(#{@exception.class})",
|
|
155
|
+
@exception.message].compact.join(" ")
|
|
156
|
+
}.to_query
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
"@type" => "ViewAction",
|
|
160
|
+
"name" => "\u{1F98A} Create Issue in GitLab",
|
|
161
|
+
"target" => [
|
|
162
|
+
"#{link}/?#{params}"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def jira_issue_link
|
|
168
|
+
{
|
|
169
|
+
"@type" => "ViewAction",
|
|
170
|
+
"name" => "🐞 Create Issue in Jira",
|
|
171
|
+
"target" => [
|
|
172
|
+
"#{@jira_url}/secure/CreateIssue!default.jspa"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def controller_and_method
|
|
178
|
+
if @controller
|
|
179
|
+
"#{@controller.controller_name}##{@controller.action_name}"
|
|
180
|
+
else
|
|
181
|
+
""
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def hash_presentation(hash)
|
|
186
|
+
text = []
|
|
187
|
+
|
|
188
|
+
hash.each do |key, value|
|
|
189
|
+
text << "* **#{key}** : `#{value}`"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
text.join(" \n")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def rails_app_name
|
|
196
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
|
197
|
+
|
|
198
|
+
if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new("6.0")
|
|
199
|
+
Rails.application.class.module_parent_name.underscore
|
|
200
|
+
else
|
|
201
|
+
Rails.application.class.parent_name.underscore
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def env_name
|
|
206
|
+
Rails.env if defined?(Rails) && Rails.respond_to?(:env)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= raw @backtrace.join("\n") %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* data: <%= raw PP.pp(@data, +"") %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% filtered_env = @request.filtered_env -%>
|
|
2
|
+
<% max = filtered_env.keys.map(&:to_s).max { |a, b| a.length <=> b.length } -%>
|
|
3
|
+
<% filtered_env.keys.map(&:to_s).sort.each do |key| -%>
|
|
4
|
+
* <%= raw safe_encode("%-*s: %s" % [max.length, key, inspect_object(filtered_env[key])]).strip %>
|
|
5
|
+
<% end -%>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<ul style="list-style: none">
|
|
2
|
+
<li>
|
|
3
|
+
<strong>URL:</strong>
|
|
4
|
+
<span><%= @request.url %></span>
|
|
5
|
+
</li>
|
|
6
|
+
<li>
|
|
7
|
+
<strong>HTTP Method:</strong>
|
|
8
|
+
<span><%= @request.request_method %></span>
|
|
9
|
+
</li>
|
|
10
|
+
<li>
|
|
11
|
+
<strong>IP Address:</strong>
|
|
12
|
+
<span><%= @request.remote_ip %></span>
|
|
13
|
+
</li>
|
|
14
|
+
<li>
|
|
15
|
+
<strong>Parameters:</strong>
|
|
16
|
+
<span><%= @request.filtered_parameters.inspect %></span>
|
|
17
|
+
</li>
|
|
18
|
+
<li>
|
|
19
|
+
<strong>Timestamp:</strong>
|
|
20
|
+
<span><%= @timestamp %></span>
|
|
21
|
+
</li>
|
|
22
|
+
<li>
|
|
23
|
+
<strong>Server:</strong>
|
|
24
|
+
<span><%= Socket.gethostname %></span>
|
|
25
|
+
</li>
|
|
26
|
+
<% if defined?(Rails) && Rails.respond_to?(:root) %>
|
|
27
|
+
<li>
|
|
28
|
+
<strong>Rails root:</strong>
|
|
29
|
+
<span><%= Rails.root %></span>
|
|
30
|
+
</li>
|
|
31
|
+
<% end %>
|
|
32
|
+
<li>
|
|
33
|
+
<strong>Process:</strong>
|
|
34
|
+
<span><%= $$ %></span>
|
|
35
|
+
</li>
|
|
36
|
+
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
* URL : <%= raw safe_encode @request.url %>
|
|
2
|
+
* HTTP Method: <%= raw @request.request_method %>
|
|
3
|
+
* IP address : <%= raw @request.remote_ip %>
|
|
4
|
+
* Parameters : <%= raw safe_encode @request.filtered_parameters.inspect %>
|
|
5
|
+
* Timestamp : <%= raw @timestamp %>
|
|
6
|
+
* Server : <%= raw Socket.gethostname %>
|
|
7
|
+
<% if defined?(Rails) && Rails.respond_to?(:root) %>
|
|
8
|
+
* Rails root : <%= raw Rails.root %>
|
|
9
|
+
<% end %>
|
|
10
|
+
* Process: <%= raw $$ %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<ul style="list-style: none">
|
|
2
|
+
<li>
|
|
3
|
+
<strong>session_id: </strong>
|
|
4
|
+
<span><%= @request.ssl? ? "[FILTERED]" : (@request.session['session_id'] || (@request.env["rack.session.options"] and @request.env["rack.session.options"][:id]).inspect) %></span>
|
|
5
|
+
</li>
|
|
6
|
+
<li>
|
|
7
|
+
<strong>data: </strong>
|
|
8
|
+
<span><%= PP.pp(@request.session.to_hash, +"") %></span>
|
|
9
|
+
</li>
|
|
10
|
+
</ul>
|