exception_notification 4.2.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +5 -5
  2. data/Appraisals +4 -3
  3. data/CHANGELOG.rdoc +57 -1
  4. data/CONTRIBUTING.md +21 -2
  5. data/Gemfile +3 -1
  6. data/README.md +106 -789
  7. data/Rakefile +4 -2
  8. data/docs/notifiers/campfire.md +50 -0
  9. data/docs/notifiers/custom.md +42 -0
  10. data/docs/notifiers/datadog.md +51 -0
  11. data/docs/notifiers/email.md +195 -0
  12. data/docs/notifiers/google_chat.md +31 -0
  13. data/docs/notifiers/hipchat.md +66 -0
  14. data/docs/notifiers/irc.md +97 -0
  15. data/docs/notifiers/mattermost.md +115 -0
  16. data/docs/notifiers/slack.md +161 -0
  17. data/docs/notifiers/sns.md +37 -0
  18. data/docs/notifiers/teams.md +54 -0
  19. data/docs/notifiers/webhook.md +60 -0
  20. data/examples/sample_app.rb +56 -0
  21. data/examples/sinatra/Gemfile +8 -6
  22. data/examples/sinatra/config.ru +3 -1
  23. data/examples/sinatra/sinatra_app.rb +19 -11
  24. data/exception_notification.gemspec +30 -23
  25. data/gemfiles/rails4_0.gemfile +1 -2
  26. data/gemfiles/rails4_1.gemfile +1 -2
  27. data/gemfiles/rails4_2.gemfile +1 -2
  28. data/gemfiles/rails5_0.gemfile +1 -2
  29. data/gemfiles/rails5_1.gemfile +7 -0
  30. data/gemfiles/rails5_2.gemfile +7 -0
  31. data/gemfiles/rails6_0.gemfile +7 -0
  32. data/lib/exception_notification.rb +3 -0
  33. data/lib/exception_notification/rack.rb +34 -27
  34. data/lib/exception_notification/rails.rb +3 -0
  35. data/lib/exception_notification/resque.rb +10 -10
  36. data/lib/exception_notification/sidekiq.rb +10 -12
  37. data/lib/exception_notification/version.rb +5 -0
  38. data/lib/exception_notifier.rb +79 -11
  39. data/lib/exception_notifier/base_notifier.rb +10 -5
  40. data/lib/exception_notifier/campfire_notifier.rb +14 -9
  41. data/lib/exception_notifier/datadog_notifier.rb +156 -0
  42. data/lib/exception_notifier/email_notifier.rb +78 -87
  43. data/lib/exception_notifier/google_chat_notifier.rb +44 -0
  44. data/lib/exception_notifier/hipchat_notifier.rb +16 -10
  45. data/lib/exception_notifier/irc_notifier.rb +38 -31
  46. data/lib/exception_notifier/mattermost_notifier.rb +54 -131
  47. data/lib/exception_notifier/modules/backtrace_cleaner.rb +2 -2
  48. data/lib/exception_notifier/modules/error_grouping.rb +87 -0
  49. data/lib/exception_notifier/modules/formatter.rb +121 -0
  50. data/lib/exception_notifier/notifier.rb +9 -6
  51. data/lib/exception_notifier/slack_notifier.rb +75 -32
  52. data/lib/exception_notifier/sns_notifier.rb +86 -0
  53. data/lib/exception_notifier/teams_notifier.rb +200 -0
  54. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +1 -1
  55. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
  56. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +1 -1
  57. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
  58. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +2 -4
  59. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +2 -2
  60. data/lib/exception_notifier/webhook_notifier.rb +19 -16
  61. data/lib/generators/exception_notification/install_generator.rb +11 -5
  62. data/lib/generators/exception_notification/templates/{exception_notification.rb → exception_notification.rb.erb} +14 -12
  63. data/test/exception_notification/rack_test.rb +90 -4
  64. data/test/exception_notification/resque_test.rb +54 -0
  65. data/test/exception_notifier/campfire_notifier_test.rb +66 -39
  66. data/test/exception_notifier/datadog_notifier_test.rb +153 -0
  67. data/test/exception_notifier/email_notifier_test.rb +301 -145
  68. data/test/exception_notifier/google_chat_notifier_test.rb +185 -0
  69. data/test/exception_notifier/hipchat_notifier_test.rb +112 -65
  70. data/test/exception_notifier/irc_notifier_test.rb +48 -30
  71. data/test/exception_notifier/mattermost_notifier_test.rb +218 -55
  72. data/test/exception_notifier/modules/error_grouping_test.rb +167 -0
  73. data/test/exception_notifier/modules/formatter_test.rb +152 -0
  74. data/test/exception_notifier/sidekiq_test.rb +9 -6
  75. data/test/exception_notifier/slack_notifier_test.rb +109 -59
  76. data/test/exception_notifier/sns_notifier_test.rb +123 -0
  77. data/test/exception_notifier/teams_notifier_test.rb +92 -0
  78. data/test/exception_notifier/webhook_notifier_test.rb +68 -38
  79. data/test/exception_notifier_test.rb +220 -37
  80. data/test/support/exception_notifier_helper.rb +14 -0
  81. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.html.erb +0 -0
  82. data/test/{dummy/app → support}/views/exception_notifier/_new_bkg_section.text.erb +0 -0
  83. data/test/{dummy/app → support}/views/exception_notifier/_new_section.html.erb +0 -0
  84. data/test/{dummy/app → support}/views/exception_notifier/_new_section.text.erb +0 -0
  85. data/test/test_helper.rb +14 -13
  86. metadata +154 -162
  87. data/test/dummy/.gitignore +0 -4
  88. data/test/dummy/Rakefile +0 -7
  89. data/test/dummy/app/controllers/application_controller.rb +0 -3
  90. data/test/dummy/app/controllers/posts_controller.rb +0 -30
  91. data/test/dummy/app/helpers/application_helper.rb +0 -2
  92. data/test/dummy/app/helpers/posts_helper.rb +0 -2
  93. data/test/dummy/app/models/post.rb +0 -2
  94. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  95. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  96. data/test/dummy/app/views/posts/new.html.erb +0 -0
  97. data/test/dummy/app/views/posts/show.html.erb +0 -0
  98. data/test/dummy/config.ru +0 -4
  99. data/test/dummy/config/application.rb +0 -42
  100. data/test/dummy/config/boot.rb +0 -6
  101. data/test/dummy/config/database.yml +0 -22
  102. data/test/dummy/config/environment.rb +0 -17
  103. data/test/dummy/config/environments/development.rb +0 -25
  104. data/test/dummy/config/environments/production.rb +0 -50
  105. data/test/dummy/config/environments/test.rb +0 -38
  106. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  107. data/test/dummy/config/initializers/inflections.rb +0 -10
  108. data/test/dummy/config/initializers/mime_types.rb +0 -5
  109. data/test/dummy/config/initializers/secret_token.rb +0 -8
  110. data/test/dummy/config/initializers/session_store.rb +0 -8
  111. data/test/dummy/config/locales/en.yml +0 -5
  112. data/test/dummy/config/routes.rb +0 -3
  113. data/test/dummy/db/migrate/20110729022608_create_posts.rb +0 -15
  114. data/test/dummy/db/schema.rb +0 -24
  115. data/test/dummy/db/seeds.rb +0 -7
  116. data/test/dummy/lib/tasks/.gitkeep +0 -0
  117. data/test/dummy/public/404.html +0 -26
  118. data/test/dummy/public/422.html +0 -26
  119. data/test/dummy/public/500.html +0 -26
  120. data/test/dummy/public/favicon.ico +0 -0
  121. data/test/dummy/public/images/rails.png +0 -0
  122. data/test/dummy/public/index.html +0 -239
  123. data/test/dummy/public/javascripts/application.js +0 -2
  124. data/test/dummy/public/javascripts/controls.js +0 -965
  125. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  126. data/test/dummy/public/javascripts/effects.js +0 -1123
  127. data/test/dummy/public/javascripts/prototype.js +0 -6001
  128. data/test/dummy/public/javascripts/rails.js +0 -191
  129. data/test/dummy/public/robots.txt +0 -5
  130. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  131. data/test/dummy/public/stylesheets/scaffold.css +0 -56
  132. data/test/dummy/script/rails +0 -6
  133. data/test/dummy/test/functional/posts_controller_test.rb +0 -218
  134. data/test/dummy/test/test_helper.rb +0 -7
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/time'
4
+ require 'action_dispatch'
5
+
6
+ module ExceptionNotifier
7
+ class Formatter
8
+ include ExceptionNotifier::BacktraceCleaner
9
+
10
+ attr_reader :app_name
11
+
12
+ def initialize(exception, opts = {})
13
+ @exception = exception
14
+
15
+ @env = opts[:env]
16
+ @errors_count = opts[:accumulated_errors_count].to_i
17
+ @app_name = opts[:app_name] || rails_app_name
18
+ end
19
+
20
+ #
21
+ # :warning: Error occurred in production :warning:
22
+ # :warning: Error occurred :warning:
23
+ #
24
+ def title
25
+ env = Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
26
+
27
+ if env
28
+ "⚠️ Error occurred in #{env} ⚠️"
29
+ else
30
+ '⚠️ Error occurred ⚠️'
31
+ end
32
+ end
33
+
34
+ #
35
+ # A *NoMethodError* occurred.
36
+ # 3 *NoMethodError* occurred.
37
+ # A *NoMethodError* occurred in *home#index*.
38
+ #
39
+ def subtitle
40
+ errors_text = if errors_count > 1
41
+ errors_count
42
+ else
43
+ exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
44
+ end
45
+
46
+ in_action = " in *#{controller_and_action}*" if controller
47
+
48
+ "#{errors_text} *#{exception.class}* occurred#{in_action}."
49
+ end
50
+
51
+ #
52
+ #
53
+ # *Request:*
54
+ # ```
55
+ # * url : https://www.example.com/
56
+ # * http_method : GET
57
+ # * ip_address : 127.0.0.1
58
+ # * parameters : {"controller"=>"home", "action"=>"index"}
59
+ # * timestamp : 2019-01-01 00:00:00 UTC
60
+ # ```
61
+ #
62
+ def request_message
63
+ request = ActionDispatch::Request.new(env) if env
64
+ return unless request
65
+
66
+ [
67
+ '```',
68
+ "* url : #{request.original_url}",
69
+ "* http_method : #{request.method}",
70
+ "* ip_address : #{request.remote_ip}",
71
+ "* parameters : #{request.filtered_parameters}",
72
+ "* timestamp : #{Time.current}",
73
+ '```'
74
+ ].join("\n")
75
+ end
76
+
77
+ #
78
+ #
79
+ # *Backtrace:*
80
+ # ```
81
+ # * app/controllers/my_controller.rb:99:in `specific_function'
82
+ # * app/controllers/my_controller.rb:70:in `specific_param'
83
+ # * app/controllers/my_controller.rb:53:in `my_controller_params'
84
+ # ```
85
+ #
86
+ def backtrace_message
87
+ backtrace = exception.backtrace ? clean_backtrace(exception) : nil
88
+
89
+ return unless backtrace
90
+
91
+ text = []
92
+
93
+ text << '```'
94
+ backtrace.first(3).each { |line| text << "* #{line}" }
95
+ text << '```'
96
+
97
+ text.join("\n")
98
+ end
99
+
100
+ #
101
+ # home#index
102
+ #
103
+ def controller_and_action
104
+ "#{controller.controller_name}##{controller.action_name}" if controller
105
+ end
106
+
107
+ private
108
+
109
+ attr_reader :exception, :env, :errors_count
110
+
111
+ def rails_app_name
112
+ return unless defined?(::Rails) && ::Rails.respond_to?(:application)
113
+
114
+ Rails.application.class.parent_name.underscore
115
+ end
116
+
117
+ def controller
118
+ env['action_controller.instance'] if env
119
+ end
120
+ end
121
+ end
@@ -1,15 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/deprecation'
2
4
 
