never-forget 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/never-forget.rb +1 -0
- data/lib/never_forget.rb +23 -0
- data/lib/never_forget/exception.rb +179 -0
- data/lib/never_forget/exception_handler.rb +95 -0
- data/lib/never_forget/list_exceptions.erb +111 -0
- data/lib/never_forget/railtie.rb +26 -0
- data/lib/never_forget/sinatra.rb +17 -0
- metadata +103 -0
data/lib/never-forget.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'never_forget'
|
data/lib/never_forget.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module NeverForget
|
2
|
+
autoload :ExceptionHandler, 'never_forget/exception_handler'
|
3
|
+
autoload :Exception, 'never_forget/exception'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_writer :enabled
|
7
|
+
def enabled?() @enabled end
|
8
|
+
end
|
9
|
+
self.enabled = true
|
10
|
+
|
11
|
+
def self.log(error, env = {}, &block)
|
12
|
+
Exception.create(error, env, &block) if enabled?
|
13
|
+
rescue
|
14
|
+
warn "NeverForget: error saving exception (#{$!.class} #{$!})"
|
15
|
+
warn $!.backtrace.first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'never_forget/railtie' if defined? Rails::Railtie
|
20
|
+
|
21
|
+
if defined?(Sinatra) and Sinatra.respond_to? :register
|
22
|
+
require 'never_forget/sinatra'
|
23
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'mingo'
|
2
|
+
require 'active_support/memoizable'
|
3
|
+
require 'active_support/core_ext/kernel/singleton_class'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'active_support/core_ext/hash'
|
6
|
+
require 'rack/request'
|
7
|
+
require 'rack/utils'
|
8
|
+
|
9
|
+
module NeverForget
|
10
|
+
class Exception < ::Mingo
|
11
|
+
def self.collection_name() "NeverForget" end
|
12
|
+
def self.collection
|
13
|
+
unless defined? @collection
|
14
|
+
db.create_collection collection_name, capped: true, size: 1.megabyte
|
15
|
+
end
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
extend ::ActiveSupport::Memoizable
|
20
|
+
include ::Mingo::Timestamps
|
21
|
+
|
22
|
+
def self.create(error, env)
|
23
|
+
if connected?
|
24
|
+
record = new.init(error, env)
|
25
|
+
yield record if block_given?
|
26
|
+
record.save
|
27
|
+
record
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.recent
|
32
|
+
find.sort('$natural', :desc).limit(20)
|
33
|
+
end
|
34
|
+
|
35
|
+
KEEP = %w[rack.url_scheme action_dispatch.remote_ip]
|
36
|
+
EXCLUDE = %w[HTTP_COOKIE QUERY_STRING SERVER_ADDR]
|
37
|
+
KNOWN_MODULES = %w[
|
38
|
+
ActiveSupport::Dependencies::Blamable
|
39
|
+
JSON::Ext::Generator::GeneratorMethods::Object
|
40
|
+
ActiveSupport::Dependencies::Loadable
|
41
|
+
PP::ObjectMixin
|
42
|
+
Kernel
|
43
|
+
]
|
44
|
+
|
45
|
+
attr_reader :exception, :env
|
46
|
+
|
47
|
+
def init(ex, env_hash)
|
48
|
+
@exception = unwrap_exception(ex)
|
49
|
+
@env = env_hash
|
50
|
+
self['name'] = exception.class.name
|
51
|
+
self['modules'] = tag_modules
|
52
|
+
self['backtrace'] = exception.backtrace.join("\n")
|
53
|
+
self['message'] = exception.message
|
54
|
+
|
55
|
+
if env['REQUEST_METHOD']
|
56
|
+
self['env'] = sanitized_env
|
57
|
+
self['params'] = extract_params
|
58
|
+
self['session'] = extract_session
|
59
|
+
self['cookies'] = extract_cookies
|
60
|
+
else
|
61
|
+
self['params'] = clean_unserializable_data(env)
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def request
|
67
|
+
@request ||= Rack::Request.new(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
def request?
|
71
|
+
self['env'].present?
|
72
|
+
end
|
73
|
+
|
74
|
+
def request_url
|
75
|
+
env = self['env']
|
76
|
+
scheme = env['rack::url_scheme']
|
77
|
+
host, port = env['HTTP_HOST'], env['SERVER_PORT'].to_i
|
78
|
+
host += ":#{port}" if 'http' == scheme && port != 80 or 'https' == scheme && port != 443
|
79
|
+
|
80
|
+
url = scheme + '://' + File.join(host, env['SCRIPT_NAME'], env['PATH_INFO'])
|
81
|
+
url << '?' << Rack::Utils::build_nested_query(self['params']) if get_request? and self['params'].present?
|
82
|
+
url
|
83
|
+
end
|
84
|
+
|
85
|
+
def request_method
|
86
|
+
self['env']['REQUEST_METHOD']
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_request?
|
90
|
+
'GET' == request_method
|
91
|
+
end
|
92
|
+
|
93
|
+
def xhr?
|
94
|
+
self['env']['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
|
95
|
+
end
|
96
|
+
|
97
|
+
def remote_ip
|
98
|
+
self['env']['action_dispatch::remote_ip'] || self['env']['REMOTE_ADDR']
|
99
|
+
end
|
100
|
+
|
101
|
+
def tag_modules
|
102
|
+
Array(exception.singleton_class.included_modules).map(&:to_s) - KNOWN_MODULES
|
103
|
+
end
|
104
|
+
memoize :tag_modules
|
105
|
+
|
106
|
+
def unwrap_exception(exception)
|
107
|
+
if exception.respond_to?(:original_exception)
|
108
|
+
exception.original_exception
|
109
|
+
elsif exception.respond_to?(:continued_exception)
|
110
|
+
exception.continued_exception
|
111
|
+
else
|
112
|
+
exception
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def extract_session
|
117
|
+
if session = env['rack.session']
|
118
|
+
session_hash = session.to_hash.stringify_keys.except('session_id', '_csrf_token')
|
119
|
+
clean_unserializable_data session_hash
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def exclude_params
|
124
|
+
Array(env['action_dispatch.parameter_filter']).map(&:to_s)
|
125
|
+
end
|
126
|
+
memoize :exclude_params
|
127
|
+
|
128
|
+
def extract_params
|
129
|
+
if params = request.params and params.any?
|
130
|
+
filtered = params.each_with_object({}) { |(key, value), keep|
|
131
|
+
keep[key] = exclude_params.include?(key.to_s) ? '[FILTERED]' : value
|
132
|
+
}
|
133
|
+
clean_unserializable_data filtered.except('utf8')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def extract_cookies
|
138
|
+
if cookies = env['rack.request.cookie_hash']
|
139
|
+
if options = env['rack.session.options']
|
140
|
+
cookies = cookies.except(options[:key])
|
141
|
+
end
|
142
|
+
clean_unserializable_data cookies
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def sanitized_env
|
147
|
+
clean_unserializable_data env.select { |key, _| keep_env? key }
|
148
|
+
end
|
149
|
+
|
150
|
+
def keep_env?(key)
|
151
|
+
( key !~ /[a-z]/ or KEEP.include?(key) ) and not discard_env?(key)
|
152
|
+
end
|
153
|
+
|
154
|
+
def discard_env?(key)
|
155
|
+
EXCLUDE.include?(key) or
|
156
|
+
( key == 'REMOTE_ADDR' and env['action_dispatch.remote_ip'] )
|
157
|
+
end
|
158
|
+
|
159
|
+
def sanitize_key(key)
|
160
|
+
key.to_s.gsub('.', '::').sub(/^\$/, 'DOLLAR::')
|
161
|
+
end
|
162
|
+
|
163
|
+
def clean_unserializable_data(data, stack = [])
|
164
|
+
return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id }
|
165
|
+
|
166
|
+
if data.respond_to?(:to_hash)
|
167
|
+
data.to_hash.each_with_object({}) do |(key, value), result|
|
168
|
+
result[sanitize_key(key)] = clean_unserializable_data(value, stack + [data.object_id])
|
169
|
+
end
|
170
|
+
elsif data.respond_to?(:to_ary)
|
171
|
+
data.collect do |value|
|
172
|
+
clean_unserializable_data(value, stack + [data.object_id])
|
173
|
+
end
|
174
|
+
else
|
175
|
+
data.to_s
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'erubis'
|
3
|
+
require 'active_support/memoizable'
|
4
|
+
|
5
|
+
module NeverForget
|
6
|
+
class ExceptionHandler
|
7
|
+
TEMPLATE_FILE = File.expand_path('../list_exceptions.erb', __FILE__)
|
8
|
+
|
9
|
+
def initialize(app, options = {})
|
10
|
+
@app = app
|
11
|
+
@options = {:list_path => '/_exceptions'}.update(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def forward(env)
|
15
|
+
begin
|
16
|
+
@app.call(env)
|
17
|
+
rescue StandardError, ScriptError => error
|
18
|
+
NeverForget.log(error, env)
|
19
|
+
raise error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
if env['PATH_INFO'] == File.join('/', @options[:list_path])
|
25
|
+
body = render_exceptions_view
|
26
|
+
[200, {'content-type' => 'text/html'}, [body]]
|
27
|
+
else
|
28
|
+
forward(env)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_exceptions_view
|
33
|
+
template = Erubis::Eruby.new(exceptions_template)
|
34
|
+
context = Erubis::Context.new(:recent => Exception.recent)
|
35
|
+
context.extend TemplateHelpers
|
36
|
+
template.evaluate(context)
|
37
|
+
end
|
38
|
+
|
39
|
+
def exceptions_template
|
40
|
+
File.read(TEMPLATE_FILE)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module TemplateHelpers
|
45
|
+
include RbConfig
|
46
|
+
extend ActiveSupport::Memoizable
|
47
|
+
|
48
|
+
def gem_path
|
49
|
+
paths = []
|
50
|
+
paths << Bundler.bundle_path << Bundler.user_bundle_path if defined? Bundler
|
51
|
+
paths << Gem.path if defined? Gem
|
52
|
+
paths.flatten.uniq
|
53
|
+
end
|
54
|
+
|
55
|
+
SYSDIRS = %w[ vendor site rubylib arch sitelib sitearch vendorlib vendorarch top ]
|
56
|
+
|
57
|
+
def system_path
|
58
|
+
SYSDIRS.map { |name| CONFIG["#{name}dir"] }.compact
|
59
|
+
end
|
60
|
+
|
61
|
+
def external_path
|
62
|
+
['/usr/ruby1.9.2', '/home/heroku_rack', gem_path, system_path].flatten.uniq
|
63
|
+
end
|
64
|
+
memoize :external_path
|
65
|
+
|
66
|
+
def collapse_line?(line)
|
67
|
+
external_path.any? {|p| line.start_with? p }
|
68
|
+
end
|
69
|
+
|
70
|
+
def ignore_line?(line)
|
71
|
+
line.include? '/Library/Application Support/Pow/'
|
72
|
+
end
|
73
|
+
|
74
|
+
def strip_root(line)
|
75
|
+
if line =~ %r{/gems/([^/]+)-(\d[\w.]*)/}
|
76
|
+
gem_name, gem_version = $1, $2
|
77
|
+
path = line.split($&, 2).last
|
78
|
+
"#{gem_name} (#{gem_version}) #{path}"
|
79
|
+
elsif path = "#{root_path}/" and line.start_with? path
|
80
|
+
line.sub(path, '')
|
81
|
+
else
|
82
|
+
line
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def root_path
|
87
|
+
if defined? Bundler then Bundler.root
|
88
|
+
elsif defined? Rails then Rails.root
|
89
|
+
elsif defined? Sinatra::Application then Sinatra::Application.root
|
90
|
+
else Dir.pwd
|
91
|
+
end
|
92
|
+
end
|
93
|
+
memoize :root_path
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<meta charset="utf-8">
|
3
|
+
<meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
|
4
|
+
<title>Exceptions</title>
|
5
|
+
<style>
|
6
|
+
body {
|
7
|
+
margin: 2em;
|
8
|
+
font: medium/1.6 Helvetica, sans-serif;
|
9
|
+
}
|
10
|
+
a:link, a:visited { color: darkblue; }
|
11
|
+
a:hover { color: crimson; }
|
12
|
+
.url, .ua { margin: .2em 0; }
|
13
|
+
.ua { color: gray; font-size: 85%; }
|
14
|
+
ol.trace {
|
15
|
+
margin: .5em 0;
|
16
|
+
padding: 0;
|
17
|
+
list-style: none;
|
18
|
+
font-size: 85%;
|
19
|
+
line-height: 1.2;
|
20
|
+
}
|
21
|
+
.trace .num { font-weight: bold; }
|
22
|
+
.trace .sys { display: none; }
|
23
|
+
.trace .expand a {
|
24
|
+
font-size: 80%; text-transform: uppercase;
|
25
|
+
}
|
26
|
+
.trace.expanded .sys { display: list-item; }
|
27
|
+
.trace.expanded .expand { display: none; }
|
28
|
+
|
29
|
+
h1 { margin: 0; padding: 0; }
|
30
|
+
article {
|
31
|
+
margin-top: 1.5em;
|
32
|
+
padding-top: 1em;
|
33
|
+
border-top: 2px solid #eee;
|
34
|
+
}
|
35
|
+
h2 {
|
36
|
+
margin: 0 0 .2em 0;
|
37
|
+
padding: 0 0 .1em 0;
|
38
|
+
display: inline-block;
|
39
|
+
}
|
40
|
+
time {
|
41
|
+
display: inline-block;
|
42
|
+
font: 80% "American Typewriter", monospace;
|
43
|
+
color: gray;
|
44
|
+
margin-left: 1em;
|
45
|
+
}
|
46
|
+
h2 span { font-weight: normal; }
|
47
|
+
h3 { margin: .5em 0 .2em 0; padding: .1em 0; font-size: 1.1em; }
|
48
|
+
pre { margin: .2em; }
|
49
|
+
|
50
|
+
@media only screen and (max-device-width:480px) {
|
51
|
+
body { margin: 7px; font-size: small; }
|
52
|
+
h1 { font-size: 22px; text-align: center; margin: 0; padding: 0; }
|
53
|
+
h2 { font-size: 18px; }
|
54
|
+
article { margin-top: .2em; padding-top: .2em; padding-bottom: .5em; }
|
55
|
+
}
|
56
|
+
</style>
|
57
|
+
|
58
|
+
<h1>Exceptions</h1>
|
59
|
+
|
60
|
+
<% if @recent.has_next? %>
|
61
|
+
<% for ex in @recent %>
|
62
|
+
<article>
|
63
|
+
<header><h2><%= ex['name'] %>: <span><%== ex['message'] %></span></h2>
|
64
|
+
<time><%== ex.created_at.strftime('%a, %b %e %T') %></time>
|
65
|
+
</header>
|
66
|
+
|
67
|
+
<% if ex.request? %>
|
68
|
+
<% url = ex.request_url %>
|
69
|
+
<p class="url">
|
70
|
+
<% if ex.xhr? %>Ajax<% end %> <%= ex.request_method %>
|
71
|
+
<a href="<%== url %>"><%== url.split('://').last %></a>
|
72
|
+
</p>
|
73
|
+
<p class="ua">
|
74
|
+
<span class="ip"><%= ex.remote_ip %></span> —
|
75
|
+
<%== ex['env']['HTTP_USER_AGENT'] %>
|
76
|
+
</p>
|
77
|
+
<% end %>
|
78
|
+
|
79
|
+
<% if ex['params'].present? %>
|
80
|
+
<h3>Params:</h3>
|
81
|
+
<pre class="params"><%== YAML.dump ex['params'] %></pre>
|
82
|
+
<% end %>
|
83
|
+
|
84
|
+
<% if ex['session'].present? %>
|
85
|
+
<h3>Session:</h3>
|
86
|
+
<pre class="session"><%== YAML.dump ex['session'] %></pre>
|
87
|
+
<% end %>
|
88
|
+
|
89
|
+
<h3>Backtrace:</h3>
|
90
|
+
<ol class="trace">
|
91
|
+
<% for line in ex['backtrace'].split("\n") %>
|
92
|
+
<% next if ignore_line? line %>
|
93
|
+
<li<%= collapse_line?(line) ? ' class="sys"' : '' %>><%= strip_root(line).sub(/:(\d+):/, ' <span class="num">\1</span> ') %></li>
|
94
|
+
<% end %>
|
95
|
+
<li class="expand"><a href="#expand">full trace</a></li>
|
96
|
+
</ol>
|
97
|
+
</article>
|
98
|
+
<% end %>
|
99
|
+
|
100
|
+
<script>
|
101
|
+
document.addEventListener('click', function(e) {
|
102
|
+
var el = e.target
|
103
|
+
if (el.nodeName == 'A' && el.getAttribute('href') == '#expand') {
|
104
|
+
e.preventDefault()
|
105
|
+
el.parentNode.parentNode.className += " expanded"
|
106
|
+
}
|
107
|
+
}, false)
|
108
|
+
</script>
|
109
|
+
<% else %>
|
110
|
+
<p><i>No exceptions captured yet.</i></p>
|
111
|
+
<% end %>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NeverForget
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
config.never_forget = ActiveSupport::OrderedOptions.new
|
4
|
+
config.never_forget.enabled = ::Rails.env.production? || ::Rails.env.staging?
|
5
|
+
config.never_forget.list_path = '/_exceptions'
|
6
|
+
|
7
|
+
initializer "never_forget" do |app|
|
8
|
+
if NeverForget.enabled = app.config.never_forget.enabled
|
9
|
+
app.config.middleware.insert_after 'ActionDispatch::ShowExceptions',
|
10
|
+
ExceptionHandler, :list_path => app.config.never_forget.list_path
|
11
|
+
|
12
|
+
::ActiveSupport.on_load(:action_controller) { include NeverForget::ControllerRescue }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ControllerRescue
|
18
|
+
def rescue_with_handler(exception)
|
19
|
+
if super
|
20
|
+
# the exception was handled, but we still want to save it
|
21
|
+
NeverForget.log(exception, request.env)
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module NeverForget
|
2
|
+
module Sinatra
|
3
|
+
module Helpers
|
4
|
+
def log_error(boom = $!, env = nil)
|
5
|
+
env = request.env if env.nil? and respond_to? :request
|
6
|
+
NeverForget.log(boom, env || {})
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.registered(app)
|
11
|
+
app.use NeverForget::ExceptionHandler
|
12
|
+
app.helpers Helpers
|
13
|
+
end
|
14
|
+
|
15
|
+
::Sinatra.register self
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: never-forget
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- "Mislav Marohni\xC4\x87"
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-09-28 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mingo
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.2"
|
24
|
+
type: :runtime
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activesupport
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rack
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id003
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: erubis
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id004
|
59
|
+
description: Never Forget is a layer of persistence for exceptions thrown in a Rack application at runtime.
|
60
|
+
email: mislav.marohnic@gmail.com
|
61
|
+
executables: []
|
62
|
+
|
63
|
+
extensions: []
|
64
|
+
|
65
|
+
extra_rdoc_files: []
|
66
|
+
|
67
|
+
files:
|
68
|
+
- lib/never-forget.rb
|
69
|
+
- lib/never_forget/exception.rb
|
70
|
+
- lib/never_forget/exception_handler.rb
|
71
|
+
- lib/never_forget/list_exceptions.erb
|
72
|
+
- lib/never_forget/railtie.rb
|
73
|
+
- lib/never_forget/sinatra.rb
|
74
|
+
- lib/never_forget.rb
|
75
|
+
homepage: https://github.com/mislav/never-forget
|
76
|
+
licenses: []
|
77
|
+
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: "0"
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.8.8
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: Saves exceptions to MongoDB
|
102
|
+
test_files: []
|
103
|
+
|