exception_notification 4.2.2 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|