3
5
  module ExceptionNotifier
4
6
  class Notifier
5
-
6
- def self.exception_notification(env, exception, options={})
7
- ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options.merge(:env => env))."
8
- ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(:env => env))
7
+ def self.exception_notification(env, exception, options = {})
8
+ ActiveSupport::Deprecation.warn(
9
+ 'Please use ExceptionNotifier.notify_exception(exception, options.merge(env: env)).'
10
+ )
11
+ ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(env: env))
9
12
  end
10
13
 
11
- def self.background_exception_notification(exception, options={})
12
- ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options)."
14
+ def self.background_exception_notification(exception, options = {})
15
+ ActiveSupport::Deprecation.warn 'Please use ExceptionNotifier.notify_exception(exception, options).'
13
16
  ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options)
14
17
  end
15
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExceptionNotifier
2
4
  class SlackNotifier < BaseNotifier
3
5
  include ExceptionNotifier::BacktraceCleaner
@@ -8,43 +10,29 @@ module ExceptionNotifier
8
10
  super
9
11
  begin
10
12
  @ignore_data_if = options[:ignore_data_if]
13
+ @backtrace_lines = options.fetch(:backtrace_lines, 10)
14
+ @additional_fields = options[:additional_fields]
11
15
 
12
16
  webhook_url = options.fetch(:webhook_url)
