asana_exception_notifier 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +26 -0
  3. data/.reek +11 -0
  4. data/.rubocop.yml +76 -0
  5. data/.travis.yml +16 -0
  6. data/CONTRIBUTING.md +46 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +180 -0
  9. data/LICENSE +20 -0
  10. data/README.md +225 -0
  11. data/Rakefile +37 -0
  12. data/asana_exception_notifier.gemspec +48 -0
  13. data/examples/sinatra/Gemfile +7 -0
  14. data/examples/sinatra/Gemfile.lock +124 -0
  15. data/examples/sinatra/Procfile +1 -0
  16. data/examples/sinatra/README.md +14 -0
  17. data/examples/sinatra/config.ru +3 -0
  18. data/examples/sinatra/sinatra_app.rb +46 -0
  19. data/init.rb +1 -0
  20. data/lib/asana_exception_notifier/classes/asana.rb +95 -0
  21. data/lib/asana_exception_notifier/classes/error_page.rb +134 -0
  22. data/lib/asana_exception_notifier/helpers/application_helper.rb +286 -0
  23. data/lib/asana_exception_notifier/initializers/zip.rb +14 -0
  24. data/lib/asana_exception_notifier/note_templates/asana_exception_notifier.html.erb +82 -0
  25. data/lib/asana_exception_notifier/note_templates/asana_exception_notifier.text.erb +24 -0
  26. data/lib/asana_exception_notifier/request/client.rb +85 -0
  27. data/lib/asana_exception_notifier/request/core.rb +132 -0
  28. data/lib/asana_exception_notifier/request/middleware.rb +41 -0
  29. data/lib/asana_exception_notifier/version.rb +27 -0
  30. data/lib/asana_exception_notifier.rb +45 -0
  31. data/lib/generators/asana_exception_notifier/install_generator.rb +14 -0
  32. data/lib/generators/asana_exception_notifier/templates/asana_exception_notifier.rb +20 -0
  33. data/spec/lib/asana_exception_notifier/classes/asana_spec.rb +23 -0
  34. data/spec/spec_helper.rb +45 -0
  35. data/spec/test_notification.rb +20 -0
  36. metadata +467 -0
