guardrail_notifier 0.2.11
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +20 -0
- data/README.rdoc +98 -0
- data/Rakefile +40 -0
- data/guardrail_notifier.gemspec +74 -0
- data/lib/guardrail_notifier.rb +37 -0
- data/lib/guardrail_notifier/configuration.rb +33 -0
- data/lib/guardrail_notifier/rails/action_controller_monitor.rb +125 -0
- data/lib/guardrail_notifier/sender.rb +43 -0
- data/lib/guardrail_notifier/version.rb +3 -0
- data/test/guardrail_test.rb +156 -0
- data/test/helper.rb +35 -0
- data/test/sender_test.rb +75 -0
- metadata +193 -0
data/.document
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
actionpack (3.2.8)
|
5
|
+
activemodel (= 3.2.8)
|
6
|
+
activesupport (= 3.2.8)
|
7
|
+
builder (~> 3.0.0)
|
8
|
+
erubis (~> 2.7.0)
|
9
|
+
journey (~> 1.0.4)
|
10
|
+
rack (~> 1.4.0)
|
11
|
+
rack-cache (~> 1.2)
|
12
|
+
rack-test (~> 0.6.1)
|
13
|
+
sprockets (~> 2.1.3)
|
14
|
+
activemodel (3.2.8)
|
15
|
+
activesupport (= 3.2.8)
|
16
|
+
builder (~> 3.0.0)
|
17
|
+
activesupport (3.2.8)
|
18
|
+
i18n (~> 0.6)
|
19
|
+
multi_json (~> 1.0)
|
20
|
+
builder (3.0.3)
|
21
|
+
erubis (2.7.0)
|
22
|
+
fakeweb (1.3.0)
|
23
|
+
git (1.2.5)
|
24
|
+
hike (1.2.1)
|
25
|
+
i18n (0.6.1)
|
26
|
+
jeweler (1.8.4)
|
27
|
+
bundler (~> 1.0)
|
28
|
+
git (>= 1.2.5)
|
29
|
+
rake
|
30
|
+
rdoc
|
31
|
+
journey (1.0.4)
|
32
|
+
json (1.7.5)
|
33
|
+
metaclass (0.0.1)
|
34
|
+
mocha (0.12.7)
|
35
|
+
metaclass (~> 0.0.1)
|
36
|
+
multi_json (1.3.6)
|
37
|
+
rack (1.4.1)
|
38
|
+
rack-cache (1.2)
|
39
|
+
rack (>= 0.4)
|
40
|
+
rack-test (0.6.2)
|
41
|
+
rack (>= 1.0)
|
42
|
+
rake (0.9.2.2)
|
43
|
+
rdoc (3.12)
|
44
|
+
json (~> 1.4)
|
45
|
+
shoulda (3.3.0)
|
46
|
+
shoulda-context (~> 1.0)
|
47
|
+
shoulda-matchers (~> 1.4)
|
48
|
+
shoulda-context (1.0.0)
|
49
|
+
shoulda-matchers (1.4.0)
|
50
|
+
activesupport (>= 3.0.0)
|
51
|
+
sprockets (2.1.3)
|
52
|
+
hike (~> 1.2)
|
53
|
+
rack (~> 1.0)
|
54
|
+
tilt (~> 1.1, != 1.3.0)
|
55
|
+
tilt (1.3.3)
|
56
|
+
|
57
|
+
PLATFORMS
|
58
|
+
ruby
|
59
|
+
|
60
|
+
DEPENDENCIES
|
61
|
+
actionpack (= 3.2.8)
|
62
|
+
fakeweb
|
63
|
+
jeweler
|
64
|
+
json
|
65
|
+
mocha
|
66
|
+
rake
|
67
|
+
shoulda
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Terrible Labs, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
= GuardrailNotifier
|
2
|
+
|
3
|
+
{<img src="https://travis-ci.org/terriblelabs/guardrail_notifier.png" />}[https://travis-ci.org/terriblelabs/guardrail_notifier]
|
4
|
+
|
5
|
+
Stop hurting your users!
|
6
|
+
|
7
|
+
Guardrail captures validation errors from your Ruby on Rails application to help you identify and fix user experience issues. The GuardrailNotifier gem makes it easy to hook up your app to the Guardrail web service.
|
8
|
+
|
9
|
+
If you haven't already signed up for a Guardrail account, visit http://guardrailapp.com.
|
10
|
+
|
11
|
+
|
12
|
+
= Installation
|
13
|
+
|
14
|
+
=== Rails 3
|
15
|
+
|
16
|
+
* Add the following line to Gemfile:
|
17
|
+
|
18
|
+
gem 'guardrail_notifier'
|
19
|
+
|
20
|
+
* Install the gem:
|
21
|
+
|
22
|
+
bundle install
|
23
|
+
|
24
|
+
=== Rails 2.3
|
25
|
+
|
26
|
+
* Add the following line to config/environment.rb:
|
27
|
+
|
28
|
+
config.gem 'guardrail_notifier'
|
29
|
+
|
30
|
+
* Install the gem:
|
31
|
+
|
32
|
+
[sudo] rake gems:install GEM=guardrail_notifier
|
33
|
+
|
34
|
+
= Configuration
|
35
|
+
|
36
|
+
In your Rails app, create config/initializers/guardrail_notifier.rb and add the following line with the API key for your Guardrail project:
|
37
|
+
|
38
|
+
GuardrailNotifier.configure do |config|
|
39
|
+
config.api_key = "<YOUR API KEY>"
|
40
|
+
end
|
41
|
+
|
42
|
+
Or, if you prefer to keep API keys and the like out of your code, set the GUARDRAIL_API_KEY environment variable and GuardrailNotifier will find it automatically.
|
43
|
+
|
44
|
+
Validation errors will now be submitted to the Guardrail service on your create and update actions in production mode.
|
45
|
+
|
46
|
+
= Advanced Usage
|
47
|
+
|
48
|
+
=== Tracking custom actions
|
49
|
+
|
50
|
+
GuardrailNotifier assumes your app is RESTful, so it only monitors the create and update actions by default. If you're creating or updating records in non-RESTful actions, you can use an after_filter to make sure any validation failures in those actions are caught as well:
|
51
|
+
|
52
|
+
after_filter :log_validation_failures_to_guardrail, :only => [:create, :update, :my_custom_action]
|
53
|
+
|
54
|
+
=== Running in other environments
|
55
|
+
|
56
|
+
GuardrailNotifier is only enabled in the production environment by default. If you want to enable it in additional environments, such as staging, you'll need to add the following line to your Guardrail initializer:
|
57
|
+
|
58
|
+
GuardrailNotifier.configure do |config|
|
59
|
+
config.monitored_environments << 'staging'
|
60
|
+
end
|
61
|
+
|
62
|
+
=== SSL
|
63
|
+
|
64
|
+
GuardrailNotifier uses a secure connection over SSL by default, but you can easily disable it if you prefer:
|
65
|
+
|
66
|
+
GuardrailNotifier.configure do |config|
|
67
|
+
config.secure = false
|
68
|
+
end
|
69
|
+
|
70
|
+
=== Filtering params
|
71
|
+
|
72
|
+
GuardrailNotifier records the parameters submitted in actions that cause validation errors. Because your app may handle sensitive data, like passwords, credit card numbers, and nuclear launch codes, GuardrailNotifier respects the parameter filtering that you configure in Rails in config/application.rb (Rails 3) or ApplicationController (Rails 2.3). For example in Rails 3:
|
73
|
+
|
74
|
+
config.filter_parameters += [:password, :secret, :cvv]
|
75
|
+
|
76
|
+
Per Rails convention, those params will show up as "[FILTERED]" in the data submitted to Guardrail.
|
77
|
+
|
78
|
+
|
79
|
+
== Contact information
|
80
|
+
|
81
|
+
* Twitter: @guardrailapp
|
82
|
+
* Email: jeremy@guardrailapp.com, jeff@guardrailapp.com
|
83
|
+
|
84
|
+
|
85
|
+
== Note on Patches/Pull Requests
|
86
|
+
|
87
|
+
* Fork the project.
|
88
|
+
* Make your feature addition or bug fix.
|
89
|
+
* Add tests for it. This is important so I don't break it in a
|
90
|
+
future version unintentionally.
|
91
|
+
* Commit, do not mess with Rakefile, version, or history.
|
92
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
93
|
+
* Send me a pull request. Bonus points for topic branches.
|
94
|
+
|
95
|
+
== Copyright
|
96
|
+
|
97
|
+
Copyright (c) 2010 Terrible Labs, Inc. See LICENSE for details.
|
98
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require File.dirname(__FILE__) + "/lib/guardrail_notifier/version.rb"
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.version = GuardrailNotifier::VERSION
|
9
|
+
gem.name = "guardrail_notifier"
|
10
|
+
gem.summary = %Q{Guardrail (http://guardrailapp.com) captures validation errors from your Ruby on Rails application.}
|
11
|
+
gem.description = %Q{Guardrail captures validation errors from your Ruby on Rails application to help you identify and fix user experience issues. The GuardrailNotifier gem makes it easy to hook up your app to the Guardrail web service.}
|
12
|
+
gem.email = "support@guardrailapp.com"
|
13
|
+
gem.homepage = "http://github.com/terriblelabs/guardrail_notifier"
|
14
|
+
gem.authors = ["Jeffrey Chupp", "Jeremy Weiskotten"]
|
15
|
+
gem.add_development_dependency "shoulda", ">= 0"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rake/testtask'
|
24
|
+
Rake::TestTask.new(:test) do |test|
|
25
|
+
test.libs << 'lib' << 'test'
|
26
|
+
test.pattern = 'test/**/*_test.rb'
|
27
|
+
test.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
task :default => :test
|
31
|
+
|
32
|
+
require 'rdoc/task'
|
33
|
+
Rake::RDocTask.new do |rdoc|
|
34
|
+
version = GuardrailNotifier::VERSION
|
35
|
+
|
36
|
+
rdoc.rdoc_dir = 'rdoc'
|
37
|
+
rdoc.title = "guardrail_notifier #{version}"
|
38
|
+
rdoc.rdoc_files.include('README*')
|
39
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
40
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "guardrail_notifier"
|
8
|
+
s.version = "0.2.11"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jeffrey Chupp", "Jeremy Weiskotten"]
|
12
|
+
s.date = "2012-10-12"
|
13
|
+
s.description = "Guardrail captures validation errors from your Ruby on Rails application to help you identify and fix user experience issues. The GuardrailNotifier gem makes it easy to hook up your app to the Guardrail web service."
|
14
|
+
s.email = "support@guardrailapp.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"guardrail_notifier.gemspec",
|
27
|
+
"lib/guardrail_notifier.rb",
|
28
|
+
"lib/guardrail_notifier/configuration.rb",
|
29
|
+
"lib/guardrail_notifier/rails/action_controller_monitor.rb",
|
30
|
+
"lib/guardrail_notifier/sender.rb",
|
31
|
+
"lib/guardrail_notifier/version.rb",
|
32
|
+
"test/guardrail_test.rb",
|
33
|
+
"test/helper.rb",
|
34
|
+
"test/sender_test.rb"
|
35
|
+
]
|
36
|
+
s.homepage = "http://github.com/terriblelabs/guardrail_notifier"
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = "1.8.23"
|
39
|
+
s.summary = "Guardrail (http://guardrailapp.com) captures validation errors from your Ruby on Rails application."
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_development_dependency(%q<actionpack>, ["= 3.2.8"])
|
46
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
47
|
+
s.add_development_dependency(%q<fakeweb>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
49
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
50
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
51
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
52
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<actionpack>, ["= 3.2.8"])
|
55
|
+
s.add_dependency(%q<json>, [">= 0"])
|
56
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
57
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
58
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
59
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
60
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
61
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<actionpack>, ["= 3.2.8"])
|
65
|
+
s.add_dependency(%q<json>, [">= 0"])
|
66
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
67
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
68
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
69
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
70
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
71
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
require 'guardrail_notifier/version'
|
5
|
+
require 'guardrail_notifier/configuration'
|
6
|
+
require 'guardrail_notifier/sender'
|
7
|
+
require 'guardrail_notifier/rails/action_controller_monitor'
|
8
|
+
|
9
|
+
module GuardrailNotifier
|
10
|
+
API_VERSION = "alpha 1"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def configure
|
18
|
+
yield(self.configuration)
|
19
|
+
end
|
20
|
+
|
21
|
+
def sender
|
22
|
+
@sender ||= Sender.new(self.configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def notify(data)
|
26
|
+
self.sender.send_to_guardrail(data.merge(notifier_params))
|
27
|
+
end
|
28
|
+
|
29
|
+
def notifier_params
|
30
|
+
@notifier_params ||= {
|
31
|
+
:notifier_version => self.configuration.notifier_version,
|
32
|
+
:api_key => self.configuration.api_key || ENV['GUARDRAIL_API_KEY'],
|
33
|
+
:api_version => API_VERSION
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module GuardrailNotifier
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :api_key
|
4
|
+
|
5
|
+
# The set of environments that should be monitored for validation errors (defaults to
|
6
|
+
# only the production environment).
|
7
|
+
attr_accessor :monitored_environments
|
8
|
+
|
9
|
+
# Number of seconds after which submission to Guardrail should timeout (defaults to 5).
|
10
|
+
attr_accessor :timeout_in_seconds
|
11
|
+
|
12
|
+
# +true+ for https connections, +false+ for http connections.
|
13
|
+
attr_accessor :secure
|
14
|
+
alias_method :secure?, :secure
|
15
|
+
|
16
|
+
# A +call+able object, such as a Proc, to be invoked if an exception occurs when
|
17
|
+
# logging to Guardrail (defaults to nil). For example, to notify Hoptoad:
|
18
|
+
#
|
19
|
+
# config.on_exception = proc { |e| notify_hoptoad(e) }
|
20
|
+
attr_accessor :on_exception
|
21
|
+
|
22
|
+
# The version of the notifier (defaults to the version of this gem).
|
23
|
+
attr_reader :notifier_version
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@notifier_version = VERSION
|
27
|
+
@timeout_in_seconds = 5
|
28
|
+
@monitored_environments = ['production']
|
29
|
+
@secure = true
|
30
|
+
@on_exception = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module GuardrailNotifier
|
2
|
+
module Rails
|
3
|
+
# This module is mixed into ActionController::Base.
|
4
|
+
module ActionControllerMonitor
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.after_filter :log_validation_failures_to_guardrail, :only => [:create, :update]
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def log_validation_failures_to_guardrail
|
13
|
+
if should_log_failures_to_guardrail? && records_with_errors.present?
|
14
|
+
GuardrailNotifier.notify(guardrail_params)
|
15
|
+
end
|
16
|
+
rescue Exception => e
|
17
|
+
::Rails.logger.error("Failed to log validation failure to Guardrail")
|
18
|
+
|
19
|
+
handler = GuardrailNotifier.configuration.on_exception
|
20
|
+
handler.call(e) if !handler.nil? && handler.respond_to?(:call)
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_log_failures_to_guardrail?
|
24
|
+
GuardrailNotifier.configuration.monitored_environments.include?(::Rails.env.to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def records_with_errors
|
28
|
+
@records_with_errors ||= begin
|
29
|
+
instance_variable_names.map do |var_name|
|
30
|
+
var = instance_variable_get(var_name)
|
31
|
+
if var.is_a?(Array)
|
32
|
+
var.select{|v| record_has_errors?(v)}
|
33
|
+
else
|
34
|
+
var if record_has_errors?(var)
|
35
|
+
end
|
36
|
+
end.flatten.compact
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def record_has_errors?(record)
|
41
|
+
record.respond_to?(:errors) && record.errors.present?
|
42
|
+
end
|
43
|
+
|
44
|
+
def guardrail_params
|
45
|
+
{}.tap do |query|
|
46
|
+
query[:_controller] = params['controller']
|
47
|
+
query[:_action] = params['action']
|
48
|
+
query[:path] = request.path
|
49
|
+
query[:data] = request_data.to_json
|
50
|
+
|
51
|
+
query[:failures] = records_with_errors.map do |record|
|
52
|
+
error_hashes(record)
|
53
|
+
end.flatten.to_json
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def error_hashes(record)
|
58
|
+
record.errors.map do |field, messages|
|
59
|
+
Array.wrap(messages).map do |message|
|
60
|
+
error_hash(record.class, field, message)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def error_hash(clazz, field, message)
|
66
|
+
{
|
67
|
+
:model => clazz.to_s,
|
68
|
+
:field => field,
|
69
|
+
:message => message
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_data
|
74
|
+
{}.tap do |data|
|
75
|
+
# TODO: limit this in size - how big?
|
76
|
+
data[:params] = filtered_params # TODO: controller and action are redundant
|
77
|
+
|
78
|
+
if respond_to?(:current_user) && !current_user.nil?
|
79
|
+
data[:current_user] = current_user.id
|
80
|
+
end
|
81
|
+
|
82
|
+
data[:user_agent] = request.user_agent
|
83
|
+
data[:cookies] = stringify_values(request.cookies)
|
84
|
+
data[:session] = stringify_values(request.session)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def filtered_params
|
89
|
+
p = if respond_to?(:filter_parameters)
|
90
|
+
# pre-Rails 3
|
91
|
+
filter_parameters(params)
|
92
|
+
elsif request.respond_to?(:filtered_parameters)
|
93
|
+
# Rails 3
|
94
|
+
request.filtered_parameters
|
95
|
+
else
|
96
|
+
params
|
97
|
+
end
|
98
|
+
filter_files_from_params(p)
|
99
|
+
end
|
100
|
+
|
101
|
+
def filter_files_from_params(params)
|
102
|
+
params.each do |k,v|
|
103
|
+
if v.is_a?(Hash)
|
104
|
+
filter_files_from_params(v)
|
105
|
+
elsif v.is_a?(Tempfile)
|
106
|
+
params[k] = '[FILTERED]'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def stringify_values(values)
|
112
|
+
case values
|
113
|
+
when Array
|
114
|
+
values.map { |v| stringify_values(v) }
|
115
|
+
when Hash
|
116
|
+
Hash[values.map { |k,v| [k, stringify_values(v)] }]
|
117
|
+
else
|
118
|
+
values.to_s
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
ActionController::Base.send(:include, GuardrailNotifier::Rails::ActionControllerMonitor)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module GuardrailNotifier
|
2
|
+
class Sender
|
3
|
+
attr_reader :configuration
|
4
|
+
|
5
|
+
def initialize(configuration)
|
6
|
+
@configuration = configuration
|
7
|
+
end
|
8
|
+
|
9
|
+
def protocol
|
10
|
+
self.configuration.secure? ? 'https' : 'http'
|
11
|
+
end
|
12
|
+
|
13
|
+
def port
|
14
|
+
self.configuration.secure? ? 443 : 80
|
15
|
+
end
|
16
|
+
|
17
|
+
def host
|
18
|
+
'api.guardrailapp.com'
|
19
|
+
end
|
20
|
+
|
21
|
+
def uri
|
22
|
+
URI.parse("#{protocol}://#{host}:#{port}").merge('/')
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_to_guardrail(data)
|
26
|
+
uri = self.uri
|
27
|
+
|
28
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
29
|
+
http.open_timeout = self.configuration.timeout_in_seconds
|
30
|
+
http.use_ssl = self.configuration.secure?
|
31
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
32
|
+
|
33
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
34
|
+
request.set_form_data(data)
|
35
|
+
|
36
|
+
begin
|
37
|
+
response = http.request(request)
|
38
|
+
rescue Exception => ex
|
39
|
+
warn "Could not submit guardrailapp notification: #{ex.class} - #{ex.message}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestGuardrail < Test::Unit::TestCase
|
4
|
+
def fake_controller(klass)
|
5
|
+
fake = klass.new
|
6
|
+
fake.params = {'action' => "foo", 'controller' => "bar"}
|
7
|
+
fake.request = OpenStruct.new(
|
8
|
+
:user_agent => 'FooFox',
|
9
|
+
:cookies => {'one' => 'two', 'three' => 'four'},
|
10
|
+
:session => {'something' => 'ok', 'some_class' => String, 'some_number' => 42, :some_array => [Date, 24]}
|
11
|
+
)
|
12
|
+
fake
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup
|
16
|
+
Rails.stubs(:env => 'production')
|
17
|
+
|
18
|
+
@foo_controller = fake_controller(FooController)
|
19
|
+
|
20
|
+
FakeWeb.register_uri(:post, 'http://api.guardrailapp.com', :body => "")
|
21
|
+
|
22
|
+
GuardrailNotifier.configure do |config|
|
23
|
+
config.api_key = "SOME API KEY"
|
24
|
+
config.monitored_environments = ['production']
|
25
|
+
end
|
26
|
+
|
27
|
+
@model_one = OpenStruct.new(:errors => {"bar" => "is blank"})
|
28
|
+
@model_two = OpenStruct.new(:errors => {"baz" => "is blank"})
|
29
|
+
@model_three = OpenStruct.new(:errors => {"f1" => "is blank","f2" => ["is too large", "is delicious"]})
|
30
|
+
|
31
|
+
@foo_controller.instance_variable_set("@lol", @model_one)
|
32
|
+
@foo_controller.instance_variable_set("@wtf", [@model_two, @model_three])
|
33
|
+
|
34
|
+
# we also set a non-errored model just to ensure that it is not matched
|
35
|
+
@foo_controller.instance_variable_set("@bbq", OpenStruct.new(:errors => []))
|
36
|
+
end
|
37
|
+
|
38
|
+
should "set an api key" do
|
39
|
+
GuardrailNotifier.configure { |c| c.api_key = "Foo" }
|
40
|
+
assert_equal "Foo", GuardrailNotifier.configuration.api_key
|
41
|
+
end
|
42
|
+
|
43
|
+
should "fallback to the ENV api key" do
|
44
|
+
GuardrailNotifier.configure { |c| c.api_key = nil }
|
45
|
+
ENV['GUARDRAIL_API_KEY'] = 'Cupcakes'
|
46
|
+
assert_equal "Cupcakes", GuardrailNotifier.notifier_params[:api_key]
|
47
|
+
end
|
48
|
+
|
49
|
+
should "set secure" do
|
50
|
+
GuardrailNotifier.configure { |c| c.secure = true }
|
51
|
+
assert GuardrailNotifier.configuration.secure?
|
52
|
+
end
|
53
|
+
|
54
|
+
should "set a timeout_in_seconds" do
|
55
|
+
assert_equal 5, GuardrailNotifier.configuration.timeout_in_seconds
|
56
|
+
GuardrailNotifier.configure { |c| c.timeout_in_seconds = 6 }
|
57
|
+
assert_equal 6, GuardrailNotifier.configuration.timeout_in_seconds
|
58
|
+
end
|
59
|
+
|
60
|
+
should "set monitored environments" do
|
61
|
+
assert_equal ["production"], GuardrailNotifier.configuration.monitored_environments
|
62
|
+
GuardrailNotifier.configure { |c| c.monitored_environments = ['stage', 'development'] }
|
63
|
+
assert_equal ['stage', 'development'], GuardrailNotifier.configuration.monitored_environments
|
64
|
+
end
|
65
|
+
|
66
|
+
should "respect monitored environments" do
|
67
|
+
assert @foo_controller.send(:should_log_failures_to_guardrail?)
|
68
|
+
|
69
|
+
GuardrailNotifier.configuration.monitored_environments = ['stage', 'development']
|
70
|
+
assert !@foo_controller.send(:should_log_failures_to_guardrail?)
|
71
|
+
end
|
72
|
+
|
73
|
+
should "identify no errors by default" do
|
74
|
+
assert_equal [], FooController.new.send(:records_with_errors)
|
75
|
+
end
|
76
|
+
|
77
|
+
should "identify records with errors" do
|
78
|
+
assert_same_elements [@model_one, @model_two, @model_three], @foo_controller.send(:records_with_errors)
|
79
|
+
end
|
80
|
+
|
81
|
+
should "create params via guardrail_params" do
|
82
|
+
expected = [
|
83
|
+
{"model"=>"OpenStruct", "field"=>"baz", "message"=>"is blank"},
|
84
|
+
{"model"=>"OpenStruct", "field"=>"f1", "message"=>"is blank"},
|
85
|
+
{"model"=>"OpenStruct", "field"=>"f2", "message"=>"is too large"},
|
86
|
+
{"model"=>"OpenStruct", "field"=>"f2", "message"=>"is delicious"},
|
87
|
+
{"model"=>"OpenStruct", "field"=>"bar", "message"=>"is blank"}
|
88
|
+
]
|
89
|
+
|
90
|
+
params = @foo_controller.send(:guardrail_params)
|
91
|
+
failures = JSON.parse(params[:failures])
|
92
|
+
assert_same_elements expected, failures
|
93
|
+
end
|
94
|
+
|
95
|
+
# should "submit errors via log_validation_failures_to_guardrail" do
|
96
|
+
# Net::HTTP.expects(:post_form).with(URI.parse("http://api.guardrailapp.com:80"), @foo_controller.send(:guardrail_params))
|
97
|
+
# @foo_controller.send(:log_validation_failures_to_guardrail)
|
98
|
+
# end
|
99
|
+
|
100
|
+
should "log controller and action" do
|
101
|
+
assert_equal @foo_controller.params['action'], @foo_controller.send(:guardrail_params)[:_action]
|
102
|
+
assert_equal @foo_controller.params['controller'], @foo_controller.send(:guardrail_params)[:_controller]
|
103
|
+
end
|
104
|
+
|
105
|
+
should "log params" do
|
106
|
+
@foo_controller.params.merge!('lolcats' => "no longer funny")
|
107
|
+
assert_equal @foo_controller.params, JSON.parse(@foo_controller.send(:guardrail_params)[:data])['params']
|
108
|
+
end
|
109
|
+
|
110
|
+
should "filter tempfiles" do
|
111
|
+
Tempfile.open('foo') do |tempfile|
|
112
|
+
tempfile.write('test data')
|
113
|
+
tempfile.rewind
|
114
|
+
|
115
|
+
@foo_controller.params.merge!('value' => 'abc', 'photo' => tempfile, 'nested' => { 'nested_value' => 1, 'nested_photo' => tempfile })
|
116
|
+
params = JSON.parse(@foo_controller.send(:guardrail_params)[:data])['params']
|
117
|
+
assert_equal @foo_controller.params.merge('value' => 'abc', 'photo' => '[FILTERED]', 'nested' => { 'nested_value' => 1, 'nested_photo' => '[FILTERED]' }), params
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
should "log current user's id if the method is exposed" do
|
122
|
+
assert_equal nil, JSON.parse(@foo_controller.send(:guardrail_params)[:data])['current_user']
|
123
|
+
assert_equal 53077, JSON.parse(fake_controller(BarController).send(:guardrail_params)[:data])['current_user']
|
124
|
+
end
|
125
|
+
|
126
|
+
should "handle exceptions" do
|
127
|
+
logger = mock()
|
128
|
+
logger.expects(:error)
|
129
|
+
Rails.stubs(:logger => logger)
|
130
|
+
|
131
|
+
@foo_controller.stubs(:guardrail_params).raises(StandardError)
|
132
|
+
@foo_controller.send(:log_validation_failures_to_guardrail)
|
133
|
+
|
134
|
+
# with a custom exception handler
|
135
|
+
list = []
|
136
|
+
GuardrailNotifier.configure { |c| c.on_exception = proc { |e| list << e } }
|
137
|
+
|
138
|
+
logger.expects(:error)
|
139
|
+
@foo_controller.send(:log_validation_failures_to_guardrail)
|
140
|
+
|
141
|
+
assert_equal 1, list.size
|
142
|
+
assert_kind_of StandardError, list.first
|
143
|
+
end
|
144
|
+
|
145
|
+
[:cookies, :user_agent].each do |kind|
|
146
|
+
should "log #{kind}" do
|
147
|
+
assert @foo_controller.request.send(kind).present?
|
148
|
+
assert_equal @foo_controller.request.send(kind), JSON.parse(fake_controller(BarController).send(:guardrail_params)[:data])[kind.to_s]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
should "log session" do
|
153
|
+
expected = {'something'=>'ok', 'some_class'=>'String', 'some_number'=>'42', 'some_array'=>['Date', '24']}
|
154
|
+
assert_equal expected, JSON.parse(fake_controller(BarController).send(:guardrail_params)[:data])['session']
|
155
|
+
end
|
156
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'shoulda'
|
5
|
+
gem 'actionpack', '~>3.2'
|
6
|
+
require 'action_controller'
|
7
|
+
require 'fakeweb'
|
8
|
+
require 'mocha'
|
9
|
+
require 'tempfile'
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
12
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
13
|
+
require 'guardrail_notifier'
|
14
|
+
|
15
|
+
class Test::Unit::TestCase
|
16
|
+
end
|
17
|
+
|
18
|
+
class FooController < ActionController::Base
|
19
|
+
end
|
20
|
+
|
21
|
+
class User
|
22
|
+
attr_accessor :id
|
23
|
+
def initialize
|
24
|
+
@id = 53077
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class BarController < ActionController::Base
|
29
|
+
def current_user
|
30
|
+
User.new
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Rails
|
35
|
+
end
|
data/test/sender_test.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class SenderTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
GuardrailNotifier.configure do |c|
|
6
|
+
c.api_key = 'API_KEY'
|
7
|
+
end
|
8
|
+
@sender = GuardrailNotifier::Sender.new(GuardrailNotifier.configuration)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "initialize" do
|
12
|
+
should "set configuration" do
|
13
|
+
assert_same GuardrailNotifier.configuration, @sender.configuration
|
14
|
+
assert_equal 'API_KEY', @sender.configuration.api_key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when not configured to be secure" do
|
19
|
+
setup do
|
20
|
+
GuardrailNotifier.configuration.secure = false
|
21
|
+
end
|
22
|
+
|
23
|
+
should "return 'http' for #protocol" do
|
24
|
+
assert_equal 'http', @sender.protocol
|
25
|
+
end
|
26
|
+
|
27
|
+
should "return 80 for #port" do
|
28
|
+
assert_equal 80, @sender.port
|
29
|
+
end
|
30
|
+
|
31
|
+
should "post data without ssl on insecure port" do
|
32
|
+
assert_sends_to_guardrail(false, 80)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when configured to be secure" do
|
37
|
+
setup do
|
38
|
+
GuardrailNotifier.configuration.secure = true
|
39
|
+
end
|
40
|
+
|
41
|
+
should "return 'https' for protocol" do
|
42
|
+
assert_equal 'https', @sender.protocol
|
43
|
+
end
|
44
|
+
|
45
|
+
should "return 443 for #port" do
|
46
|
+
assert_equal 443, @sender.port
|
47
|
+
end
|
48
|
+
|
49
|
+
should "post data with ssl on secure port" do
|
50
|
+
assert_sends_to_guardrail(true, 443)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
should "return api.guardrailapp.com for #host" do
|
55
|
+
assert_equal 'api.guardrailapp.com', @sender.host
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def assert_sends_to_guardrail(use_ssl, port)
|
60
|
+
data = {:key => :value}
|
61
|
+
|
62
|
+
mock_request = mock()
|
63
|
+
mock_request.expects(:set_form_data).with(data)
|
64
|
+
Net::HTTP::Post.stubs(:new => mock_request)
|
65
|
+
|
66
|
+
mock_http = mock()
|
67
|
+
mock_http.expects(:open_timeout=)
|
68
|
+
mock_http.expects(:use_ssl=).with(use_ssl)
|
69
|
+
mock_http.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
|
70
|
+
mock_http.expects(:request).with(mock_request)
|
71
|
+
Net::HTTP.stubs(:new => mock_http)
|
72
|
+
|
73
|
+
@sender.send_to_guardrail(data)
|
74
|
+
end
|
75
|
+
end
|
metadata
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: guardrail_notifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.11
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jeffrey Chupp
|
9
|
+
- Jeremy Weiskotten
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-10-12 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: actionpack
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - '='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.2.8
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - '='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 3.2.8
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: json
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: fakeweb
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: shoulda
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: mocha
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rake
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: jeweler
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: shoulda
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
description: Guardrail captures validation errors from your Ruby on Rails application
|
144
|
+
to help you identify and fix user experience issues. The GuardrailNotifier gem makes
|
145
|
+
it easy to hook up your app to the Guardrail web service.
|
146
|
+
email: support@guardrailapp.com
|
147
|
+
executables: []
|
148
|
+
extensions: []
|
149
|
+
extra_rdoc_files:
|
150
|
+
- LICENSE
|
151
|
+
- README.rdoc
|
152
|
+
files:
|
153
|
+
- .document
|
154
|
+
- Gemfile
|
155
|
+
- Gemfile.lock
|
156
|
+
- LICENSE
|
157
|
+
- README.rdoc
|
158
|
+
- Rakefile
|
159
|
+
- guardrail_notifier.gemspec
|
160
|
+
- lib/guardrail_notifier.rb
|
161
|
+
- lib/guardrail_notifier/configuration.rb
|
162
|
+
- lib/guardrail_notifier/rails/action_controller_monitor.rb
|
163
|
+
- lib/guardrail_notifier/sender.rb
|
164
|
+
- lib/guardrail_notifier/version.rb
|
165
|
+
- test/guardrail_test.rb
|
166
|
+
- test/helper.rb
|
167
|
+
- test/sender_test.rb
|
168
|
+
homepage: http://github.com/terriblelabs/guardrail_notifier
|
169
|
+
licenses: []
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ! '>='
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
none: false
|
182
|
+
requirements:
|
183
|
+
- - ! '>='
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
requirements: []
|
187
|
+
rubyforge_project:
|
188
|
+
rubygems_version: 1.8.23
|
189
|
+
signing_key:
|
190
|
+
specification_version: 3
|
191
|
+
summary: Guardrail (http://guardrailapp.com) captures validation errors from your
|
192
|
+
Ruby on Rails application.
|
193
|
+
test_files: []
|