13
17
  @message_opts = options.fetch(:additional_parameters, {})
18
+ @color = @message_opts.delete(:color) { 'danger' }
14
19
  @notifier = Slack::Notifier.new webhook_url, options
15
- rescue
20
+ rescue StandardError
16
21
  @notifier = nil
17
22
  end
18
23
  end
19
24
 
20
- def call(exception, options={})
21
- env = options[:env] || {}
22
- title = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
23
- data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
24
- text = "*An exception occurred while doing*: `#{title}`\n"
25
-
26
- clean_message = exception.message.gsub("`", "'")
27
- fields = [ { title: 'Exception', value: clean_message} ]
28
-
29
- fields.push({ title: 'Hostname', value: Socket.gethostname })
30
-
31
- if exception.backtrace
32
- formatted_backtrace = "```#{exception.backtrace.join("\n")}```"
33
- fields.push({ title: 'Backtrace', value: formatted_backtrace })
34
- end
25
+ def call(exception, options = {})
26
+ clean_message = exception.message.tr('`', "'")
27
+ attchs = attchs(exception, clean_message, options)
35
28
 
36
- unless data.empty?
37
- deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc)
38
- data_string = data.map{|k,v| "#{k}: #{v}"}.join("\n")
39
- fields.push({ title: 'Data', value: "```#{data_string}```" })
40
- end
29
+ return unless valid?
41
30
 
