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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +16 -0
  3. data/CODE_OF_CONDUCT.md +22 -0
  4. data/CONTRIBUTING.md +33 -0
  5. data/MIT-LICENSE +23 -0
  6. data/README.md +534 -0
  7. data/RELEASING.md +51 -0
  8. data/Rakefile +25 -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/slack.md +154 -0
  13. data/docs/notifiers/sns.md +37 -0
  14. data/docs/notifiers/teams.md +54 -0
  15. data/docs/notifiers/webhook.md +60 -0
  16. data/exceptify.gemspec +48 -0
  17. data/lib/exceptify/base_notifier.rb +30 -0
  18. data/lib/exceptify/configuration.rb +184 -0
  19. data/lib/exceptify/datadog_notifier.rb +160 -0
  20. data/lib/exceptify/dispatcher.rb +49 -0
  21. data/lib/exceptify/email_notifier.rb +208 -0
  22. data/lib/exceptify/modules/backtrace_cleaner.rb +13 -0
  23. data/lib/exceptify/modules/error_grouping.rb +170 -0
  24. data/lib/exceptify/modules/formatter.rb +119 -0
  25. data/lib/exceptify/notification.rb +71 -0
  26. data/lib/exceptify/notifier.rb +19 -0
  27. data/lib/exceptify/notifier_registry.rb +55 -0
  28. data/lib/exceptify/rack.rb +88 -0
  29. data/lib/exceptify/rails/runner_tie.rb +57 -0
  30. data/lib/exceptify/rails.rb +29 -0
  31. data/lib/exceptify/rake.rb +59 -0
  32. data/lib/exceptify/request_context.rb +35 -0
  33. data/lib/exceptify/resque.rb +25 -0
  34. data/lib/exceptify/sidekiq.rb +15 -0
  35. data/lib/exceptify/slack_notifier.rb +141 -0
  36. data/lib/exceptify/sns_notifier.rb +98 -0
  37. data/lib/exceptify/solid_queue.rb +68 -0
  38. data/lib/exceptify/teams_notifier.rb +209 -0
  39. data/lib/exceptify/version.rb +5 -0
  40. data/lib/exceptify/views/exceptify/_backtrace.html.erb +3 -0
  41. data/lib/exceptify/views/exceptify/_backtrace.text.erb +1 -0
  42. data/lib/exceptify/views/exceptify/_data.html.erb +6 -0
  43. data/lib/exceptify/views/exceptify/_data.text.erb +1 -0
  44. data/lib/exceptify/views/exceptify/_environment.html.erb +10 -0
  45. data/lib/exceptify/views/exceptify/_environment.text.erb +5 -0
  46. data/lib/exceptify/views/exceptify/_request.html.erb +36 -0
  47. data/lib/exceptify/views/exceptify/_request.text.erb +10 -0
  48. data/lib/exceptify/views/exceptify/_session.html.erb +10 -0
  49. data/lib/exceptify/views/exceptify/_session.text.erb +2 -0
  50. data/lib/exceptify/views/exceptify/_title.html.erb +3 -0
  51. data/lib/exceptify/views/exceptify/_title.text.erb +3 -0
  52. data/lib/exceptify/views/exceptify/background_exceptify.html.erb +53 -0
  53. data/lib/exceptify/views/exceptify/background_exceptify.text.erb +14 -0
  54. data/lib/exceptify/views/exceptify/exceptify.html.erb +52 -0
  55. data/lib/exceptify/views/exceptify/exceptify.text.erb +24 -0
  56. data/lib/exceptify/webhook_notifier.rb +63 -0
  57. data/lib/exceptify.rb +177 -0
  58. data/lib/generators/exceptify/install_generator.rb +24 -0
  59. data/lib/generators/exceptify/templates/exceptify.rb.erb +44 -0
  60. metadata +364 -0
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext/numeric/time"
5
+ require "active_support/concern"
6
+
7
+ module Exceptify
8
+ module ErrorGrouping
9
+ extend ActiveSupport::Concern
10
+
11
+ class Service
12
+ attr_reader :cache, :fallback_cache_store, :period, :notification_trigger, :logger
13
+
14
+ def initialize(cache:, fallback_cache_store:, period:, notification_trigger:, logger:)
15
+ @cache = cache
16
+ @fallback_cache_store = fallback_cache_store
17
+ @period = period
18
+ @notification_trigger = notification_trigger
19
+ @logger = logger
20
+ end
21
+
22
+ def error_count(error_key)
23
+ count =
24
+ begin
25
+ cache_store.read(error_key)
26
+ rescue => e
27
+ log_cache_error(cache_store, e, :read)
28
+ fallback_cache_store.read(error_key)
29
+ end
30
+
31
+ count&.to_i
32
+ end
33
+
34
+ def save_error_count(error_key, count)
35
+ cache_store.write(error_key, count, expires_in: period)
36
+ rescue => e
37
+ log_cache_error(cache_store, e, :write)
38
+ fallback_cache_store.write(error_key, count, expires_in: period)
39
+ end
40
+
41
+ def group_error!(exception, options)
42
+ message_based_key = key_for_message(exception)
43
+ accumulated_errors_count = 1
44
+
45
+ if (count = error_count(message_based_key))
46
+ accumulated_errors_count = count + 1
47
+ save_error_count(message_based_key, accumulated_errors_count)
48
+ else
49
+ backtrace_based_key = key_for_backtrace(exception)
50
+
51
+ if (count = error_count(backtrace_based_key))
52
+ accumulated_errors_count = count + 1
53
+ save_error_count(backtrace_based_key, accumulated_errors_count)
54
+ else
55
+ save_error_count(backtrace_based_key, accumulated_errors_count)
56
+ save_error_count(message_based_key, accumulated_errors_count)
57
+ end
58
+ end
59
+
60
+ options[:accumulated_errors_count] = accumulated_errors_count
61
+ end
62
+
63
+ def send_notification?(exception, count)
64
+ if notification_trigger.respond_to?(:call)
65
+ notification_trigger.call(exception, count)
66
+ else
67
+ factor = Math.log2(count)
68
+ factor.to_i == factor
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def cache_store
75
+ cache || fallback_cache_store
76
+ end
77
+
78
+ def key_for_message(exception)
79
+ "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
80
+ end
81
+
82
+ def key_for_backtrace(exception)
83
+ "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
84
+ end
85
+
86
+ def log_cache_error(cache, exception, action)
87
+ logger.warn(
88
+ "#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
89
+ )
90
+ end
91
+ end
92
+
93
+ included do
94
+ mattr_accessor :error_grouping
95
+ self.error_grouping = false
96
+
97
+ mattr_accessor :error_grouping_period
98
+ self.error_grouping_period = 5.minutes
99
+
100
+ mattr_accessor :notification_trigger
101
+
102
+ mattr_accessor :error_grouping_cache
103
+ end
104
+
105
+ module ClassMethods
106
+ # Fallback to the memory store while the specified cache store doesn't work
107
+ #
108
+ def fallback_cache_store
109
+ @fallback_cache_store ||= ActiveSupport::Cache::MemoryStore.new
110
+ end
111
+
112
+ def error_count(error_key)
113
+ count =
114
+ begin
115
+ error_grouping_cache.read(error_key)
116
+ rescue => e
117
+ log_cache_error(error_grouping_cache, e, :read)
118
+ fallback_cache_store.read(error_key)
119
+ end
120
+
121
+ count&.to_i
122
+ end
123
+
124
+ def save_error_count(error_key, count)
125
+ error_grouping_cache.write(error_key, count, expires_in: error_grouping_period)
126
+ rescue => e
127
+ log_cache_error(error_grouping_cache, e, :write)
128
+ fallback_cache_store.write(error_key, count, expires_in: error_grouping_period)
129
+ end
130
+
131
+ def group_error!(exception, options)
132
+ message_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\nmessage:#{exception.message}")}"
133
+ accumulated_errors_count = 1
134
+
135
+ if (count = error_count(message_based_key))
136
+ accumulated_errors_count = count + 1
137
+ save_error_count(message_based_key, accumulated_errors_count)
138
+ else
139
+ backtrace_based_key =
140
+ "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
141
+
142
+ if (count = error_grouping_cache.read(backtrace_based_key))
143
+ accumulated_errors_count = count + 1
144
+ save_error_count(backtrace_based_key, accumulated_errors_count)
145
+ else
146
+ save_error_count(backtrace_based_key, accumulated_errors_count)
147
+ save_error_count(message_based_key, accumulated_errors_count)
148
+ end
149
+ end
150
+
151
+ options[:accumulated_errors_count] = accumulated_errors_count
152
+ end
153
+
154
+ def send_notification?(exception, count)
155
+ if notification_trigger.respond_to?(:call)
156
+ notification_trigger.call(exception, count)
157
+ else
158
+ factor = Math.log2(count)
159
+ factor.to_i == factor
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def log_cache_error(cache, exception, action)
166
+ "#{cache.inspect} failed to #{action}, reason: #{exception.message}. Falling back to memory cache store."
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/time"
4
+ require "action_dispatch"
5
+ require "exceptify/notification"
6
+
7
+ module Exceptify
8
+ class Formatter
9
+ include Exceptify::BacktraceCleaner
10
+
11
+ attr_reader :app_name
12
+
13
+ def initialize(exception_or_notification, opts = {})
14
+ @notification = if exception_or_notification.is_a?(Notification)
15
+ exception_or_notification
16
+ else
17
+ Notification.new(exception_or_notification, opts, backtrace_cleaner: self)
18
+ end
19
+ @exception = notification.exception
20
+ @errors_count = notification.options[:accumulated_errors_count].to_i
21
+ @app_name = notification.app_name
22
+ end
23
+
24
+ #
25
+ # :warning: Error occurred in production :warning:
26
+ # :warning: Error occurred :warning:
27
+ #
28
+ def title
29
+ env = notification.env_name
30
+
31
+ if env
32
+ "⚠️ Error occurred in #{env} ⚠️"
33
+ else
34
+ "⚠️ Error occurred ⚠️"
35
+ end
36
+ end
37
+
38
+ #
39
+ # A *NoMethodError* occurred.
40
+ # 3 *NoMethodError* occurred.
41
+ # A *NoMethodError* occurred in *home#index*.
42
+ #
43
+ def subtitle
44
+ errors_text = if errors_count > 1
45
+ errors_count
46
+ else
47
+ /^[aeiou]/i.match?(exception.class.to_s) ? "An" : "A"
48
+ end
49
+
50
+ in_action = " in *#{controller_and_action}*" if controller
51
+
52
+ "#{errors_text} *#{exception.class}* occurred#{in_action}."
53
+ end
54
+
55
+ #
56
+ #
57
+ # *Request:*
58
+ # ```
59
+ # * url : https://www.example.com/
60
+ # * http_method : GET
61
+ # * ip_address : 127.0.0.1
62
+ # * parameters : {"controller"=>"home", "action"=>"index"}
63
+ # * timestamp : 2019-01-01 00:00:00 UTC
64
+ # ```
65
+ #
66
+ def request_message
67
+ request = notification.request_context.request
68
+ return unless request
69
+
70
+ [
71
+ "```",
72
+ "* url : #{request.original_url}",
73
+ "* http_method : #{request.method}",
74
+ "* ip_address : #{request.remote_ip}",
75
+ "* parameters : #{request.filtered_parameters}",
76
+ "* timestamp : #{notification.timestamp}",
77
+ "```"
78
+ ].join("\n")
79
+ end
80
+
81
+ #
82
+ #
83
+ # *Backtrace:*
84
+ # ```
85
+ # * app/controllers/my_controller.rb:99:in `specific_function'
86
+ # * app/controllers/my_controller.rb:70:in `specific_param'
87
+ # * app/controllers/my_controller.rb:53:in `my_controller_params'
88
+ # ```
89
+ #
90
+ def backtrace_message
91
+ backtrace = notification.backtrace
92
+
93
+ return if backtrace.empty?
94
+
95
+ text = []
96
+
97
+ text << "```"
98
+ backtrace.first(3).each { |line| text << "* #{line}" }
99
+ text << "```"
100
+
101
+ text.join("\n")
102
+ end
103
+
104
+ #
105
+ # home#index
106
+ #
107
+ def controller_and_action
108
+ notification.controller_and_action
109
+ end
110
+
111
+ private
112
+
113
+ attr_reader :exception, :errors_count, :notification
114
+
115
+ def controller
116
+ notification.controller
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "active_support/core_ext/time"
5
+ require "exceptify/request_context"
6
+
7
+ module Exceptify
8
+ class Notification
9
+ attr_reader :exception, :options, :request_context
10
+
11
+ def initialize(exception, options = {}, clock: Time, hostname: -> { Socket.gethostname }, backtrace_cleaner: nil, **keyword_options)
12
+ @exception = exception
13
+ @options = options.merge(keyword_options)
14
+ @clock = clock
15
+ @hostname = hostname
16
+ @backtrace_cleaner = backtrace_cleaner
17
+ @request_context = @options[:request_context] || RequestContext.new(@options[:env])
18
+ end
19
+
20
+ def env
21
+ request_context.env
22
+ end
23
+
24
+ def data
25
+ request_context.exception_data.merge(options[:data] || {})
26
+ end
27
+
28
+ def backtrace
29
+ return [] unless exception.backtrace
30
+ return @backtrace_cleaner.clean_backtrace(exception) if @backtrace_cleaner
31
+
32
+ exception.backtrace
33
+ end
34
+
35
+ def timestamp
36
+ @clock.respond_to?(:current) ? @clock.current : @clock.now
37
+ end
38
+
39
+ def hostname
40
+ @hostname.call
41
+ end
42
+
43
+ def app_name
44
+ options[:app_name] || rails_app_name
45
+ end
46
+
47
+ def env_name
48
+ Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
49
+ end
50
+
51
+ def controller
52
+ request_context.controller
53
+ end
54
+
55
+ def controller_and_action
56
+ request_context.controller_and_action
57
+ end
58
+
59
+ private
60
+
61
+ def rails_app_name
62
+ return unless defined?(::Rails) && ::Rails.respond_to?(:application)
63
+
64
+ if Rails::VERSION::MAJOR >= 6
65
+ Rails.application.class.module_parent_name.underscore
66
+ else
67
+ Rails.application.class.parent_name.underscore
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/deprecation"
4
+
5
+ module Exceptify
6
+ class Notifier
7
+ def self.exceptify(env, exception, options = {})
8
+ ActiveSupport::Deprecation.warn(
9
+ "Please use Exceptify.notify_exception(exception, options.merge(env: env))."
10
+ )
11
+ Exceptify.registered_notifier(:email).create_email(exception, options.merge(env: env))
12
+ end
13
+
14
+ def self.background_exceptify(exception, options = {})
15
+ ActiveSupport::Deprecation.warn "Please use Exceptify.notify_exception(exception, options)."
16
+ Exceptify.registered_notifier(:email).create_email(exception, options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module Exceptify
6
+ class NotifierRegistry
7
+ attr_reader :notifiers
8
+
9
+ def initialize(notifiers = {}, factory: nil)
10
+ @notifiers = notifiers.dup
11
+ @factory = factory || method(:build_notifier)
12
+ end
13
+
14
+ def register(name, notifier_or_options)
15
+ if notifier_or_options.respond_to?(:call)
16
+ notifiers[name] = notifier_or_options
17
+ elsif notifier_or_options.is_a?(Hash)
18
+ notifiers[name] = @factory.call(name, notifier_or_options)
19
+ else
20
+ raise ArgumentError, "Invalid notifier '#{name}' defined as #{notifier_or_options.inspect}"
21
+ end
22
+ end
23
+
24
+ def unregister(name)
25
+ notifiers.delete(name)
26
+ end
27
+
28
+ def fetch(name)
29
+ notifiers[name]
30
+ end
31
+
32
+ def names
33
+ notifiers.keys
34
+ end
35
+
36
+ def clear
37
+ notifiers.clear
38
+ end
39
+
40
+ def copy
41
+ self.class.new(notifiers, factory: @factory)
42
+ end
43
+
44
+ private
45
+
46
+ def build_notifier(name, options)
47
+ notifier_classname = "#{name}_notifier".camelize
48
+ notifier_class = Exceptify.const_get(notifier_classname)
49
+ notifier_class.new(options)
50
+ rescue NameError => e
51
+ raise UndefinedNotifierError,
52
+ "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "exceptify"
4
+
5
+ module Exceptify
6
+ class Rack
7
+ class CascadePassException < RuntimeError; end
8
+
9
+ attr_reader :configuration
10
+
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ @dispatcher = options.delete(:dispatcher)
14
+ @configuration = options.delete(:configuration)
15
+ @ignore_cascade_pass = options.delete(:ignore_cascade_pass) { true }
16
+
17
+ return if @dispatcher
18
+ return if options.empty? && @configuration.nil?
19
+
20
+ @configuration ||= Exceptify.configuration.copy
21
+ apply_options(@configuration, options)
22
+ @dispatcher = Dispatcher.new(@configuration)
23
+ end
24
+
25
+ def call(env)
26
+ _, headers, = response = @app.call(env)
27
+
28
+ if !@ignore_cascade_pass && headers["X-Cascade"] == "pass"
29
+ msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " \
30
+ "Rails, this often means that the route was not found (404 error)."
31
+ raise CascadePassException, msg
32
+ end
33
+
34
+ response
35
+ rescue Exception => e # standard:disable Lint/RescueException
36
+ env["exceptify.delivered"] = true if dispatcher.notify_exception(e, env: env)
37
+
38
+ raise e unless e.is_a?(CascadePassException)
39
+
40
+ response
41
+ end
42
+
43
+ private
44
+
45
+ def dispatcher
46
+ @dispatcher || Exceptify
47
+ end
48
+
49
+ def apply_options(configuration, options)
50
+ configuration.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions)
51
+ configuration.error_grouping = options.delete(:error_grouping) if options.key?(:error_grouping)
52
+ configuration.error_grouping_period = options.delete(:error_grouping_period) if options.key?(:error_grouping_period)
53
+ configuration.notification_trigger = options.delete(:notification_trigger) if options.key?(:notification_trigger)
54
+
55
+ if options.key?(:error_grouping_cache)
56
+ configuration.error_grouping_cache = options.delete(:error_grouping_cache)
57
+ elsif defined?(Rails) && Rails.respond_to?(:cache)
58
+ configuration.error_grouping_cache = Rails.cache
59
+ end
60
+
61
+ apply_ignore_options(configuration, options)
62
+
63
+ options.each do |notifier_name, opts|
64
+ configuration.register_notifier(notifier_name, opts)
65
+ end
66
+ end
67
+
68
+ def apply_ignore_options(configuration, options)
69
+ if options.key?(:ignore_if)
70
+ rack_ignore = options.delete(:ignore_if)
71
+ configuration.ignore_if do |exception, opts|
72
+ opts.key?(:env) && rack_ignore.call(opts[:env], exception)
73
+ end
74
+ end
75
+
76
+ if options.key?(:ignore_notifier_if)
77
+ rack_ignore_by_notifier = options.delete(:ignore_notifier_if)
78
+ rack_ignore_by_notifier.each do |notifier, proc|
79
+ configuration.ignore_notifier_if(notifier) do |exception, opts|
80
+ opts.key?(:env) && proc.call(opts[:env], exception)
81
+ end
82
+ end
83
+ end
84
+
85
+ configuration.ignore_crawlers(options.delete(:ignore_crawlers)) if options.key?(:ignore_crawlers)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "exceptify"
4
+
5
+ module Exceptify
6
+ module Rails
7
+ class RunnerTie
8
+ class << self
9
+ attr_writer :installed
10
+
11
+ def installed?
12
+ @installed == true
13
+ end
14
+
15
+ def reset!
16
+ @installed = false
17
+ end
18
+ end
19
+
20
+ def initialize(registrar: nil, error_source: nil, notifier: Exceptify)
21
+ @registrar = registrar || ->(&block) { at_exit(&block) }
22
+ @error_source = error_source || -> { $ERROR_INFO }
23
+ @notifier = notifier
24
+ end
25
+
26
+ # Registers an at_exit callback, which checks if there was an exception. This is a pretty
27
+ # crude way to detect exceptions from runner commands, but Rails doesn't provide a better API.
28
+ #
29
+ # This should only be called from a runner callback in your Rails config; otherwise you may
30
+ # register the at_exit callback in more places than you need or want it.
31
+ def call
32
+ return false if self.class.installed?
33
+
34
+ self.class.installed = true
35
+
36
+ @registrar.call do
37
+ exception = @error_source.call
38
+ if exception && !exception.is_a?(SystemExit)
39
+ @notifier.notify_exception(exception, data: data_for_exceptify(exception))
40
+ end
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ def data_for_exceptify(exception = nil)
49
+ data = {}
50
+ data[:error_class] = exception.class.name if exception
51
+ data[:error_message] = exception.message if exception
52
+
53
+ data
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Warning: This must be required after rails but before initializers have been run. If you require
4
+ # it from config/initializers/exceptify.rb, then the rails and rake_task callbacks
5
+ # registered here will have no effect, because Rails will have already invoked all registered rails
6
+ # and rake_tasks handlers.
7
+
8
+ require "exceptify"
9
+
10
+ module Exceptify
11
+ class Engine < ::Rails::Engine
12
+ config.exceptify = Exceptify
13
+ config.exceptify.logger = Rails.logger
14
+ config.exceptify.error_grouping_cache = Rails.cache
15
+
16
+ config.app_middleware.use Exceptify::Rack
17
+
18
+ rake_tasks do
19
+ # Report exceptions occurring in Rake tasks.
20
+ require "exceptify/rake"
21
+ end
22
+
23
+ runner do
24
+ # Report exceptions occurring in runner commands.
25
+ require "exceptify/rails/runner_tie"
26
+ Exceptify::Rails::RunnerTie.new.call
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copied/adapted from https://github.com/airbrake/airbrake/blob/master/lib/airbrake/rake.rb
4
+
5
+ require "exceptify"
6
+ require "rake"
7
+
8
+ Rake::TaskManager.record_task_metadata = true if Rake.const_defined?(:TaskManager)
9
+
10
+ module Exceptify
11
+ module RakeTaskExtensions
12
+ # A wrapper around the original +#execute+, that catches all errors and
13
+ # passes them on to Exceptify.
14
+ def execute(args = nil)
15
+ super
16
+ rescue Exception => e # standard:disable Lint/RescueException
17
+ Exceptify.notify_exception(e, data: data_for_exceptify(e)) unless e.is_a?(SystemExit)
18
+ raise e
19
+ end
20
+
21
+ private
22
+
23
+ def data_for_exceptify(exception = nil)
24
+ data = {}
25
+ data[:error_class] = exception.class.name if exception
26
+ data[:error_message] = exception.message if exception
27
+
28
+ data[:rake] = {}
29
+ data[:rake][:rake_command_line] = reconstruct_command_line
30
+ data[:rake][:name] = name
31
+ data[:rake][:timestamp] = timestamp.to_s
32
+ # data[:investigation] = investigation
33
+
34
+ data[:rake][:full_comment] = full_comment if full_comment
35
+ data[:rake][:arg_names] = arg_names if arg_names.any?
36
+ data[:rake][:arg_description] = arg_description if arg_description
37
+ data[:rake][:locations] = locations if locations.any?
38
+ data[:rake][:sources] = sources if sources.any?
39
+
40
+ if prerequisite_tasks.any?
41
+ data[:rake][:prerequisite_tasks] = prerequisite_tasks.map do |p|
42
+ p.__send__(:data_for_exceptify)[:rake]
43
+ end
44
+ end
45
+
46
+ data
47
+ end
48
+
49
+ def reconstruct_command_line
50
+ "rake #{ARGV.join(" ")}"
51
+ end
52
+ end
53
+ end
54
+
55
+ module Rake
56
+ class Task
57
+ prepend Exceptify::RakeTaskExtensions
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+
5
+ module Exceptify
6
+ class RequestContext
7
+ attr_reader :env
8
+
9
+ def initialize(env)
10
+ @env = env
11
+ end
12
+
13
+ def present?
14
+ !env.nil?
15
+ end
16
+
17
+ def request
18
+ @request ||= ActionDispatch::Request.new(env) if present?
19
+ end
20
+
21
+ def controller
22
+ env["action_controller.instance"] if present?
23
+ end
24
+
25
+ def controller_and_action
26
+ "#{controller.controller_name}##{controller.action_name}" if controller
27
+ end
28
+
29
+ def exception_data
30
+ return {} unless present?
31
+
32
+ env["exceptify.exception_data"] || {}
33
+ end
34
+ end
35
+ end