exception_notification 4.2.2 → 4.3.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/Appraisals +1 -2
- data/CHANGELOG.rdoc +17 -2
- data/README.md +130 -5
- data/exception_notification.gemspec +3 -2
- data/gemfiles/rails4_0.gemfile +0 -1
- data/gemfiles/rails4_1.gemfile +0 -1
- data/gemfiles/rails4_2.gemfile +0 -1
- data/gemfiles/rails5_0.gemfile +0 -1
- data/gemfiles/rails5_1.gemfile +7 -0
- data/lib/exception_notifier.rb +10 -5
- data/lib/exception_notifier/email_notifier.rb +5 -5
- data/lib/exception_notifier/google_chat_notifier.rb +136 -0
- data/lib/exception_notifier/mattermost_notifier.rb +5 -0
- data/lib/exception_notifier/modules/error_grouping.rb +1 -1
- data/lib/exception_notifier/sns_notifier.rb +79 -0
- data/lib/exception_notifier/teams_notifier.rb +179 -0
- data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +9 -9
- data/test/exception_notifier/google_chat_notifier_test.rb +128 -0
- data/test/exception_notifier/modules/error_grouping_test.rb +1 -1
- data/test/exception_notifier/sns_notifier_test.rb +126 -0
- data/test/exception_notifier/teams_notifier_test.rb +93 -0
- data/test/exception_notifier_test.rb +29 -0
- metadata +27 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ed3e1aae22db5d3e8005cf160d81b2e4cbf629c
|
4
|
+
data.tar.gz: 12fda9fcf321b8b3c5d2fa5eb2a1c35358e03105
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4cce16554760bb205c3d2f64ce7c4278f9a7e376254989d486132aec26ebe9802cd62e3dcc18eb51cc94f70d58dd40b57263777ac8e9315f18a68b09b1a335a8
|
7
|
+
data.tar.gz: 34578cb651e0363e1a58b08ef3bbc70187afc7fb8de9e57bfa0ad3f35e1f93dd639ff4a63b1fe148c014875fafaffea6471895c9882102a99ed0b1127c095902
|
data/Appraisals
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0']
|
1
|
+
rails_versions = ['~> 4.0.5', '~> 4.1.1', '~> 4.2.0', '~> 5.0.0', '~> 5.1.0']
|
2
2
|
|
3
3
|
rails_versions.each do |rails_version|
|
4
4
|
appraise "rails#{rails_version.slice(/\d+\.\d+/).gsub('.', '_')}" do
|
5
5
|
gem 'rails', rails_version
|
6
|
-
gem "sqlite3"
|
7
6
|
end
|
8
7
|
end
|
data/CHANGELOG.rdoc
CHANGED
@@ -1,7 +1,22 @@
|
|
1
|
+
== 4.3.0
|
2
|
+
|
3
|
+
* enhancements
|
4
|
+
* Add Microsoft Teams Notifier (by @phaelin)
|
5
|
+
* Add SNS notifier (by @FLarra)
|
6
|
+
* Add Google Chats notifier (by @renatolond)
|
7
|
+
* Align output of section-headers consistently (by @kronn)
|
8
|
+
* ExceptionNotifier.notify_exception receives block & pass it to each notifier (by @pocke)
|
9
|
+
* Update Travis to latest rubies (by @lostapathy)
|
10
|
+
|
11
|
+
* bug fixes
|
12
|
+
* Replace all before_filter to before_action on readme (by @pastullo)
|
13
|
+
* Fix error when using error grouping outside of rails (by @garethcokell)
|
14
|
+
* Fix missing MissingController Mattermost class (by @n-rodriguez)
|
15
|
+
|
1
16
|
== 4.2.2
|
2
17
|
|
3
18
|
* enhancements
|
4
|
-
* Error
|
19
|
+
* Error grouping (by @Martin91)
|
5
20
|
* Additional fields for Slack support (by @schurig)
|
6
21
|
* Enterprise HipChat support (by @seanhuber)
|
7
22
|
|
@@ -131,7 +146,7 @@
|
|
131
146
|
* Add normalize_subject option to remove numbers from email so that they thread (by @jjb)
|
132
147
|
* Allow the user to provide a custom message and hash of data (by @jjb)
|
133
148
|
* Add support for configurable background sections and a data partial (by @jeffrafter)
|
134
|
-
* Include timestamp of exception in notification body
|
149
|
+
* Include timestamp of exception in notification body
|
135
150
|
* Add support for rack based session management (by @phoet)
|
136
151
|
* Add ignore_crawlers option to ignore exceptions generated by crawlers
|
137
152
|
* Add verbode_subject option to exclude exception message from subject (by @amishyn)
|
data/README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
---
|
11
11
|
|
12
|
-
The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [IRC](#irc-notifier) or via custom [WebHooks](#webhook-notifier).
|
12
|
+
The Exception Notification gem provides a set of [notifiers](#notifiers) for sending notifications when errors occur in a Rack/Rails application. The built-in notifiers can deliver notifications by [email](#email-notifier), [Campfire](#campfire-notifier), [HipChat](#hipchat-notifier), [Slack](#slack-notifier), [Mattermost](#mattermost-notifier), [Teams](#teams-notifier), [IRC](#irc-notifier), [Amazon SNS](#amazon-sns-notifier), [Google Chat](#google-chat-notifier) or via custom [WebHooks](#webhook-notifier).
|
13
13
|
|
14
14
|
There's a great [Railscast about Exception Notification](http://railscasts.com/episodes/104-exception-notifications-revised) you can see that may help you getting started.
|
15
15
|
|
@@ -56,7 +56,7 @@ Save the current user in the `request` using a controller callback.
|
|
56
56
|
|
57
57
|
```ruby
|
58
58
|
class ApplicationController < ActionController::Base
|
59
|
-
|
59
|
+
before_action :prepare_exception_notifier
|
60
60
|
private
|
61
61
|
def prepare_exception_notifier
|
62
62
|
request.env["exception_notifier.exception_data"] = {
|
@@ -90,6 +90,9 @@ ExceptionNotification relies on notifiers to deliver notifications when errors o
|
|
90
90
|
* [IRC notifier](#irc-notifier)
|
91
91
|
* [Slack notifier](#slack-notifier)
|
92
92
|
* [Mattermost notifier](#mattermost-notifier)
|
93
|
+
* [Teams notifier](#teams-notifier)
|
94
|
+
* [Amazon SNS](#amazon-sns-notifier)
|
95
|
+
* [Google Chat notifier](#google-chat-notifier)
|
93
96
|
* [WebHook notifier](#webhook-notifier)
|
94
97
|
|
95
98
|
But, you also can easily implement your own [custom notifier](#custom-notifier).
|
@@ -220,7 +223,7 @@ If your new section requires information that isn't available by default, make s
|
|
220
223
|
|
221
224
|
```ruby
|
222
225
|
class ApplicationController < ActionController::Base
|
223
|
-
|
226
|
+
before_action :log_additional_data
|
224
227
|
...
|
225
228
|
protected
|
226
229
|
def log_additional_data
|
@@ -542,7 +545,7 @@ The slack notification will include any data saved under `env["exception_notifie
|
|
542
545
|
An example of how to send the server name to Slack in Rails (put this code in application_controller.rb):
|
543
546
|
|
544
547
|
```ruby
|
545
|
-
|
548
|
+
before_action :set_notification
|
546
549
|
|
547
550
|
def set_notification
|
548
551
|
request.env['exception_notifier.exception_data'] = {"server" => request.env['SERVER_NAME']}
|
@@ -608,7 +611,7 @@ Contains additional payload for a message (e.g avatar, attachments, etc). See [s
|
|
608
611
|
|
609
612
|
Contains additional fields that will be added to the attachement. See [Slack documentation](https://api.slack.com/docs/message-attachments).
|
610
613
|
|
611
|
-
|
614
|
+
### Mattermost notifier
|
612
615
|
|
613
616
|
Post notification in a mattermost channel via [incoming webhook](http://docs.mattermost.com/developer/webhooks-incoming.html)
|
614
617
|
|
@@ -723,6 +726,128 @@ Url of your gitlab or github with your organisation name for issue creation link
|
|
723
726
|
|
724
727
|
Your application name used for issue creation link. Defaults to ``` Rails.application.class.parent_name.underscore```.
|
725
728
|
|
729
|
+
### Google Chat Notifier
|
730
|
+
|
731
|
+
Post notifications in a Google Chats channel via [incoming webhook](https://developers.google.com/hangouts/chat/how-tos/webhooks)
|
732
|
+
|
733
|
+
Add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`:
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
gem 'httparty'
|
737
|
+
```
|
738
|
+
|
739
|
+
To configure it, you **need** to set the `webhook_url` option.
|
740
|
+
|
741
|
+
```ruby
|
742
|
+
Rails.application.config.middleware.use ExceptionNotification::Rack,
|
743
|
+
:google_chat => {
|
744
|
+
:webhook_url => 'https://chat.googleapis.com/v1/spaces/XXXXXXXX/messages?key=YYYYYYYYYYYYY&token=ZZZZZZZZZZZZ'
|
745
|
+
}
|
746
|
+
```
|
747
|
+
|
748
|
+
##### webhook_url
|
749
|
+
|
750
|
+
*String, required*
|
751
|
+
|
752
|
+
The Incoming WebHook URL on Google Chats.
|
753
|
+
|
754
|
+
##### app_name
|
755
|
+
|
756
|
+
*String, optional*
|
757
|
+
|
758
|
+
Your application name, shown in the notification. Defaults to `Rails.application.class.parent_name.underscore`.
|
759
|
+
|
760
|
+
### Amazon SNS Notifier
|
761
|
+
|
762
|
+
Notify all exceptions Amazon - Simple Notification Service: [SNS](https://aws.amazon.com/sns/).
|
763
|
+
|
764
|
+
#### Usage
|
765
|
+
|
766
|
+
Add the [aws-sdk-sns](https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-sns) gem to your `Gemfile`:
|
767
|
+
|
768
|
+
```ruby
|
769
|
+
gem 'aws-sdk-sns', '~> 1.5'
|
770
|
+
```
|
771
|
+
|
772
|
+
To configure it, you **need** to set 3 required options for aws: `region`, `access_key_id` and `secret_access_key`, and one more option for sns: `topic_arn`.
|
773
|
+
|
774
|
+
```ruby
|
775
|
+
Rails.application.config.middleware.use ExceptionNotification::Rack,
|
776
|
+
sns: {
|
777
|
+
region: 'us-east-x',
|
778
|
+
access_key_id: 'access_key_id',
|
779
|
+
secret_access_key: 'secret_access_key',
|
780
|
+
topic_arn: 'arn:aws:sns:us-east-x:XXXX:my-topic'
|
781
|
+
}
|
782
|
+
```
|
783
|
+
|
784
|
+
##### sns_prefix
|
785
|
+
*String, optional *
|
786
|
+
|
787
|
+
Prefix in the notification subject, by default: "[Error]"
|
788
|
+
|
789
|
+
##### backtrace_lines
|
790
|
+
*Integer, optional *
|
791
|
+
|
792
|
+
Number of backtrace lines to be displayed in the notification message. By default: 10
|
793
|
+
|
794
|
+
#### Note:
|
795
|
+
* You may need to update your previous `aws-sdk-*` gems in order to setup `aws-sdk-sns` correctly.
|
796
|
+
* If you need any further information about the available regions or any other SNS related topic consider: [SNS faqs](https://aws.amazon.com/sns/faqs/)
|
797
|
+
|
798
|
+
### Teams notifier
|
799
|
+
|
800
|
+
Post notification in a Microsoft Teams channel via [Incoming Webhook Connector](https://docs.microsoft.com/en-us/outlook/actionable-messages/actionable-messages-via-connectors)
|
801
|
+
Just add the [HTTParty](https://github.com/jnunemaker/httparty) gem to your `Gemfile`:
|
802
|
+
|
803
|
+
```ruby
|
804
|
+
gem 'httparty'
|
805
|
+
```
|
806
|
+
|
807
|
+
To configure it, you **need** to set the `webhook_url` option.
|
808
|
+
If you are using GitLab for issue tracking, you can specify `git_url` as follows to add a *Create issue* button in your notification.
|
809
|
+
By default this will use your Rails application name to match the git repository. If yours differs, you can specify `app_name`.
|
810
|
+
By that same notion, you may also set a `jira_url` to get a button that will send you to the New Issue screen in Jira.
|
811
|
+
|
812
|
+
```ruby
|
813
|
+
Rails.application.config.middleware.use ExceptionNotification::Rack,
|
814
|
+
:email => {
|
815
|
+
:email_prefix => "[PREFIX] ",
|
816
|
+
:sender_address => %{"notifier" <notifier@example.com>},
|
817
|
+
:exception_recipients => %w{exceptions@example.com}
|
818
|
+
},
|
819
|
+
:teams => {
|
820
|
+
:webhook_url => 'https://outlook.office.com/webhook/your-guid/IncomingWebhook/team-guid',
|
821
|
+
:git_url => 'https://your-gitlab.com/Group/Project',
|
822
|
+
:jira_url => 'https://your-jira.com'
|
823
|
+
}
|
824
|
+
```
|
825
|
+
|
826
|
+
#### Options
|
827
|
+
|
828
|
+
##### webhook_url
|
829
|
+
|
830
|
+
*String, required*
|
831
|
+
|
832
|
+
The Incoming WebHook URL on mattermost.
|
833
|
+
|
834
|
+
##### git_url
|
835
|
+
|
836
|
+
*String, optional*
|
837
|
+
|
838
|
+
Url of your gitlab or github with your organisation name for issue creation link (Eg: `github.com/aschen`). Defaults to nil and doesn't add link to the notification.
|
839
|
+
|
840
|
+
##### jira_url
|
841
|
+
|
842
|
+
*String, optional*
|
843
|
+
|
844
|
+
Url of your Jira instance, adds button for Create Issue screen. Defaults to nil and doesn't add a button to the card.
|
845
|
+
|
846
|
+
##### app_name
|
847
|
+
|
848
|
+
*String, optional*
|
849
|
+
|
850
|
+
Your application name used for git issue creation link. Defaults to `Rails.application.class.parent_name.underscore`.
|
726
851
|
|
727
852
|
### WebHook notifier
|
728
853
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'exception_notification'
|
3
|
-
s.version = '4.
|
3
|
+
s.version = '4.3.0'
|
4
4
|
s.authors = ["Jamis Buck", "Josh Peek"]
|
5
|
-
s.date = %q{
|
5
|
+
s.date = %q{2018-11-22}
|
6
6
|
s.summary = "Exception notification for Rails apps"
|
7
7
|
s.homepage = "https://smartinez87.github.io/exception_notification/"
|
8
8
|
s.email = "smartinez87@gmail.com"
|
@@ -32,4 +32,5 @@ Gem::Specification.new do |s|
|
|
32
32
|
s.add_development_dependency "hipchat", ">= 1.0.0"
|
33
33
|
s.add_development_dependency "carrier-pigeon", ">= 0.7.0"
|
34
34
|
s.add_development_dependency "slack-notifier", ">= 1.0.0"
|
35
|
+
s.add_development_dependency "aws-sdk-sns", "~> 1"
|
35
36
|
end
|
data/gemfiles/rails4_0.gemfile
CHANGED
data/gemfiles/rails4_1.gemfile
CHANGED
data/gemfiles/rails4_2.gemfile
CHANGED
data/gemfiles/rails5_0.gemfile
CHANGED
data/lib/exception_notifier.rb
CHANGED
@@ -17,6 +17,9 @@ module ExceptionNotifier
|
|
17
17
|
autoload :IrcNotifier, 'exception_notifier/irc_notifier'
|
18
18
|
autoload :SlackNotifier, 'exception_notifier/slack_notifier'
|
19
19
|
autoload :MattermostNotifier, 'exception_notifier/mattermost_notifier'
|
20
|
+
autoload :TeamsNotifier, 'exception_notifier/teams_notifier'
|
21
|
+
autoload :SnsNotifier, 'exception_notifier/sns_notifier'
|
22
|
+
autoload :GoogleChatNotifier, 'exception_notifier/google_chat_notifier'
|
20
23
|
|
21
24
|
class UndefinedNotifierError < StandardError; end
|
22
25
|
|
@@ -42,7 +45,7 @@ module ExceptionNotifier
|
|
42
45
|
self.testing_mode = true
|
43
46
|
end
|
44
47
|
|
45
|
-
def notify_exception(exception, options={})
|
48
|
+
def notify_exception(exception, options={}, &block)
|
46
49
|
return false if ignored_exception?(options[:ignore_exceptions], exception)
|
47
50
|
return false if ignored?(exception, options)
|
48
51
|
if error_grouping
|
@@ -52,7 +55,7 @@ module ExceptionNotifier
|
|
52
55
|
|
53
56
|
selected_notifiers = options.delete(:notifiers) || notifiers
|
54
57
|
[*selected_notifiers].each do |notifier|
|
55
|
-
fire_notification(notifier, exception, options.dup)
|
58
|
+
fire_notification(notifier, exception, options.dup, &block)
|
56
59
|
end
|
57
60
|
true
|
58
61
|
end
|
@@ -104,12 +107,14 @@ module ExceptionNotifier
|
|
104
107
|
end
|
105
108
|
|
106
109
|
def ignored_exception?(ignore_array, exception)
|
107
|
-
(Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
110
|
+
all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
|
111
|
+
exception_ancestors = exception.class.ancestors.map(&:to_s)
|
112
|
+
!(all_ignored_exceptions & exception_ancestors).empty?
|
108
113
|
end
|
109
114
|
|
110
|
-
def fire_notification(notifier_name, exception, options)
|
115
|
+
def fire_notification(notifier_name, exception, options, &block)
|
111
116
|
notifier = registered_exception_notifier(notifier_name)
|
112
|
-
notifier.call(exception, options)
|
117
|
+
notifier.call(exception, options, &block)
|
113
118
|
rescue Exception => e
|
114
119
|
raise e if @@testing_mode
|
115
120
|
|
@@ -60,8 +60,8 @@ module ExceptionNotifier
|
|
60
60
|
|
61
61
|
def compose_subject
|
62
62
|
subject = "#{@options[:email_prefix]}"
|
63
|
-
subject << "(#{@options[:accumulated_errors_count]} times)
|
64
|
-
subject << "#{@kontroller.controller_name}
|
63
|
+
subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
|
64
|
+
subject << "#{@kontroller.controller_name} #{@kontroller.action_name}" if @kontroller && @options[:include_controller_and_action_names_in_subject]
|
65
65
|
subject << " (#{@exception.class})"
|
66
66
|
subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
|
67
67
|
subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
|
@@ -75,17 +75,17 @@ module ExceptionNotifier
|
|
75
75
|
end
|
76
76
|
|
77
77
|
helper_method :inspect_object
|
78
|
-
|
78
|
+
|
79
79
|
def truncate(string, max)
|
80
80
|
string.length > max ? "#{string[0...max]}..." : string
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
def inspect_object(object)
|
84
84
|
case object
|
85
85
|
when Hash, Array
|
86
86
|
truncate(object.inspect, 300)
|
87
87
|
else
|
88
|
-
object.to_s
|
88
|
+
object.to_s
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'action_dispatch'
|
2
|
+
require 'active_support/core_ext/time'
|
3
|
+
|
4
|
+
module ExceptionNotifier
|
5
|
+
class GoogleChatNotifier
|
6
|
+
include ExceptionNotifier::BacktraceCleaner
|
7
|
+
|
8
|
+
class MissingController
|
9
|
+
def method_missing(*args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :httparty
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
super()
|
17
|
+
@default_options = options
|
18
|
+
@httparty = HTTParty
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(exception, options = {})
|
22
|
+
@options = options.merge(@default_options)
|
23
|
+
@exception = exception
|
24
|
+
@backtrace = exception.backtrace ? clean_backtrace(exception) : nil
|
25
|
+
|
26
|
+
@env = @options.delete(:env)
|
27
|
+
|
28
|
+
@application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore
|
29
|
+
|
30
|
+
@webhook_url = @options.delete(:webhook_url)
|
31
|
+
raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url
|
32
|
+
|
33
|
+
unless @env.nil?
|
34
|
+
@controller = @env['action_controller.instance'] || MissingController.new
|
35
|
+
|
36
|
+
request = ActionDispatch::Request.new(@env)
|
37
|
+
|
38
|
+
@request_items = { url: request.original_url,
|
39
|
+
http_method: request.method,
|
40
|
+
ip_address: request.remote_ip,
|
41
|
+
parameters: request.filtered_parameters,
|
42
|
+
timestamp: Time.current }
|
43
|
+
else
|
44
|
+
@controller = @request_items = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
@options[:body] = payload.to_json
|
49
|
+
@options[:headers] ||= {}
|
50
|
+
@options[:headers].merge!({ 'Content-Type' => 'application/json' })
|
51
|
+
|
52
|
+
@httparty.post(@webhook_url, @options)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def payload
|
58
|
+
{
|
59
|
+
text: exception_text
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def header
|
64
|
+
errors_count = @options[:accumulated_errors_count].to_i
|
65
|
+
text = ['']
|
66
|
+
|
67
|
+
text << "Application: *#{@application_name}*"
|
68
|
+
text << "#{errors_count > 1 ? errors_count : 'An'} *#{@exception.class}* occured" + if @controller then " in *#{controller_and_method}*." else "." end
|
69
|
+
|
70
|
+
text
|
71
|
+
end
|
72
|
+
|
73
|
+
def exception_text
|
74
|
+
text = []
|
75
|
+
|
76
|
+
text << header
|
77
|
+
text << ''
|
78
|
+
|
79
|
+
text << "⚠️ Error 500 in #{Rails.env} ⚠️"
|
80
|
+
text << "*#{@exception.message.gsub('`', %q('))}*"
|
81
|
+
|
82
|
+
if @request_items
|
83
|
+
text << ''
|
84
|
+
text += message_request
|
85
|
+
end
|
86
|
+
|
87
|
+
if @backtrace
|
88
|
+
text << ''
|
89
|
+
text += message_backtrace
|
90
|
+
end
|
91
|
+
|
92
|
+
text.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
def message_request
|
96
|
+
text = []
|
97
|
+
|
98
|
+
text << "*Request:*"
|
99
|
+
text << "```"
|
100
|
+
text << hash_presentation(@request_items)
|
101
|
+
text << "```"
|
102
|
+
|
103
|
+
text
|
104
|
+
end
|
105
|
+
|
106
|
+
def hash_presentation(hash)
|
107
|
+
text = []
|
108
|
+
|
109
|
+
hash.each do |key, value|
|
110
|
+
text << "* #{key} : #{value}"
|
111
|
+
end
|
112
|
+
|
113
|
+
text.join("\n")
|
114
|
+
end
|
115
|
+
|
116
|
+
def message_backtrace(size = 3)
|
117
|
+
text = []
|
118
|
+
|
119
|
+
size = @backtrace.size < size ? @backtrace.size : size
|
120
|
+
text << "*Backtrace:*"
|
121
|
+
text << "```"
|
122
|
+
size.times { |i| text << "* " + @backtrace[i] }
|
123
|
+
text << "```"
|
124
|
+
|
125
|
+
text
|
126
|
+
end
|
127
|
+
|
128
|
+
def controller_and_method
|
129
|
+
if @controller
|
130
|
+
"#{@controller.controller_name}##{@controller.action_name}"
|
131
|
+
else
|
132
|
+
""
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -52,7 +52,7 @@ module ExceptionNotifier
|
|
52
52
|
else
|
53
53
|
backtrace_based_key = "exception:#{Zlib.crc32("#{exception.class.name}\npath:#{exception.backtrace.try(:first)}")}"
|
54
54
|
|
55
|
-
if count =
|
55
|
+
if count = error_grouping_cache.read(backtrace_based_key)
|
56
56
|
accumulated_errors_count = count + 1
|
57
57
|
save_error_count(backtrace_based_key, accumulated_errors_count)
|
58
58
|
else
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ExceptionNotifier
|
2
|
+
class SnsNotifier < BaseNotifier
|
3
|
+
def initialize(options)
|
4
|
+
super
|
5
|
+
|
6
|
+
raise ArgumentError.new "You must provide 'region' option" unless options[:region]
|
7
|
+
raise ArgumentError.new "You must provide 'access_key_id' option" unless options[:access_key_id]
|
8
|
+
raise ArgumentError.new "You must provide 'secret_access_key' option" unless options[:secret_access_key]
|
9
|
+
|
10
|
+
@notifier = Aws::SNS::Client.new(
|
11
|
+
region: options[:region],
|
12
|
+
access_key_id: options[:access_key_id],
|
13
|
+
secret_access_key: options[:secret_access_key]
|
14
|
+
)
|
15
|
+
@options = default_options.merge(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(exception, custom_opts = {})
|
19
|
+
custom_options = options.merge(custom_opts)
|
20
|
+
|
21
|
+
subject = build_subject(exception, custom_options)
|
22
|
+
message = build_message(exception, custom_options)
|
23
|
+
|
24
|
+
notifier.publish(
|
25
|
+
topic_arn: custom_options[:topic_arn],
|
26
|
+
message: message,
|
27
|
+
subject: subject
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :notifier, :options
|
34
|
+
|
35
|
+
def build_subject(exception, options)
|
36
|
+
subject = "#{options[:sns_prefix]} - "
|
37
|
+
subject << accumulated_exception_name(exception, options)
|
38
|
+
subject << " occurred"
|
39
|
+
subject.length > 120 ? subject[0...120] + "..." : subject
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_message(exception, options)
|
43
|
+
exception_name = accumulated_exception_name(exception, options)
|
44
|
+
|
45
|
+
if options[:env].nil?
|
46
|
+
text = "#{exception_name} occured in background\n"
|
47
|
+
else
|
48
|
+
env = options[:env]
|
49
|
+
|
50
|
+
kontroller = env['action_controller.instance']
|
51
|
+
request = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
|
52
|
+
|
53
|
+
text = "#{exception_name} occurred while #{request}"
|
54
|
+
text += " was processed by #{kontroller.controller_name}##{kontroller.action_name}\n" if kontroller
|
55
|
+
end
|
56
|
+
|
57
|
+
text += "Exception: #{exception.message}\n"
|
58
|
+
text += "Hostname: #{Socket.gethostname}\n"
|
59
|
+
|
60
|
+
if exception.backtrace
|
61
|
+
formatted_backtrace = "#{exception.backtrace.first(options[:backtrace_lines]).join("\n")}"
|
62
|
+
text += "Backtrace:\n#{formatted_backtrace}\n"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def accumulated_exception_name(exception, options)
|
67
|
+
errors_count = options[:accumulated_errors_count].to_i
|
68
|
+
measure_word = errors_count > 1 ? errors_count : (exception.class.to_s =~ /^[aeiou]/i ? 'An' : 'A')
|
69
|
+
"#{measure_word} #{exception.class.to_s}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_options
|
73
|
+
{
|
74
|
+
sns_prefix: '[ERROR]',
|
75
|
+
backtrace_lines: 10
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'action_dispatch'
|
2
|
+
require 'active_support/core_ext/time'
|
3
|
+
|
4
|
+
module ExceptionNotifier
|
5
|
+
class TeamsNotifier < BaseNotifier
|
6
|
+
include ExceptionNotifier::BacktraceCleaner
|
7
|
+
|
8
|
+
class MissingController
|
9
|
+
def method_missing(*args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :httparty
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
super
|
17
|
+
@default_options = options
|
18
|
+
@httparty = HTTParty
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(exception, options={})
|
22
|
+
@options = options.merge(@default_options)
|
23
|
+
@exception = exception
|
24
|
+
@backtrace = exception.backtrace ? clean_backtrace(exception) : nil
|
25
|
+
|
26
|
+
@env = @options.delete(:env)
|
27
|
+
|
28
|
+
@application_name = @options.delete(:app_name) || Rails.application.class.parent_name.underscore
|
29
|
+
@gitlab_url = @options.delete(:git_url)
|
30
|
+
@jira_url = @options.delete(:jira_url)
|
31
|
+
|
32
|
+
@webhook_url = @options.delete(:webhook_url)
|
33
|
+
raise ArgumentError.new "You must provide 'webhook_url' parameter." unless @webhook_url
|
34
|
+
|
35
|
+
unless @env.nil?
|
36
|
+
@controller = @env['action_controller.instance'] || MissingController.new
|
37
|
+
|
38
|
+
request = ActionDispatch::Request.new(@env)
|
39
|
+
|
40
|
+
@request_items = { url: request.original_url,
|
41
|
+
http_method: request.method,
|
42
|
+
ip_address: request.remote_ip,
|
43
|
+
parameters: request.filtered_parameters,
|
44
|
+
timestamp: Time.current }
|
45
|
+
|
46
|
+
if request.session["warden.user.user.key"]
|
47
|
+
current_user = User.find(request.session["warden.user.user.key"][0][0])
|
48
|
+
@request_items.merge!({ current_user: { id: current_user.id, email: current_user.email } })
|
49
|
+
end
|
50
|
+
else
|
51
|
+
@controller = @request_items = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
payload = message_text
|
55
|
+
|
56
|
+
@options[:body] = payload.to_json
|
57
|
+
@options[:headers] ||= {}
|
58
|
+
@options[:headers].merge!({ 'Content-Type' => 'application/json' })
|
59
|
+
@options[:debug_output] = $stdout
|
60
|
+
|
61
|
+
@httparty.post(@webhook_url, @options)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def message_text
|
67
|
+
errors_count = @options[:accumulated_errors_count].to_i
|
68
|
+
|
69
|
+
text = {
|
70
|
+
"@type" => "MessageCard",
|
71
|
+
"@context" => "http://schema.org/extensions",
|
72
|
+
"summary" => "#{@application_name} Exception Alert",
|
73
|
+
"title" => "⚠️ Exception Occurred in #{Rails.env} ⚠️",
|
74
|
+
"sections" => [
|
75
|
+
{
|
76
|
+
"activityTitle" => "#{errors_count > 1 ? errors_count : 'A'} *#{@exception.class}* occurred" + if @controller then " in *#{controller_and_method}*." else "." end,
|
77
|
+
"activitySubtitle" => "#{@exception.message}"
|
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 message_request
|
104
|
+
{
|
105
|
+
"name" => "Request",
|
106
|
+
"value" => "#{hash_presentation(@request_items)}\n "
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def message_backtrace(size = 3)
|
111
|
+
text = []
|
112
|
+
size = @backtrace.size < size ? @backtrace.size : size
|
113
|
+
text << "```"
|
114
|
+
size.times { |i| text << "* " + @backtrace[i] }
|
115
|
+
text << "```"
|
116
|
+
|
117
|
+
{
|
118
|
+
"name" => "Backtrace",
|
119
|
+
"value" => "#{text.join(" \n")}"
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def gitlab_view_link
|
124
|
+
{
|
125
|
+
"@type" => "ViewAction",
|
126
|
+
"name" => "🦊 View in GitLab",
|
127
|
+
"target" => [
|
128
|
+
"#{@gitlab_url}/#{@application_name}"
|
129
|
+
]
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def gitlab_issue_link
|
134
|
+
link = [@gitlab_url, @application_name, "issues", "new"].join("/")
|
135
|
+
params = {
|
136
|
+
"issue[title]" => ["[BUG] Error 500 :",
|
137
|
+
controller_and_method,
|
138
|
+
"(#{@exception.class})",
|
139
|
+
@exception.message].compact.join(" ")
|
140
|
+
}.to_query
|
141
|
+
|
142
|
+
{
|
143
|
+
"@type" => "ViewAction",
|
144
|
+
"name" => "🦊 Create Issue in GitLab",
|
145
|
+
"target" => [
|
146
|
+
"#{link}/?#{params}"
|
147
|
+
]
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def jira_issue_link
|
152
|
+
{
|
153
|
+
"@type" => "ViewAction",
|
154
|
+
"name" => "🐞 Create Issue in Jira",
|
155
|
+
"target" => [
|
156
|
+
"#{@jira_url}/secure/CreateIssue!default.jspa"
|
157
|
+
]
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
def controller_and_method
|
162
|
+
if @controller
|
163
|
+
"#{@controller.controller_name}##{@controller.action_name}"
|
164
|
+
else
|
165
|
+
""
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def hash_presentation(hash)
|
170
|
+
text = []
|
171
|
+
|
172
|
+
hash.each do |key, value|
|
173
|
+
text << "* **#{key}** : `#{value}`"
|
174
|
+
end
|
175
|
+
|
176
|
+
text.join(" \n")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
<%= @exception.message %>
|
4
4
|
<%= @backtrace.first %>
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
<% sections = @sections.map do |section|
|
7
|
+
summary = render(section).strip
|
8
|
+
unless summary.blank?
|
9
|
+
title = render("title", :title => section).strip
|
10
|
+
"#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
|
11
|
+
end
|
12
|
+
end.join
|
13
|
+
%>
|
14
|
+
<%= raw sections %>
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
class GoogleChatNotifierTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
test "should send notification if properly configured" do
|
7
|
+
options = {
|
8
|
+
:webhook_url => 'http://localhost:8000'
|
9
|
+
}
|
10
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
11
|
+
google_chat_notifier.httparty = FakeHTTParty.new
|
12
|
+
|
13
|
+
options = google_chat_notifier.call ArgumentError.new("foo"), options
|
14
|
+
|
15
|
+
body = ActiveSupport::JSON.decode options[:body]
|
16
|
+
assert body.has_key? 'text'
|
17
|
+
|
18
|
+
text = body['text'].split("\n")
|
19
|
+
assert_equal 6, text.size
|
20
|
+
assert_equal 'Application: *dummy*', text[1]
|
21
|
+
assert_equal 'An *ArgumentError* occured.', text[2]
|
22
|
+
assert_equal '*foo*', text[5]
|
23
|
+
end
|
24
|
+
|
25
|
+
test "should use 'An' for exceptions count if :accumulated_errors_count option is nil" do
|
26
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
27
|
+
exception = ArgumentError.new("foo")
|
28
|
+
google_chat_notifier.instance_variable_set(:@exception, exception)
|
29
|
+
google_chat_notifier.instance_variable_set(:@options, {})
|
30
|
+
|
31
|
+
assert_includes google_chat_notifier.send(:header), "An *ArgumentError* occured."
|
32
|
+
end
|
33
|
+
|
34
|
+
test "shoud use direct errors count if :accumulated_errors_count option is 5" do
|
35
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
36
|
+
exception = ArgumentError.new("foo")
|
37
|
+
google_chat_notifier.instance_variable_set(:@exception, exception)
|
38
|
+
google_chat_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 })
|
39
|
+
|
40
|
+
assert_includes google_chat_notifier.send(:header), "5 *ArgumentError* occured."
|
41
|
+
end
|
42
|
+
|
43
|
+
test "Message request should be formatted as hash" do
|
44
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
45
|
+
request_items = { url: 'http://test.address',
|
46
|
+
http_method: :get,
|
47
|
+
ip_address: '127.0.0.1',
|
48
|
+
parameters: '{"id"=>"foo"}',
|
49
|
+
timestamp: Time.parse('2018-08-13 12:13:24 UTC') }
|
50
|
+
google_chat_notifier.instance_variable_set(:@request_items, request_items)
|
51
|
+
|
52
|
+
message_request = google_chat_notifier.send(:message_request).join("\n")
|
53
|
+
assert_includes message_request, '* url : http://test.address'
|
54
|
+
assert_includes message_request, '* http_method : get'
|
55
|
+
assert_includes message_request, '* ip_address : 127.0.0.1'
|
56
|
+
assert_includes message_request, '* parameters : {"id"=>"foo"}'
|
57
|
+
assert_includes message_request, '* timestamp : 2018-08-13 12:13:24 UTC'
|
58
|
+
end
|
59
|
+
|
60
|
+
test 'backtrace with less than 3 lines should be displayed fully' do
|
61
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
62
|
+
|
63
|
+
backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"]
|
64
|
+
google_chat_notifier.instance_variable_set(:@backtrace, backtrace)
|
65
|
+
|
66
|
+
message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n")
|
67
|
+
assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'"
|
68
|
+
assert_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'"
|
69
|
+
end
|
70
|
+
|
71
|
+
test 'backtrace with more than 3 lines should display only top 3 lines' do
|
72
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
73
|
+
|
74
|
+
backtrace = ["app/controllers/my_controller.rb:99:in `specific_function'", "app/controllers/my_controller.rb:70:in `specific_param'", "app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"]
|
75
|
+
google_chat_notifier.instance_variable_set(:@backtrace, backtrace)
|
76
|
+
|
77
|
+
message_backtrace = google_chat_notifier.send(:message_backtrace).join("\n")
|
78
|
+
assert_includes message_backtrace, "* app/controllers/my_controller.rb:99:in `specific_function'"
|
79
|
+
assert_includes message_backtrace, "* app/controllers/my_controller.rb:70:in `specific_param'"
|
80
|
+
assert_includes message_backtrace, "* app/controllers/my_controller.rb:53:in `my_controller_params'"
|
81
|
+
assert_not_includes message_backtrace, "* app/controllers/my_controller.rb:34:in `update'"
|
82
|
+
end
|
83
|
+
|
84
|
+
test 'Get text with backtrace and request info' do
|
85
|
+
google_chat_notifier = ExceptionNotifier::GoogleChatNotifier.new
|
86
|
+
|
87
|
+
backtrace = ["app/controllers/my_controller.rb:53:in `my_controller_params'", "app/controllers/my_controller.rb:34:in `update'"]
|
88
|
+
google_chat_notifier.instance_variable_set(:@backtrace, backtrace)
|
89
|
+
|
90
|
+
request_items = { url: 'http://test.address',
|
91
|
+
http_method: :get,
|
92
|
+
ip_address: '127.0.0.1',
|
93
|
+
parameters: '{"id"=>"foo"}',
|
94
|
+
timestamp: Time.parse('2018-08-13 12:13:24 UTC') }
|
95
|
+
google_chat_notifier.instance_variable_set(:@request_items, request_items)
|
96
|
+
|
97
|
+
google_chat_notifier.instance_variable_set(:@options, {accumulated_errors_count: 0})
|
98
|
+
|
99
|
+
google_chat_notifier.instance_variable_set(:@application_name, 'dummy')
|
100
|
+
|
101
|
+
exception = ArgumentError.new("foo")
|
102
|
+
google_chat_notifier.instance_variable_set(:@exception, exception)
|
103
|
+
|
104
|
+
text = google_chat_notifier.send(:exception_text)
|
105
|
+
expected_text = %q(
|
106
|
+
Application: *dummy*
|
107
|
+
An *ArgumentError* occured.
|
108
|
+
|
109
|
+
⚠️ Error 500 in test ⚠️
|
110
|
+
*foo*
|
111
|
+
|
112
|
+
*Request:*
|
113
|
+
```
|
114
|
+
* url : http://test.address
|
115
|
+
* http_method : get
|
116
|
+
* ip_address : 127.0.0.1
|
117
|
+
* parameters : {"id"=>"foo"}
|
118
|
+
* timestamp : 2018-08-13 12:13:24 UTC
|
119
|
+
```
|
120
|
+
|
121
|
+
*Backtrace:*
|
122
|
+
```
|
123
|
+
* app/controllers/my_controller.rb:53:in `my_controller_params'
|
124
|
+
* app/controllers/my_controller.rb:34:in `update'
|
125
|
+
```)
|
126
|
+
assert_equal text, expected_text
|
127
|
+
end
|
128
|
+
end
|
@@ -5,7 +5,7 @@ class ErrorGroupTest < ActiveSupport::TestCase
|
|
5
5
|
setup do
|
6
6
|
module TestModule
|
7
7
|
include ExceptionNotifier::ErrorGrouping
|
8
|
-
@@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/
|
8
|
+
@@error_grouping_cache = ActiveSupport::Cache::FileStore.new("test/dummy/tmp/non_default_location")
|
9
9
|
end
|
10
10
|
|
11
11
|
@exception = RuntimeError.new("ERROR")
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'aws-sdk-sns'
|
3
|
+
|
4
|
+
class SnsNotifierTest < ActiveSupport::TestCase
|
5
|
+
def setup
|
6
|
+
@exception = fake_exception
|
7
|
+
@exception.stubs(:class).returns('MyException')
|
8
|
+
@exception.stubs(:backtrace).returns(fake_backtrace)
|
9
|
+
@exception.stubs(:message).returns("undefined method 'method=' for Empty")
|
10
|
+
@options = {
|
11
|
+
access_key_id: 'my-access_key_id',
|
12
|
+
secret_access_key: 'my-secret_access_key',
|
13
|
+
region: 'us-east',
|
14
|
+
topic_arn: 'topicARN',
|
15
|
+
sns_prefix: '[App Exception]',
|
16
|
+
}
|
17
|
+
Socket.stubs(:gethostname).returns('example.com')
|
18
|
+
end
|
19
|
+
|
20
|
+
# initialize
|
21
|
+
|
22
|
+
test 'should initialize aws notifier with received params' do
|
23
|
+
Aws::SNS::Client.expects(:new).with(
|
24
|
+
region: 'us-east',
|
25
|
+
access_key_id: 'my-access_key_id',
|
26
|
+
secret_access_key: 'my-secret_access_key'
|
27
|
+
)
|
28
|
+
|
29
|
+
ExceptionNotifier::SnsNotifier.new(@options)
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'should raise an exception if region is not received' do
|
33
|
+
@options[:region] = nil
|
34
|
+
|
35
|
+
error = assert_raises ArgumentError do
|
36
|
+
ExceptionNotifier::SnsNotifier.new(@options)
|
37
|
+
end
|
38
|
+
assert_equal "You must provide 'region' option", error.message
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'should raise an exception on publish if access_key_id is not received' do
|
42
|
+
@options[:access_key_id] = nil
|
43
|
+
error = assert_raises ArgumentError do
|
44
|
+
ExceptionNotifier::SnsNotifier.new(@options)
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal "You must provide 'access_key_id' option", error.message
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'should raise an exception on publish if secret_access_key is not received' do
|
51
|
+
@options[:secret_access_key] = nil
|
52
|
+
error = assert_raises ArgumentError do
|
53
|
+
ExceptionNotifier::SnsNotifier.new(@options)
|
54
|
+
end
|
55
|
+
|
56
|
+
assert_equal "You must provide 'secret_access_key' option", error.message
|
57
|
+
end
|
58
|
+
|
59
|
+
# call
|
60
|
+
|
61
|
+
test 'should send a sns notification in background' do
|
62
|
+
Aws::SNS::Client.any_instance.expects(:publish).with(
|
63
|
+
{
|
64
|
+
topic_arn: "topicARN",
|
65
|
+
message: "3 MyException occured in background\n"\
|
66
|
+
"Exception: undefined method 'method=' for Empty\n"\
|
67
|
+
"Hostname: example.com\n"\
|
68
|
+
"Backtrace:\n#{fake_backtrace.join("\n")}\n",
|
69
|
+
subject: "[App Exception] - 3 MyException occurred"
|
70
|
+
})
|
71
|
+
|
72
|
+
sns_notifier = ExceptionNotifier::SnsNotifier.new(@options)
|
73
|
+
sns_notifier.call(@exception, { accumulated_errors_count: 3 })
|
74
|
+
end
|
75
|
+
|
76
|
+
test 'should send a sns notification with controller#action information' do
|
77
|
+
ExamplesController.any_instance.stubs(:action_name).returns('index')
|
78
|
+
|
79
|
+
Aws::SNS::Client.any_instance.expects(:publish).with(
|
80
|
+
{
|
81
|
+
topic_arn: "topicARN",
|
82
|
+
message: "A MyException occurred while GET </examples> "\
|
83
|
+
"was processed by examples#index\n"\
|
84
|
+
"Exception: undefined method 'method=' for Empty\n"\
|
85
|
+
"Hostname: example.com\n"\
|
86
|
+
"Backtrace:\n#{fake_backtrace.join("\n")}\n",
|
87
|
+
subject: "[App Exception] - A MyException occurred"
|
88
|
+
})
|
89
|
+
|
90
|
+
sns_notifier = ExceptionNotifier::SnsNotifier.new(@options)
|
91
|
+
sns_notifier.call(@exception,
|
92
|
+
env: {
|
93
|
+
'REQUEST_METHOD' => 'GET',
|
94
|
+
'REQUEST_URI' => '/examples',
|
95
|
+
'action_controller.instance' => ExamplesController.new
|
96
|
+
}
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
class ExamplesController < ActionController::Base; end
|
103
|
+
|
104
|
+
def fake_exception
|
105
|
+
begin
|
106
|
+
1 / 0
|
107
|
+
rescue Exception => e
|
108
|
+
e
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def fake_exception_without_backtrace
|
113
|
+
StandardError.new('my custom error')
|
114
|
+
end
|
115
|
+
|
116
|
+
def fake_backtrace
|
117
|
+
[
|
118
|
+
'backtrace line 1',
|
119
|
+
'backtrace line 2',
|
120
|
+
'backtrace line 3',
|
121
|
+
'backtrace line 4',
|
122
|
+
'backtrace line 5',
|
123
|
+
'backtrace line 6'
|
124
|
+
]
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
class TeamsNotifierTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
test "should send notification if properly configured" do
|
7
|
+
options = {
|
8
|
+
:webhook_url => 'http://localhost:8000'
|
9
|
+
}
|
10
|
+
teams_notifier = ExceptionNotifier::TeamsNotifier.new
|
11
|
+
teams_notifier.httparty = FakeHTTParty.new
|
12
|
+
|
13
|
+
options = teams_notifier.call ArgumentError.new("foo"), options
|
14
|
+
|
15
|
+
body = ActiveSupport::JSON.decode options[:body]
|
16
|
+
assert body.has_key? 'title'
|
17
|
+
assert body.has_key? 'sections'
|
18
|
+
|
19
|
+
sections = body['sections']
|
20
|
+
header = sections[0]
|
21
|
+
|
22
|
+
assert_equal 2, sections.size
|
23
|
+
assert_equal 'A *ArgumentError* occurred.', header['activityTitle']
|
24
|
+
assert_equal 'foo', header['activitySubtitle']
|
25
|
+
end
|
26
|
+
|
27
|
+
test "should send notification with create gitlab issue link if specified" do
|
28
|
+
options = {
|
29
|
+
:webhook_url => 'http://localhost:8000',
|
30
|
+
:git_url => 'github.com/aschen'
|
31
|
+
}
|
32
|
+
teams_notifier = ExceptionNotifier::TeamsNotifier.new
|
33
|
+
teams_notifier.httparty = FakeHTTParty.new
|
34
|
+
|
35
|
+
options = teams_notifier.call ArgumentError.new("foo"), options
|
36
|
+
|
37
|
+
body = ActiveSupport::JSON.decode options[:body]
|
38
|
+
|
39
|
+
potential_action = body['potentialAction']
|
40
|
+
assert_equal 2, potential_action.size
|
41
|
+
assert_equal '🦊 View in GitLab', potential_action[0]['name']
|
42
|
+
assert_equal '🦊 Create Issue in GitLab', potential_action[1]['name']
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'should add other HTTParty options to params' do
|
46
|
+
options = {
|
47
|
+
:webhook_url => 'http://localhost:8000',
|
48
|
+
:username => "Test Bot",
|
49
|
+
:avatar => 'http://site.com/icon.png',
|
50
|
+
:basic_auth => {
|
51
|
+
:username => 'clara',
|
52
|
+
:password => 'password'
|
53
|
+
}
|
54
|
+
}
|
55
|
+
teams_notifier = ExceptionNotifier::TeamsNotifier.new
|
56
|
+
teams_notifier.httparty = FakeHTTParty.new
|
57
|
+
|
58
|
+
options = teams_notifier.call ArgumentError.new("foo"), options
|
59
|
+
|
60
|
+
assert options.has_key? :basic_auth
|
61
|
+
assert 'clara', options[:basic_auth][:username]
|
62
|
+
assert 'password', options[:basic_auth][:password]
|
63
|
+
end
|
64
|
+
|
65
|
+
test "should use 'A' for exceptions count if :accumulated_errors_count option is nil" do
|
66
|
+
teams_notifier = ExceptionNotifier::TeamsNotifier.new
|
67
|
+
exception = ArgumentError.new("foo")
|
68
|
+
teams_notifier.instance_variable_set(:@exception, exception)
|
69
|
+
teams_notifier.instance_variable_set(:@options, {})
|
70
|
+
|
71
|
+
message_text = teams_notifier.send(:message_text)
|
72
|
+
header = message_text['sections'][0]
|
73
|
+
assert_equal 'A *ArgumentError* occurred.', header['activityTitle']
|
74
|
+
end
|
75
|
+
|
76
|
+
test "should use direct errors count if :accumulated_errors_count option is 5" do
|
77
|
+
teams_notifier = ExceptionNotifier::TeamsNotifier.new
|
78
|
+
exception = ArgumentError.new("foo")
|
79
|
+
teams_notifier.instance_variable_set(:@exception, exception)
|
80
|
+
teams_notifier.instance_variable_set(:@options, { accumulated_errors_count: 5 })
|
81
|
+
message_text = teams_notifier.send(:message_text)
|
82
|
+
header = message_text['sections'][0]
|
83
|
+
assert_equal '5 *ArgumentError* occurred.', header['activityTitle']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class FakeHTTParty
|
88
|
+
|
89
|
+
def post(url, options)
|
90
|
+
return options
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -110,6 +110,35 @@ class ExceptionNotifierTest < ActiveSupport::TestCase
|
|
110
110
|
assert_equal @notifier_calls, 1
|
111
111
|
end
|
112
112
|
|
113
|
+
test "should not send notification if subclass of one of ignored exceptions" do
|
114
|
+
ExceptionNotifier.register_exception_notifier(:test, @test_notifier)
|
115
|
+
|
116
|
+
class StandardErrorSubclass < StandardError
|
117
|
+
end
|
118
|
+
|
119
|
+
exception = StandardErrorSubclass.new
|
120
|
+
|
121
|
+
ExceptionNotifier.notify_exception(exception, {:notifiers => :test})
|
122
|
+
assert_equal @notifier_calls, 1
|
123
|
+
|
124
|
+
ExceptionNotifier.notify_exception(exception, {:notifiers => :test, :ignore_exceptions => 'StandardError' })
|
125
|
+
assert_equal @notifier_calls, 1
|
126
|
+
end
|
127
|
+
|
128
|
+
test "should call received block" do
|
129
|
+
@block_called = false
|
130
|
+
notifier = lambda { |exception, options, &block| block.call }
|
131
|
+
ExceptionNotifier.register_exception_notifier(:test, notifier)
|
132
|
+
|
133
|
+
exception = ExceptionOne.new
|
134
|
+
|
135
|
+
ExceptionNotifier.notify_exception(exception) do
|
136
|
+
@block_called = true
|
137
|
+
end
|
138
|
+
|
139
|
+
assert @block_called
|
140
|
+
end
|
141
|
+
|
113
142
|
test "should not call group_error! or send_notification? if error_grouping false" do
|
114
143
|
exception = StandardError.new
|
115
144
|
ExceptionNotifier.expects(:group_error!).never
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exception_notification
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamis Buck
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2018-11-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: actionmailer
|
@@ -231,6 +231,20 @@ dependencies:
|
|
231
231
|
- - ">="
|
232
232
|
- !ruby/object:Gem::Version
|
233
233
|
version: 1.0.0
|
234
|
+
- !ruby/object:Gem::Dependency
|
235
|
+
name: aws-sdk-sns
|
236
|
+
requirement: !ruby/object:Gem::Requirement
|
237
|
+
requirements:
|
238
|
+
- - "~>"
|
239
|
+
- !ruby/object:Gem::Version
|
240
|
+
version: '1'
|
241
|
+
type: :development
|
242
|
+
prerelease: false
|
243
|
+
version_requirements: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - "~>"
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: '1'
|
234
248
|
description:
|
235
249
|
email: smartinez87@gmail.com
|
236
250
|
executables: []
|
@@ -256,6 +270,7 @@ files:
|
|
256
270
|
- gemfiles/rails4_1.gemfile
|
257
271
|
- gemfiles/rails4_2.gemfile
|
258
272
|
- gemfiles/rails5_0.gemfile
|
273
|
+
- gemfiles/rails5_1.gemfile
|
259
274
|
- lib/exception_notification.rb
|
260
275
|
- lib/exception_notification/rack.rb
|
261
276
|
- lib/exception_notification/rails.rb
|
@@ -265,6 +280,7 @@ files:
|
|
265
280
|
- lib/exception_notifier/base_notifier.rb
|
266
281
|
- lib/exception_notifier/campfire_notifier.rb
|
267
282
|
- lib/exception_notifier/email_notifier.rb
|
283
|
+
- lib/exception_notifier/google_chat_notifier.rb
|
268
284
|
- lib/exception_notifier/hipchat_notifier.rb
|
269
285
|
- lib/exception_notifier/irc_notifier.rb
|
270
286
|
- lib/exception_notifier/mattermost_notifier.rb
|
@@ -272,6 +288,8 @@ files:
|
|
272
288
|
- lib/exception_notifier/modules/error_grouping.rb
|
273
289
|
- lib/exception_notifier/notifier.rb
|
274
290
|
- lib/exception_notifier/slack_notifier.rb
|
291
|
+
- lib/exception_notifier/sns_notifier.rb
|
292
|
+
- lib/exception_notifier/teams_notifier.rb
|
275
293
|
- lib/exception_notifier/views/exception_notifier/_backtrace.html.erb
|
276
294
|
- lib/exception_notifier/views/exception_notifier/_backtrace.text.erb
|
277
295
|
- lib/exception_notifier/views/exception_notifier/_data.html.erb
|
@@ -346,12 +364,15 @@ files:
|
|
346
364
|
- test/exception_notification/rack_test.rb
|
347
365
|
- test/exception_notifier/campfire_notifier_test.rb
|
348
366
|
- test/exception_notifier/email_notifier_test.rb
|
367
|
+
- test/exception_notifier/google_chat_notifier_test.rb
|
349
368
|
- test/exception_notifier/hipchat_notifier_test.rb
|
350
369
|
- test/exception_notifier/irc_notifier_test.rb
|
351
370
|
- test/exception_notifier/mattermost_notifier_test.rb
|
352
371
|
- test/exception_notifier/modules/error_grouping_test.rb
|
353
372
|
- test/exception_notifier/sidekiq_test.rb
|
354
373
|
- test/exception_notifier/slack_notifier_test.rb
|
374
|
+
- test/exception_notifier/sns_notifier_test.rb
|
375
|
+
- test/exception_notifier/teams_notifier_test.rb
|
355
376
|
- test/exception_notifier/webhook_notifier_test.rb
|
356
377
|
- test/exception_notifier_test.rb
|
357
378
|
- test/test_helper.rb
|
@@ -375,7 +396,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
375
396
|
version: 1.8.11
|
376
397
|
requirements: []
|
377
398
|
rubyforge_project:
|
378
|
-
rubygems_version: 2.5.
|
399
|
+
rubygems_version: 2.5.2
|
379
400
|
signing_key:
|
380
401
|
specification_version: 4
|
381
402
|
summary: Exception notification for Rails apps
|
@@ -435,12 +456,15 @@ test_files:
|
|
435
456
|
- test/exception_notification/rack_test.rb
|
436
457
|
- test/exception_notifier/campfire_notifier_test.rb
|
437
458
|
- test/exception_notifier/email_notifier_test.rb
|
459
|
+
- test/exception_notifier/google_chat_notifier_test.rb
|
438
460
|
- test/exception_notifier/hipchat_notifier_test.rb
|
439
461
|
- test/exception_notifier/irc_notifier_test.rb
|
440
462
|
- test/exception_notifier/mattermost_notifier_test.rb
|
441
463
|
- test/exception_notifier/modules/error_grouping_test.rb
|
442
464
|
- test/exception_notifier/sidekiq_test.rb
|
443
465
|
- test/exception_notifier/slack_notifier_test.rb
|
466
|
+
- test/exception_notifier/sns_notifier_test.rb
|
467
|
+
- test/exception_notifier/teams_notifier_test.rb
|
444
468
|
- test/exception_notifier/webhook_notifier_test.rb
|
445
469
|
- test/exception_notifier_test.rb
|
446
470
|
- test/test_helper.rb
|