42
- attchs = [color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields)]
31
+ args = [exception, options, clean_message, @message_opts.merge(attachments: attchs)]
32
+ send_notice(*args) do |_msg, message_opts|
33
+ message_opts[:channel] = options[:channel] if options.key?(:channel)
43
34
 
44
- if valid?
45
- send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts|
46
- @notifier.ping '', message_opts
47
- end
35
+ @notifier.ping '', message_opts
48
36
  end
49
37
  end
50
38
 
@@ -56,15 +44,70 @@ module ExceptionNotifier
56
44
 
57
45
  def deep_reject(hash, block)
58
46
  hash.each do |k, v|
59
- if v.is_a?(Hash)
60
- deep_reject(v, block)
61
- end
47
+ deep_reject(v, block) if v.is_a?(Hash)
62
48
 
63
- if block.call(k, v)
64
- hash.delete(k)
65
- end
49
+ hash.delete(k) if block.call(k, v)
66
50
  end
67
51
  end
68
52
 
53
+ private
54
+
55
+ def attchs(exception, clean_message, options)
56
+ text, data = information_from_options(exception.class, options)
57
+ backtrace = clean_backtrace(exception) if exception.backtrace
58
+ fields = fields(clean_message, backtrace, data)
59
+
60
+ [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]]
61
+ end
62
+
63
+ def information_from_options(exception_class, options)
64
+ errors_count = options[:accumulated_errors_count].to_i
65
+
66
+ measure_word = if errors_count > 1
67
+ errors_count
68
+ else
69
+ exception_class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
70
+ end
71
+
72
+ exception_name = "*#{measure_word}* `#{exception_class}`"
73
+ env = options[:env]
74
+
75
+ if env.nil?
76
+ data = options[:data] || {}
77
+ text = "#{exception_name} *occured in background*\n"
78
+ else
79
+ data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
80
+
81
+ kontroller = env['action_controller.instance']
82
+ request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
83
+ text = "#{exception_name} *occurred while* `#{request}`"
84
+ text += " *was processed by* `#{kontroller.controller_name}##{kontroller.action_name}`" if kontroller
85
+ text += "\n"
86
+ end
87
+
88
+ [text, data]
89
+ end
90
+
91
+ def fields(clean_message, backtrace, data)
92
+ fields = [
93
+ { title: 'Exception', value: clean_message },
94
+ { title: 'Hostname', value: Socket.gethostname }
95
+ ]
96
+
97
+ if backtrace
98
+ formatted_backtrace = "```#{backtrace.first(@backtrace_lines).join("\n")}```"
99
+ fields << { title: 'Backtrace', value: formatted_backtrace }
100
+ end
101
+
102
+ unless data.empty?
103
+ deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc)
104
+ data_string = data.map { |k, v| "#{k}: #{v}" }.join("\n")
105
+ fields << { title: 'Data', value: "```#{data_string}```" }
106
+ end
107
+
108
+ fields.concat(@additional_fields) if @additional_fields
109
+
110
+ fields
111
+ end
69
112
  end