@@ -0,0 +1,286 @@
1
+ module AsanaExceptionNotifier
2
+ # module that is used for formatting numbers using metrics
3
+ module ApplicationHelper
4
+ # function that makes the methods incapsulated as utility functions
5
+
6
+ module_function
7
+
8
+ def permitted_options
9
+ {
10
+ asana_api_key: nil,
11
+ workspace: nil,
12
+ assignee: nil,
13
+ assignee_status: nil,
14
+ due_at: nil,
15
+ due_on: nil,
16
+ hearted: false,
17
+ hearts: [],
18
+ projects: [],
19
+ followers: [],
20
+ memberships: [],
21
+ tags: [],
22
+ notes: '',
23
+ name: '',
24
+ template_path: default_template_path
25
+ }
26
+ end
27
+
28
+ def multi_request_manager
29
+ @multi_manager ||= EventMachine::MultiRequest.new
30
+ end
31
+
32
+ def extract_body(env)
33
+ return if env.blank? || !env.is_a?(Hash)
34
+ io = env['rack.input']
35
+ io.rewind if io.respond_to?(:rewind)
36
+ io.read
37
+ end
38
+
39
+ def show_hash_content(hash)
40
+ hash.map do |key, value|
41
+ value.is_a?(Hash) ? show_hash_content(value) : ["#{key}:", value]
42
+ end.join("\n ")
43
+ end
44
+
45
+ def tempfile_details(tempfile)
46
+ file_details = get_extension_and_name_from_file(tempfile)
47
+ {
48
+ file: tempfile,
49
+ path: tempfile.path
50
+ }.merge(file_details)
51
+ end
52
+
53
+ # Returns utf8 encoding of the msg
54
+ # @param [String] msg
55
+ # @return [String] ReturnsReturns utf8 encoding of the msg
56
+ def force_utf8_encoding(msg)
57
+ msg.respond_to?(:force_encoding) && msg.encoding.name != 'UTF-8' ? msg.force_encoding('UTF-8') : msg
58
+ end
59
+
60
+ # returns the logger used to log messages and errors
61
+ #
62
+ # @return [Logger]
63
+ #
64
+ # @api public
65
+ def logger
66
+ @logger ||= defined?(Rails) ? Rails.logger : ExceptionNotifier.logger
67
+ end
68
+
69
+ def ensure_eventmachine_running(&block)
70
+ Thread.abort_on_exception = true
71
+ register_em_error_handler
72
+ run_em_reactor(&block)
73
+ end
74
+
75
+ def register_em_error_handler
76
+ EM.error_handler do |error|
77
+ logger.debug '[AsanaExceptionNotifier]: Error during event loop :'
78
+ logger.debug "[AsanaExceptionNotifier]: #{log_exception(error)}"
79
+ end
80
+ end
81
+
82
+ def log_exception(exception)
83
+ logger.debug exception.inspect
84
+ log_bactrace(exception) if exception.respond_to?(:backtrace)
85
+ end
86
+
87
+ def log_bactrace(exception)
88
+ logger.debug exception.backtrace.join("\n")
89
+ end
90
+
91
+ def execute_with_rescue(options = {})
92
+ yield if block_given?
93
+ rescue Interrupt
94
+ rescue_interrupt
95
+ rescue => error
96
+ log_exception(error)
97
+ options.fetch(:value, '')
98
+ end
99
+
100
+ def rescue_interrupt
101
+ `stty icanon echo`
102
+ puts "\n Command was cancelled due to an Interrupt error."
103
+ end
104
+
105
+ def run_em_reactor
106
+ Thread.new do
107
+ EM.run do
108
+ EM.defer proc { yield if block_given? }
109
+ end
110
+ end.join
111
+ end
112
+
113
+ def template_dir
114
+ File.expand_path(File.join(root, 'note_templates'))
115
+ end
116
+
117
+ def default_template_path
118
+ File.join(template_dir, 'asana_exception_notifier.html.erb')
119
+ end
120
+
121
+ def template_path_exist(path)
122
+ return path if File.exist?(path)
123
+ fail ArgumentError, "file #{path} doesn't exist"
124
+ end
125
+
126
+ def max_length(rows, index)
127
+ value = rows.max_by { |array| array[index].to_s.size }
128
+ value.is_a?(Array) ? value[index] : value
129
+ end
130
+
131
+ def get_hash_rows(hash, rows = [], prefix = '')
132
+ hash.each do |key, value|
133
+ if value.is_a?(Hash)
134
+ get_hash_rows(value, rows, key)
135
+ else
136
+ rows.push(["#{prefix}#{key}".inspect, escape(value.inspect)])
137
+ end
138
+ end
139
+ rows
140
+ end
141
+
142
+ def link_helper(link)
143
+ <<-LINK
144
+ <a href="javascript:void(0)" onclick="AjaxExceptionNotifier.hideAllAndToggle('#{link.downcase}')">#{link.camelize}</a>
145
+ LINK
146
+ end
147
+
148
+ def escape(text)
149
+ text.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
150
+ end
151
+
152
+ def set_fieldset_key(links, prefix)
153
+ links[prefix] ||= {}
154
+ prefix
155
+ end
156
+
157
+ def parse_fieldset_value(options)
158
+ value = options[:value]
159
+ value.is_a?(Hash) ? value.reject! { |_new_key, new_value| new_value.is_a?(Hash) } : value
160
+ end
161
+
162
+ # Gets a bidimensional array and create a table.
163
+ # The first array is used as label.
164
+ #
165
+ def mount_table(array, options = {})
166
+ return '' if array.blank?
167
+ header = array.shift
168
+
169
+ header = header.map { |name| escape(name.to_s.humanize) }
170
+ rows = array.map { |name| "<tr><td>#{name.join('</td><td>')}</td></tr>" }
171
+
172
+ <<-TABLE
173
+ <table #{hash_to_html_attributes(options)}>
174
+ <thead><tr><th>#{header.join('</th><th>')}</th></tr></thead>
175
+ <tbody>#{rows.join}</tbody>
176
+ </table>
177
+ TABLE
178
+ end
179
+
180
+ # Mount table for hash, using name and value and adding a name_value class
181
+ # to the generated table.
182
+ #
183
+ def mount_table_for_hash(hash, options = {})
184
+ return if hash.blank?
185
+ rows = get_hash_rows(hash, options.fetch('rows', []))
186
+ mount_table(rows.unshift(%w(Name Value)), { class: 'name_values' }.merge(options))
187
+ end
188
+
189
+ def hash_to_html_attributes(hash)
190
+ hash.map do |key, value|
191
+ "#{key}=\"#{value.gsub('"', '\"')}\" "
192
+ end.join(' ')
193
+ end
194
+
195
+ # returns the root path of the gem
196
+ #
197
+ # @return [void]
198
+ #
199
+ # @api public
200
+ def root
201
+ File.expand_path(File.dirname(__dir__))
202
+ end
203
+
204
+ def get_extension_and_name_from_file(tempfile)
205
+ path = tempfile.respond_to?(:path) ? tempfile.path : tempfile
206
+ pathname = Pathname.new(path)
207
+ extension = pathname.extname
208
+ {
209
+ extension: extension,
210
+ filename: File.basename(pathname, extension),
211
+ file_path: path
212
+ }
213
+ end
214
+
215
+ def setup_em_options(options)
216
+ options.symbolize_keys!
217
+ options[:em_request] ||= {}
218
+ options
219
+ end
220
+
221
+ def create_upload_file_part(file)
222
+ Part.new(name: 'file',
223
+ body: force_utf8_encoding(File.read(file)),
224
+ filename: file,
225
+ content_type: 'application/zip'
226
+ )
227
+ end
228
+
229
+ def multipart_file_upload_details(file)
230
+ boundary = "---------------------------#{rand(10_000_000_000_000_000_000)}"
231
+ body = MultipartBody.new([create_upload_file_part(file)], boundary)
232
+ file_upload_request_options(boundary, body, file)
233
+ end
234
+
235
+ def file_upload_request_options(boundary, body, file)
236
+ {
237
+ body: body.to_s,
238
+ head:
239
+ {
240
+ 'Content-Type' => "multipart/form-data;boundary=#{boundary}",
241
+ 'Content-Length' => File.size(file),
242
+ 'Expect' => '100-continue'
243
+ }
244
+ }
245
+ end
246
+
247
+ def get_response_from_request(http, _options)
248
+ http.respond_to?(:response) ? http.response : http.responses
249
+ end
250
+
251
+ def get_multi_request_values(http_response, key)
252
+ response_method = key.to_s == 'callback' ? 'response' : 'error'
253
+ http_response[key.to_sym].values.map { |request| request.send(response_method) }.reject(&:blank?)
254
+ end
255
+
256
+ def split_archive(archive, partial_name, segment_size)
257
+ indexes = Zip::File.split(archive, segment_size, true, partial_name)
258
+ archives = Array.new(indexes) do |index|
259
+ File.join(File.dirname(archive), "#{partial_name}.zip.#{format('%03d', index + 1)}")
260
+ end if indexes.present?
261
+ archives.blank? ? [archive] : archives
262
+ end
263
+
264
+ def compress_files(directory, name, files)
265
+ archive = create_archive(directory, name)
266
+ ::Zip::File.open(archive, Zip::File::CREATE) do |zipfile|
267
+ add_files_to_zip(zipfile, files)
268
+ end
269
+ archive
270
+ end
271
+
272
+ def add_files_to_zip(zipfile, files)
273
+ files.each do |file|
274
+ zipfile.add(file.sub(File.dirname(file) + '/', ''), file)
275
+ end
276
+ end
277
+
278
+ def create_archive(directory, name)
279
+ archive = File.join(directory, name + '.zip')
280
+ archive_dir = File.dirname(archive)
281
+ FileUtils.mkdir_p(archive_dir) unless File.directory?(archive_dir)
282
+ FileUtils.rm archive, force: true if File.exist?(archive)
283
+ archive
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,14 @@
1
+ Zip.setup do |c|
2
+ c.on_exists_proc = true
3
+ c.continue_on_exists_proc = true
4
+ c.unicode_names = false
5
+ c.default_compression = Zlib::BEST_COMPRESSION
6
+ end
7
+
8
+ Zip::File.class_eval do
9
+ singleton_class.send(:alias_method, :original_get_segment_size_for_split, :get_segment_size_for_split)
10
+
11
+ def self.get_segment_size_for_split(segment_size)
12
+ segment_size
13
+ end
14
+ end
@@ -0,0 +1,82 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style type="text/css">
5
+ #asana_exception_notifier {font-size: 11px; font-family: Consolas, monaco, monospace; font-weight: normal; margin: 2em 0 1em 0; text-align: center; color: #444; line-height: 16px; background: #fff;}
6
+ #asana_exception_notifier th, #asana_exception_notifier td {color: #444; line-height: 18px;}
7
+ #asana_exception_notifier a {color: #9b1b1b; font-weight: inherit; text-decoration: none; line-height: 18px;}
8
+ #asana_exception_notifier table {text-align: left; width: 100%;}
9
+ #asana_exception_notifier table td {padding: 5px; border-bottom: 1px solid #ccc;}
10
+ #asana_exception_notifier table td strong {color: #9b1b1b;}
11
+ #asana_exception_notifier table th {padding: 5px; border-bottom: 1px solid #ccc;}
12
+ #asana_exception_notifier table tr:nth-child(2n) td {background: #eee;}
13
+ #asana_exception_notifier table tr:nth-child(2n + 1) td {background: #fff;}
14
+ #asana_exception_notifier tbody {text-align: left;}
15
+ #asana_exception_notifier .name_values td {vertical-align: top;}
16
+ #asana_exception_notifier legend {background-color: #fff;}
17
+ #asana_exception_notifier fieldset {text-align: left; border: 1px dashed #aaa; padding: 0.5em 1em 1em 1em; margin: 1em 2em; color: #444; background-color: #FFF;}
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div style="clear:both"></div>
22
+ <div id="asana_exception_notifier">
23
+ <div id="all">
24
+ Show: <%= fieldsets_links %>
25
+ <% fieldsets.each do |key, value| %>
26
+ <fieldset class='ajax_exception_notifier_fieldset' id='<%= key %>' style="<%= fieldsets.keys.first != key ? "display:none" : '' %>">
27
+ <legend>Debug Information for <%= key.to_s.camelize %></legend>
28
+ <div>
29
+ <%= mount_table_for_hash(value) %>
30
+ </div>
31
+ </fieldset>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+ <script type="text/javascript">
36
+ var AjaxExceptionNotifier = function() {
37
+ function hideAll(){
38
+ fields = document.getElementsByClassName('ajax_exception_notifier_fieldset')
39
+ for (index = 0; index < fields.length; ++index) {
40
+ AjaxExceptionNotifier.hide(fields[index]);
41
+ }
42
+ }
43
+ function hideAllAndToggle(id) {
44
+ var n = note(id);
45
+ var display = n.style.display;
46
+ hideAll();
47
+ // Restore original display to allow toggling
48
+ n.style.display = display;
49
+ toggle(id)
50
+ location.href = '#ajax_exception_notifier';
51
+ }
52
+ function note(id) {
53
+ console.log(id);
54
+ return (document.getElementById(id));
55
+ }
56
+ function toggle(id){
57
+ var el = note(id);
58
+ if (el.style.display == 'none') {
59
+ AjaxExceptionNotifier.show(el);
60
+ } else {
61
+ AjaxExceptionNotifier.hide(el);
62
+ }
63
+ }
64
+ function show(element) {
65
+ element.style.display = 'block'
66
+ }
67
+ function hide(element) {
68
+ element.style.display = 'none'
69
+ }
70
+ return {
71
+ show: show,
72
+ hide: hide,
73
+ toggle: toggle,
74
+ hideAll: hideAll,
75
+ node: note,
76
+ hideAllAndToggle: hideAllAndToggle
77
+ }
78
+ }();
79
+ /* Additional Javascript */
80
+ </script>
81
+ </body>
82
+ </html>
@@ -0,0 +1,24 @@
1
+ A <%= exception.class.to_s %> occured: <%= exception.message.to_s %>
2
+ <% if exception.respond_to?(:backtrace) && exception.backtrace.present? %>
3
+ File: <%= exception.backtrace.slice(0, 1).join("\n ") %>
4
+ <% end %>
5
+ <% if request_data[:ip_address].present? %>
6
+ IP: <%= request_data[:ip_address] %>
7
+ <% end %>
8
+ <% if request_data[:session].present? %>
9
+ Session: <%= request_data[:session]%>
10
+ <% end %>
11
+ <% if request_data[:url].present? %>
12
+ Request URI: <%= request_data[:url] %>
13
+ <% end %>
14
+ <% if request_data[:referrer].present? %>
15
+ Referer: <%= request_data[:referrer] %>
16
+ <% end %>
17
+ PID: <%= process %>
18
+ PWD: <%= pwd %>
19
+ Uname: <%= uname %>
20
+ Server: <%= server %>
21
+ Error time: <%= timestamp %>
22
+ <% if request_data[:user_agent].present? %>
23
+ User Agent: <%= request_data[:user_agent] %>
24
+ <% end %>
@@ -0,0 +1,85 @@
1
+ require_relative './core'
2
+ module AsanaExceptionNotifier
3
+ module Request
4
+ # class used to make request in deferrable way
5
+ class Client
6
+ include AsanaExceptionNotifier::Request::Core
7
+ include EM::Deferrable
8
+
9
+ attr_reader :url, :options, :api_key, :request_name, :request_final, :action
10
+
11
+ def initialize(api_key, url, options, &callback)
12
+ @api_key = api_key
13
+ @url = url
14
+
15
+ @options = options.symbolize_keys
16
+ @request_name = @options.fetch(:request_name, '')
17
+ @request_final = @options.fetch(:request_final, false)
18
+ @action = @options.fetch(:action, '')
19
+
20
+ self.callback(&callback)
21
+
22
+ send_request_and_rescue
23
+ end
24
+
25
+ def multi_manager
26
+ @multi_manager ||= options.fetch(:multi_manager, nil)
27
+ end
28
+
29
+ def em_request_options
30
+ request = setup_em_options(@options).delete(:em_request)
31
+ params = {
32
+ head: (request[:head] || {}).merge(
33
+ 'Authorization' => "Bearer #{@api_key}"
34
+ ),
35
+ body: request[:body]
36
+ }
37
+ super(params)
38
+ end
39
+
40
+ def send_request_and_rescue
41
+ @http = em_request(@url, @options)
42
+ send_request
43
+ rescue => exception
44
+ log_exception(exception)
45
+ fail(result: { message: exception })
46
+ end
47
+
48
+ def send_request
49
+ fetch_data(@options) do |http_response|
50
+ handle_all_responses(http_response)
51
+ end
52
+ end
53
+
54
+ def handle_all_responses(http_response)
55
+ @multi_manager.requests.delete(@http) if @multi_manager.present?
56
+ if http_response.is_a?(Hash) && %i(callback errback).all? { |key| http_response.symbolize_keys.keys.include?(key) }
57
+ handle_multi_response(http_response)
58
+ else
59
+ handle_response(http_response, @options.fetch(:request_name, ''))
60
+ end
61
+ end
62
+
63
+ def handle_multi_response(http_response)
64
+ logger.debug('[AsanaExceptionNotifier]: Handling multi responses')
65
+ get_multi_request_values(http_response, :callback).each { |request_name, response| handle_response(response, request_name) }
66
+ get_multi_request_values(http_response, :errback).each { |request_name, response| handle_error(response, request_name) }
67
+ end
68
+
69
+ def handle_error(error, key = '')
70
+ logger.debug("[AsanaExceptionNotifier]: Task #{key} #{@action} returned: #{error}")
71
+ fail(error)
72
+ end
73
+
74
+ def handle_response(http_response, key = '')
75
+ logger.debug("[AsanaExceptionNotifier]: Task #{key} #{@action} returned: #{http_response}")
76
+ data = JSON.parse(http_response)
77
+ callback_task_creation(data)
78
+ end
79
+
80
+ def callback_task_creation(data)
81
+ data.fetch('errors', {}).present? ? handle_error(data) : succeed(data)
82
+ end
83
+ end
84
+ end
85
+ end