70
113
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExceptionNotifier
4
+ class SnsNotifier < BaseNotifier
5
+ def initialize(options)
6
+ super
7
+
8
+ raise ArgumentError, "You must provide 'region' option" unless options[:region]
9
+ raise ArgumentError, "You must provide 'access_key_id' option" unless options[:access_key_id]
10
+ raise ArgumentError, "You must provide 'secret_access_key' option" unless options[:secret_access_key]
11
+
12
+ @notifier = Aws::SNS::Client.new(
13
+ region: options[:region],
14
+ access_key_id: options[:access_key_id],
15
+ secret_access_key: options[:secret_access_key]
16
+ )
17
+ @options = default_options.merge(options)
18
+ end
19
+
20
+ def call(exception, custom_opts = {})
21
+ custom_options = options.merge(custom_opts)
22
+
23
+ subject = build_subject(exception, custom_options)
24
+ message = build_message(exception, custom_options)
25
+
26
+ notifier.publish(
27
+ topic_arn: custom_options[:topic_arn],
28
+ message: message,
29
+ subject: subject
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :notifier, :options
36
+
37
+ def build_subject(exception, options)
38
+ subject =
39
+ "#{options[:sns_prefix]} - #{accumulated_exception_name(exception, options)} occurred"
40
+ subject.length > 120 ? subject[0...120] + '...' : subject
41
+ end
42
+
43
+ def build_message(exception, options)
44
+ exception_name = accumulated_exception_name(exception, options)
45
+
46
+ if options[:env].nil?
47
+ text = "#{exception_name} occured in background\n"
48
+ else
49
+ env = options[:env]
50
+
51
+ kontroller = env['action_controller.instance']
52
+ request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
53
+
54
+ text = "#{exception_name} occurred while #{request}"
55
+ text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller
56
+ end
57
+
58
+ text += "Exception: #{exception.message}\n"
59
+ text += "Hostname: #{Socket.gethostname}\n"
60
+
61
+ return unless exception.backtrace
62
+
63
+ formatted_backtrace = exception.backtrace.first(options[:backtrace_lines]).join("\n").to_s
64
+ text + "Backtrace:\n#{formatted_backtrace}\n"
65
+ end
66
+
67
+ def accumulated_exception_name(exception, options)
68
+ errors_count = options[:accumulated_errors_count].to_i
69
+
70
+ measure_word = if errors_count > 1
71
+ errors_count
72
+ else
73
+ exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A'
74
+ end
75
+
76
+ "#{measure_word} #{exception.class}"
77
+ end
78
+
79
+ def default_options
80
+ {
81
+ sns_prefix: '[ERROR]',
82
+ backtrace_lines: 10
83
+ }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_dispatch'
4
+ require 'active_support/core_ext/time'
5
+ require 'json'
6
+
7
+ module ExceptionNotifier
8
+ class TeamsNotifier < BaseNotifier
9
+ include ExceptionNotifier::BacktraceCleaner
10
+
11
+ class MissingController
12
+ def method_missing(*args, &block); end
13
+ end
14
+
15
+ attr_accessor :httparty
16
+
17
+ def initialize(options = {})
18
+ super
19
+ @default_options = options
20
+ @httparty = HTTParty
21
+ end
22
+
23
+ def call(exception, options = {})
24
+ @options = options.merge(@default_options)
25
+ @exception = exception
26
+ @backtrace = exception.backtrace ? clean_backtrace(exception) : nil
27
+
28
+ @env = @options.delete(:env)
29
+
30
+ @application_name = @options.delete(:app_name) || rails_app_name
31
+ @gitlab_url = @options.delete(:git_url)
32
+ @jira_url = @options.delete(:jira_url)
33
+
34
+ @webhook_url = @options.delete(:webhook_url)
35
+ raise ArgumentError, "You must provide 'webhook_url' parameter." unless @webhook_url
36
+
37
+ if @env.nil?
38
+ @controller = @request_items = nil
39
+ else
40
+ @controller = @env['action_controller.instance'] || MissingController.new
41
+
42
+ request = ActionDispatch::Request.new(@env)
43
+
44
+ @request_items = { url: request.original_url,
45
+ http_method: request.method,
46
+ ip_address: request.remote_ip,
47
+ parameters: request.filtered_parameters,
48
+ timestamp: Time.current }
49
+
50
+ if request.session['warden.user.user.key']
51
+ current_user = User.find(request.session['warden.user.user.key'][0][0])
52
+ @request_items[:current_user] = { id: current_user.id, email: current_user.email }
53
+ end
54
+ end
55
+
56
+ payload = message_text
57
+
58
+ @options[:body] = payload.to_json
59
+ @options[:headers] ||= {}
60
+ @options[:headers]['Content-Type'] = 'application/json'
61
+ @options[:debug_output] = $stdout
62
+
63
+ @httparty.post(@webhook_url, @options)
64
+ end
65
+
66
+ private
67
+
68
+ def message_text
69
+ text = {
70
+ '@type' => 'MessageCard',
71
+ '@context' => 'http://schema.org/extensions',
72
+ 'summary' => "#{@application_name} Exception Alert",
73
+ 'title' => "⚠️ Exception Occurred in #{env_name} ⚠️",
74
+ 'sections' => [
75
+ {
76
+ 'activityTitle' => activity_title,
77
+ 'activitySubtitle' => @exception.message.to_s
78
+ }
79
+ ],
80
+ 'potentialAction' => []
81
+ }
82
+
83
+ text['sections'].push details
84
+ text['potentialAction'].push gitlab_view_link unless @gitlab_url.nil?
85
+ text['potentialAction'].push gitlab_issue_link unless @gitlab_url.nil?
86
+ text['potentialAction'].push jira_issue_link unless @jira_url.nil?
87
+
88
+ text
89
+ end
90
+
91
+ def details
92
+ details = {
93
+ 'title' => 'Details',
94
+ 'facts' => []
95
+ }
96
+
97
+ details['facts'].push message_request unless @request_items.nil?
98
+ details['facts'].push message_backtrace unless @backtrace.nil?
99
+
100
+ details
101
+ end
102
+
103
+ def activity_title
104
+ errors_count = @options[:accumulated_errors_count].to_i
105
+
106
+ "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" +
107
+ (@controller ? " in *#{controller_and_method}*." : '.')
108
+ end
109
+
110
+ def message_request
111
+ {
112
+ 'name' => 'Request',
113
+ 'value' => "#{hash_presentation(@request_items)}\n "
114
+ }
115
+ end
116
+
117
+ def message_backtrace(size = 3)
118
+ text = []
119
+ size = @backtrace.size < size ? @backtrace.size : size
120
+ text << '```'
121
+ size.times { |i| text << '* ' + @backtrace[i] }
122
+ text << '```'
123
+
124
+ {
125
+ 'name' => 'Backtrace',
126
+ 'value' => text.join(" \n").to_s
127
+ }
128
+ end
129
+
130
+ def gitlab_view_link
131
+ {
132
+ '@type' => 'ViewAction',
133
+ 'name' => "\u{1F98A} View in GitLab",
134
+ 'target' => [
135
+ "#{@gitlab_url}/#{@application_name}"
136
+ ]
137
+ }
138
+ end
139
+
140
+ def gitlab_issue_link
141
+ link = [@gitlab_url, @application_name, 'issues', 'new'].join('/')
142
+ params = {
143
+ 'issue[title]' => ['[BUG] Error 500 :',
144
+ controller_and_method,
145
+ "(#{@exception.class})",
146
+ @exception.message].compact.join(' ')
147
+ }.to_query
148
+
149
+ {
150
+ '@type' => 'ViewAction',
151
+ 'name' => "\u{1F98A} Create Issue in GitLab",
152
+ 'target' => [
153
+ "#{link}/?#{params}"
154
+ ]
155
+ }
156
+ end
157
+
158
+ def jira_issue_link
159
+ {
160
+ '@type' => 'ViewAction',
161
+ 'name' => '🐞 Create Issue in Jira',
162
+ 'target' => [
163
+ "#{@jira_url}/secure/CreateIssue!default.jspa"
164
+ ]
165
+ }
166
+ end
167
+
168
+ def controller_and_method
169
+ if @controller
170
+ "#{@controller.controller_name}##{@controller.action_name}"
171
+ else
172
+ ''
173
+ end
174
+ end
175
+
176
+ def hash_presentation(hash)
177
+ text = []
178
+
179
+ hash.each do |key, value|
180
+ text << "* **#{key}** : `#{value}`"
181
+ end
182
+
183
+ text.join(" \n")
184
+ end
185
+
186
+ def rails_app_name
187
+ return unless defined?(Rails) && Rails.respond_to?(:application)
188
+
189
+ if ::Gem::Version.new(Rails.version) >= ::Gem::Version.new('6.0')
190
+ Rails.application.class.module_parent_name.underscore
191
+ else
192
+ Rails.application.class.parent_name.underscore
193
+ end
194
+ end
195
+
196
+ def env_name
197
+ Rails.env if defined?(Rails) && Rails.respond_to?(:env)
198
+ end
199
+ end
200
+ end