hoptoad-ribbit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## EDITOR
5
+ *.kpf
6
+ *.tmproj
7
+ tmtags
8
+
9
+ ## EMACS
10
+ *~
11
+ \#*
12
+ .\#*
13
+
14
+ ## VIM
15
+ *.swp
16
+
17
+ ## PROJECT::GENERAL
18
+ coverage
19
+ rdoc
20
+ pkg
21
+
22
+ ## PROJECT::SPECIFIC
23
+ *.ignore
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2007, Tammer Saleh, Thoughtbot, Inc.
2
+ Copyright (c) 2009 Glen Mailer
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ = Ribbit
2
+
3
+ This Hoptoad Notifier attempts to remove anything rails specific from the
4
+ original notifier released by Thoughtbot.
5
+
6
+ My intention is to have a completely rails-free core - and then provide
7
+ adapters for popular frameworks - firstly Merb as thats what I'm using,
8
+ and then subsequently things like Rails and Rack.
9
+
10
+ == Note on Patches/Pull Requests
11
+
12
+ * Fork the project.
13
+ * Make your feature addition or bug fix.
14
+ * Add tests for it. This is important so I don't break it in a
15
+ future version unintentionally.
16
+ * Commit, do not mess with rakefile, version, or history.
17
+ (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)
18
+ * Send me a pull request. Bonus points for topic branches.
19
+
20
+ == Copyright
21
+
22
+ Copyright (c) 2009 Glen Mailer. See LICENSE for details.
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "hoptoad-ribbit"
8
+ gem.summary = "Generic Hoptoad Notifications and an adapter for merb"
9
+ gem.description = %q{
10
+ Provides a Module to handle sending notifications to hoptoad.
11
+
12
+ Currently includes a Merb adapter, but the intention is to add more later.
13
+ }
14
+ gem.email = "glenjamin@gmail.com"
15
+ gem.homepage = "http://github.com/glenjamin/hoptoad-notifier"
16
+ gem.authors = ["Glen Mailer"]
17
+ gem.add_dependency "builder", ">= 2.0.0"
18
+ gem.add_development_dependency "rspec", ">= 1.2.9"
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new(:spec) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.spec_files = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
33
+ spec.libs << 'lib' << 'spec'
34
+ spec.pattern = 'spec/**/*_spec.rb'
35
+ spec.rcov = true
36
+ end
37
+
38
+ task :spec => :check_dependencies
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "ribbit #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{hoptoad-ribbit}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Glen Mailer"]
12
+ s.date = %q{2010-01-05}
13
+ s.description = %q{
14
+ Provides a Module to handle sending notifications to hoptoad.
15
+
16
+ Currently includes a Merb adapter, but the intention is to add more later.
17
+ }
18
+ s.email = %q{glenjamin@gmail.com}
19
+ s.extra_rdoc_files = [
20
+ "LICENSE",
21
+ "README.rdoc"
22
+ ]
23
+ s.files = [
24
+ ".document",
25
+ ".gitignore",
26
+ "LICENSE",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "hoptoad-ribbit.gemspec",
31
+ "lib/ribbit.rb",
32
+ "lib/ribbit/adapters.rb",
33
+ "lib/ribbit/adapters/adapter.rb",
34
+ "lib/ribbit/adapters/merb.rb",
35
+ "lib/ribbit/adapters/none.rb",
36
+ "lib/ribbit/backtrace.rb",
37
+ "lib/ribbit/configuration.rb",
38
+ "lib/ribbit/notice.rb",
39
+ "lib/ribbit/sender.rb",
40
+ "spec/ribbit_spec.rb",
41
+ "spec/spec.opts",
42
+ "spec/spec_helper.rb"
43
+ ]
44
+ s.homepage = %q{http://github.com/glenjamin/hoptoad-notifier}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubygems_version = %q{1.3.5}
48
+ s.summary = %q{Generic Hoptoad Notifications and an adapter for merb}
49
+ s.test_files = [
50
+ "spec/ribbit_spec.rb",
51
+ "spec/spec_helper.rb"
52
+ ]
53
+
54
+ if s.respond_to? :specification_version then
55
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
59
+ s.add_runtime_dependency(%q<builder>, [">= 2.0.0"])
60
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
61
+ else
62
+ s.add_dependency(%q<builder>, [">= 2.0.0"])
63
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
64
+ end
65
+ else
66
+ s.add_dependency(%q<builder>, [">= 2.0.0"])
67
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
68
+ end
69
+ end
70
+
@@ -0,0 +1,100 @@
1
+ require 'ribbit/configuration'
2
+ require 'ribbit/notice'
3
+ require 'ribbit/sender'
4
+ require 'ribbit/backtrace'
5
+ require 'ribbit/adapters'
6
+ require 'ribbit/adapters/adapter'
7
+
8
+ module Ribbit
9
+ CLIENT_NAME = "Ribbit"
10
+ VERSION = "0.1.0.dev"
11
+ API_VERSION = "2.0"
12
+ LOG_PREFIX = "** [Ribbit] "
13
+
14
+ class << self
15
+ # The sender object is responsible for delivering formatted data to the Hoptoad server.
16
+ # Must respond to #send_to_hoptoad. See Ribbit::Sender.
17
+ attr_accessor :sender
18
+
19
+ # A Hoptoad configuration object. Must act like a hash and return sensible
20
+ # values for all Hoptoad configuration options. See Ribbit::Configuration.
21
+ attr_accessor :configuration
22
+
23
+ # The adapter allows different things to happen on different environments
24
+ # Basicially we proxy some methods through to subclasses of
25
+ # Ribbit::Adapters::Adapter
26
+ attr_accessor :adapter
27
+
28
+ # Collection of all existing adapters
29
+ def adapters
30
+ Ribbit::Adapters.adapters
31
+ end
32
+
33
+ # Proxy logger onto the adapter
34
+ def logger
35
+ adapter.logger if adapter
36
+ end
37
+
38
+ # Call this method to modify defaults in your initializers.
39
+ #
40
+ # @example
41
+ # Ribbit.configure do |config|
42
+ # config.api_key = '1234567890abcdef'
43
+ # config.secure = false
44
+ # end
45
+ def configure(silent = false)
46
+ self.configuration ||= Configuration.new
47
+ yield(configuration)
48
+ self.sender = Sender.new(configuration)
49
+ # Attempt to attach an adapter, either by class or name
50
+ if adapters.include? configuration.adapter
51
+ self.adapter = configuration.adapter.new(configuration)
52
+ elsif configuration.adapter
53
+ adapter_class = Ribbit::Adapters.load_adapter configuration.adapter
54
+ self.adapter = adapter_class.new(configuration) rescue nil
55
+ end
56
+ self.adapter.activate! if self.adapter
57
+ end
58
+
59
+ # Sends an exception manually using this method, even when you are not in a controller.
60
+ #
61
+ # @param [Exception] exception The exception you want to notify Hoptoad about.
62
+ # @param [Hash] opts Data that will be sent to Hoptoad.
63
+ #
64
+ # @option opts [String] :api_key The API key for this project. The API key is a unique identifier that Hoptoad uses for identification.
65
+ # @option opts [String] :error_message The error returned by the exception (or the message you want to log).
66
+ # @option opts [String] :backtrace A backtrace, usually obtained with +caller+.
67
+ # @option opts [String] :request The controller's request object.
68
+ # @option opts [String] :session The contents of the user's session.
69
+ # @option opts [String] :environment ENV merged with the contents of the request's environment.
70
+ def notify(exception, opts = {})
71
+ send_notice(build_notice_for(exception, opts))
72
+ end
73
+
74
+ # Sends the notice unless it is one of the default ignored exceptions
75
+ # @see Ribbit.notify
76
+ def notify_or_ignore(exception, opts = {})
77
+ notice = build_notice_for(exception, opts)
78
+ send_notice(notice) unless notice.ignore?
79
+ end
80
+
81
+ private
82
+
83
+ def send_notice(notice)
84
+ if configuration.public?
85
+ sender.send_to_hoptoad(notice.to_xml)
86
+ end
87
+ end
88
+
89
+ def build_notice_for(exception, opts = {})
90
+ if exception.respond_to?(:to_hash)
91
+ opts = opts.merge(exception)
92
+ else
93
+ opts = opts.merge(:exception => exception)
94
+ end
95
+
96
+ Notice.new(configuration.merge(opts))
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ module Ribbit
2
+ module Adapters
3
+ class << self
4
+
5
+ attr_accessor :adapters
6
+
7
+ def adapters
8
+ @adapters ||= []
9
+ end
10
+
11
+ # Register a new adapter - you do not need to call this directly if subclassing Adapter
12
+ def add_adapter(adapter)
13
+ self.adapters << adapter
14
+ end
15
+
16
+ def load_adapter(name)
17
+ require_adapter!(name)
18
+ adapter_class_name = name.to_s.capitalize
19
+ Ribbit::Adapters.const_get(adapter_class_name) rescue nil
20
+ end
21
+
22
+ private
23
+
24
+ def adapter_path(name)
25
+ name = name.to_s.downcase
26
+ File.join(File.dirname(__FILE__), 'adapters', "#{name}.rb")
27
+ end
28
+
29
+ def require_adapter!(name)
30
+ begin
31
+ require adapter_path(name)
32
+ rescue LoadError
33
+ false
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ module Ribbit
2
+ module Adapters
3
+ class Adapter
4
+
5
+ attr_accessor :configuration
6
+
7
+ class << self
8
+ def inherited(klass)
9
+ Ribbit::Adapters.add_adapter klass
10
+ end
11
+ end
12
+
13
+ def initialize(config)
14
+ self.configuration = config
15
+ end
16
+
17
+ # This should provide something that implements the logger interface
18
+ def logger
19
+ raise NotImplementedError
20
+ end
21
+
22
+ # This should perform the steps to integrate hoptoad catching into the framework
23
+ def activate!
24
+ raise NotImplementedError
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ module Ribbit
2
+ module Adapters
3
+ class Merb < Adapter
4
+
5
+ def initialize(config)
6
+ super(config)
7
+ config.environment_name = ::Merb.env
8
+ config.project_root = ::Merb.root
9
+ end
10
+
11
+ def logger
12
+ ::Merb.logger
13
+ end
14
+
15
+ def activate!
16
+ ::Merb::BootLoader.after_app_loads do
17
+ ::Merb::AbstractController.send(:include, ControllerMixin)
18
+ if configuration.automatic?
19
+ ::Merb::Dispatcher::DefaultException.send(:include, ControllerMixin)
20
+ ::Merb::Dispatcher::DefaultException.send(:include, DefaultExceptionExtensions)
21
+ end
22
+ end
23
+ end
24
+
25
+ module ControllerMixin
26
+ protected
27
+
28
+ def notify_hoptoad(ex=nil)
29
+ exceptions = request.exceptions || [ex]
30
+ exceptions.each do |exception|
31
+ Ribbit.notify(exception, {
32
+ :parameters => params,
33
+ :session => session,
34
+ :controller => params[:controller],
35
+ :action => params[:action],
36
+ :url => absolute_url(:this),
37
+ :cgi_data => ENV.to_hash.merge(request.env)
38
+ })
39
+ end
40
+ end
41
+ end # ControllerMixin
42
+
43
+ module DefaultExceptionExtensions
44
+ def self.included(mod)
45
+ mod.class_eval do
46
+ before :notify_hoptoad
47
+ end
48
+ end
49
+ end
50
+
51
+ end # class Merb
52
+ end # module Adapters
53
+ end # module Ribbit
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+
3
+ module Ribbit
4
+ module Adapters
5
+ class None < Adapter
6
+
7
+ # This should provide something that implements the logger interface
8
+ def logger
9
+ @logger ||= Logger.new(STDOUT)
10
+ end
11
+
12
+ # This should perform the steps to integrate hoptoad catching into the framework
13
+ def activate!
14
+
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,99 @@
1
+ module Ribbit
2
+ # Front end to parsing the backtrace for each notice
3
+ class Backtrace
4
+
5
+ # Handles backtrace parsing line by line
6
+ class Line
7
+
8
+ INPUT_FORMAT = %r{^([^:]+):(\d+)(?::in `([^']+)')?$}.freeze
9
+
10
+ # The file portion of the line (such as app/models/user.rb)
11
+ attr_reader :file
12
+
13
+ # The line number portion of the line
14
+ attr_reader :number
15
+
16
+ # The method of the line (such as index)
17
+ attr_reader :method
18
+
19
+ # Parses a single line of a given backtrace
20
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace
21
+ # @return [Line] The parsed backtrace line
22
+ def self.parse(unparsed_line)
23
+ _, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
24
+ new(file, number, method)
25
+ end
26
+
27
+ def initialize(file, number, method)
28
+ self.file = file
29
+ self.number = number
30
+ self.method = method
31
+ end
32
+
33
+ # Reconstructs the line in a readable fashion
34
+ def to_s
35
+ "#{file}:#{number}:in `#{method}'"
36
+ end
37
+
38
+ def ==(other)
39
+ to_s == other.to_s
40
+ end
41
+
42
+ def inspect
43
+ "<Line:#{to_s}>"
44
+ end
45
+
46
+ private
47
+
48
+ attr_writer :file, :number, :method
49
+ end
50
+
51
+ # holder for an Array of Backtrace::Line instances
52
+ attr_reader :lines
53
+
54
+ def self.parse(ruby_backtrace, opts = {})
55
+ ruby_lines = split_multiline_backtrace(ruby_backtrace)
56
+
57
+ filters = opts[:filters] || []
58
+ filtered_lines = ruby_lines.to_a.map do |line|
59
+ filters.inject(line) do |line, proc|
60
+ proc.call(line)
61
+ end
62
+ end.compact
63
+
64
+ lines = filtered_lines.collect do |unparsed_line|
65
+ Line.parse(unparsed_line)
66
+ end
67
+
68
+ instance = new(lines)
69
+ end
70
+
71
+ def initialize(lines)
72
+ self.lines = lines
73
+ end
74
+
75
+ def inspect
76
+ "<Backtrace: " + lines.collect { |line| line.inspect }.join(", ") + ">"
77
+ end
78
+
79
+ def ==(other)
80
+ if other.respond_to?(:lines)
81
+ lines == other.lines
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ attr_writer :lines
90
+
91
+ def self.split_multiline_backtrace(backtrace)
92
+ if backtrace.to_a.size == 1
93
+ backtrace.to_a.first.split(/\n\s*/)
94
+ else
95
+ backtrace
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,234 @@
1
+ module Ribbit
2
+ # Used to set up and modify settings for the notifier.
3
+ class Configuration
4
+
5
+ OPTIONS = [:adapter, :api_key, :automatic, :backtrace_filters,
6
+ :development_environments, :development_lookup, :environment_filters,
7
+ :environment_name, :host, :http_open_timeout, :http_read_timeout,
8
+ :ignore, :ignore_by_filters, :ignore_user_agent, :notifier_name,
9
+ :notifier_url, :notifier_version, :params_filters, :project_root,
10
+ :port, :protocol, :proxy_host, :proxy_pass, :proxy_port, :proxy_user,
11
+ :secure].freeze
12
+
13
+ # The API key for your project, found on the project edit form.
14
+ attr_accessor :api_key
15
+
16
+ # The host to connect to (defaults to hoptoadapp.com).
17
+ attr_accessor :host
18
+
19
+ # The port on which your Hoptoad server runs (defaults to 443 for secure
20
+ # connections, 80 for insecure connections).
21
+ attr_accessor :port
22
+
23
+ # +true+ for https connections, +false+ for http connections.
24
+ attr_accessor :secure
25
+
26
+ # The HTTP open timeout in seconds (defaults to 2).
27
+ attr_accessor :http_open_timeout
28
+
29
+ # The HTTP read timeout in seconds (defaults to 5).
30
+ attr_accessor :http_read_timeout
31
+
32
+ # The hostname of your proxy server (if using a proxy)
33
+ attr_accessor :proxy_host
34
+
35
+ # The port of your proxy server (if using a proxy)
36
+ attr_accessor :proxy_port
37
+
38
+ # The username to use when logging into your proxy server (if using a proxy)
39
+ attr_accessor :proxy_user
40
+
41
+ # The password to use when logging into your proxy server (if using a proxy)
42
+ attr_accessor :proxy_pass
43
+
44
+ # A list of parameters that should be filtered out of what is sent to Hoptoad.
45
+ # By default, all "password" attributes will have their contents replaced.
46
+ attr_reader :params_filters
47
+
48
+ # A list of environment keys that should be filtered out of what is send to Hoptoad.
49
+ # Empty by default.
50
+ attr_reader :environment_filters
51
+
52
+ # A list of filters for cleaning and pruning the backtrace. See #filter_backtrace.
53
+ attr_reader :backtrace_filters
54
+
55
+ # A list of filters for ignoring exceptions. See #ignore_by_filter.
56
+ attr_reader :ignore_by_filters
57
+
58
+ # A list of exception classes to ignore. The array can be appended to.
59
+ attr_reader :ignore
60
+
61
+ # A list of user agents that are being ignored. The array can be appended to.
62
+ attr_reader :ignore_user_agent
63
+
64
+ # A list of environments in which notifications should not be sent.
65
+ attr_accessor :development_environments
66
+
67
+ # +true+ if errors should be caught automatically
68
+ attr_accessor :automatic
69
+ alias_method :automatic?, :automatic
70
+
71
+ # +true+ if you want to check for production errors matching development errors, +false+ otherwise.
72
+ attr_accessor :development_lookup
73
+
74
+ # The name of the environment the application is running in
75
+ attr_accessor :environment_name
76
+
77
+ # The adapter to use to plug functionality into the parent application
78
+ attr_accessor :adapter
79
+
80
+ # The path to the project in which the error occurred, such as the RAILS_ROOT
81
+ attr_accessor :project_root
82
+
83
+ # The name of the notifier library being used to send notifications (such as "Hoptoad Notifier")
84
+ attr_accessor :notifier_name
85
+
86
+ # The version of the notifier library being used to send notifications (such as "1.0.2")
87
+ attr_accessor :notifier_version
88
+
89
+ # The url of the notifier library being used to send notifications
90
+ attr_accessor :notifier_url
91
+
92
+ DEFAULT_PARAMS_FILTERS = %w(password password_confirmation).freeze
93
+
94
+ DEFAULT_BACKTRACE_FILTERS = [
95
+ lambda { |line|
96
+ if defined?(Ribbit.configuration.project_root)
97
+ line.gsub(/#{Ribbit.configuration.project_root}/, "[PROJECT_ROOT]")
98
+ else
99
+ line
100
+ end
101
+ },
102
+ lambda { |line| line.gsub(/^\.\//, "") },
103
+ lambda { |line|
104
+ if defined?(Gem)
105
+ Gem.path.inject(line) do |line, path|
106
+ line.gsub(/#{path}/, "[GEM_ROOT]")
107
+ end
108
+ end
109
+ },
110
+ lambda { |line| line if line !~ %r{lib/ribbit} }
111
+ ].freeze
112
+
113
+ IGNORE_DEFAULT = []
114
+
115
+ # Some of these don't exist for whatever the app is, so we have to consider that.
116
+ IGNORE_DEFAULT.map!{|e| eval(e) rescue nil }.compact!
117
+ IGNORE_DEFAULT.freeze
118
+
119
+ alias_method :secure?, :secure
120
+
121
+ def initialize
122
+ @secure = false
123
+ @host = 'hoptoadapp.com'
124
+ @http_open_timeout = 2
125
+ @http_read_timeout = 5
126
+ @params_filters = DEFAULT_PARAMS_FILTERS.dup
127
+ @environment_filters = []
128
+ @backtrace_filters = DEFAULT_BACKTRACE_FILTERS.dup
129
+ @ignore_by_filters = []
130
+ @ignore = IGNORE_DEFAULT.dup
131
+ @ignore_user_agent = []
132
+ @development_environments = %w(development test)
133
+ @automatic = true
134
+ @development_lookup = true
135
+ @notifier_name = CLIENT_NAME
136
+ @notifier_version = VERSION
137
+ @notifier_url = 'http://hoptoadapp.com'
138
+ end
139
+
140
+ # Takes a block and adds it to the list of backtrace filters. When the filters
141
+ # run, the block will be handed each line of the backtrace and can modify
142
+ # it as necessary.
143
+ #
144
+ # @example
145
+ # config.filter_bracktrace do |line|
146
+ # line.gsub(/^#{Rails.root}/, "[RAILS_ROOT]")
147
+ # end
148
+ #
149
+ # @param [Proc] block The new backtrace filter.
150
+ # @yieldparam [String] line A line in the backtrace.
151
+ def filter_backtrace(&block)
152
+ self.backtrace_filters << block
153
+ end
154
+
155
+ # Takes a block and adds it to the list of ignore filters.
156
+ # When the filters run, the block will be handed the exception.
157
+ # @example
158
+ # config.ignore_by_filter do |exception_data|
159
+ # true if exception_data[:error_class] == "RuntimeError"
160
+ # end
161
+ #
162
+ # @param [Proc] block The new ignore filter
163
+ # @yieldparam [Hash] data The exception data given to +Ribbit.notify+
164
+ # @yieldreturn [Boolean] If the block returns true the exception will be ignored, otherwise it will be processed by hoptoad.
165
+ def ignore_by_filter(&block)
166
+ self.ignore_by_filters << block
167
+ end
168
+
169
+ # Overrides the list of default ignored errors.
170
+ #
171
+ # @param [Array<Exception>] names A list of exceptions to ignore.
172
+ def ignore_only=(names)
173
+ @ignore = [names].flatten
174
+ end
175
+
176
+ # Overrides the list of default ignored user agents
177
+ #
178
+ # @param [Array<String>] A list of user agents to ignore
179
+ def ignore_user_agent_only=(names)
180
+ @ignore_user_agent = [names].flatten
181
+ end
182
+
183
+ # Allows config options to be read like a hash
184
+ #
185
+ # @param [Symbol] option Key for a given attribute
186
+ def [](option)
187
+ send(option)
188
+ end
189
+
190
+ # Returns a hash of all configurable options
191
+ def to_hash
192
+ OPTIONS.inject({}) do |hash, option|
193
+ hash.merge(option.to_sym => send(option))
194
+ end
195
+ end
196
+
197
+ # Returns a hash of all configurable options merged with +hash+
198
+ #
199
+ # @param [Hash] hash A set of configuration options that will take precedence over the defaults
200
+ def merge(hash)
201
+ to_hash.merge(hash)
202
+ end
203
+
204
+ # Determines if the notifier will send notices.
205
+ # @return [Boolean] Returns +false+ if in a development environment, +true+ otherwise.
206
+ def public?
207
+ !development_environments.include?(environment_name)
208
+ end
209
+
210
+ def port
211
+ @port || default_port
212
+ end
213
+
214
+ def protocol
215
+ if secure?
216
+ 'https'
217
+ else
218
+ 'http'
219
+ end
220
+ end
221
+
222
+ private
223
+
224
+ def default_port
225
+ if secure?
226
+ 443
227
+ else
228
+ 80
229
+ end
230
+ end
231
+
232
+ end
233
+
234
+ end
@@ -0,0 +1,283 @@
1
+ require 'builder'
2
+
3
+ module Ribbit
4
+ class Notice
5
+
6
+ # The exception that caused this notice, if any
7
+ attr_reader :exception
8
+
9
+ # The API key for the project to which this notice should be sent
10
+ attr_reader :api_key
11
+
12
+ # The backtrace from the given exception or hash.
13
+ attr_reader :backtrace
14
+
15
+ # The name of the class of error (such as RuntimeError)
16
+ attr_reader :error_class
17
+
18
+ # The name of the server environment (such as "production")
19
+ attr_reader :environment_name
20
+
21
+ # CGI variables such as HTTP_METHOD
22
+ attr_reader :cgi_data
23
+
24
+ # The message from the exception, or a general description of the error
25
+ attr_reader :error_message
26
+
27
+ # See Configuration#backtrace_filters
28
+ attr_reader :backtrace_filters
29
+
30
+ # See Configuration#params_filters
31
+ attr_reader :params_filters
32
+
33
+ # A hash of parameters from the query string or post body.
34
+ attr_reader :parameters
35
+ alias_method :params, :parameters
36
+
37
+ # The component (if any) which was used in this request (usually the controller)
38
+ attr_reader :component
39
+ alias_method :controller, :component
40
+
41
+ # The action (if any) that was called in this request
42
+ attr_reader :action
43
+
44
+ # A hash of session data from the request
45
+ attr_reader :session_data
46
+
47
+ # The path to the project that caused the error (usually RAILS_ROOT)
48
+ attr_reader :project_root
49
+
50
+ # The URL at which the error occurred (if any)
51
+ attr_reader :url
52
+
53
+ # See Configuration#ignore
54
+ attr_reader :ignore
55
+
56
+ # See Configuration#ignore_by_filters
57
+ attr_reader :ignore_by_filters
58
+
59
+ # The name of the notifier library sending this notice, such as "Hoptoad Notifier"
60
+ attr_reader :notifier_name
61
+
62
+ # The version number of the notifier library sending this notice, such as "2.1.3"
63
+ attr_reader :notifier_version
64
+
65
+ # A URL for more information about the notifier library sending this notice
66
+ attr_reader :notifier_url
67
+
68
+ def initialize(args)
69
+ self.args = args
70
+ self.exception = args[:exception]
71
+ self.api_key = args[:api_key]
72
+ self.project_root = args[:project_root]
73
+ self.url = args[:url]
74
+
75
+ self.notifier_name = args[:notifier_name]
76
+ self.notifier_version = args[:notifier_version]
77
+ self.notifier_url = args[:notifier_url]
78
+
79
+ self.ignore = args[:ignore] || []
80
+ self.ignore_by_filters = args[:ignore_by_filters] || []
81
+ self.backtrace_filters = args[:backtrace_filters] || []
82
+ self.params_filters = args[:params_filters] || []
83
+ self.parameters = args[:parameters] || {}
84
+ self.component = args[:component] || args[:controller]
85
+ self.action = args[:action]
86
+
87
+ self.environment_name = args[:environment_name]
88
+ self.cgi_data = args[:cgi_data] || ENV
89
+ self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller))
90
+ self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
91
+ self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
92
+ "#{exception.class.name}: #{exception.message}"
93
+ end
94
+
95
+ find_session_data
96
+ clean_params
97
+ end
98
+
99
+ # Converts the given notice to XML
100
+ def to_xml
101
+ builder = Builder::XmlMarkup.new
102
+ builder.instruct!
103
+ xml = builder.notice(:version => Ribbit::API_VERSION) do |notice|
104
+ notice.tag!("api-key", api_key)
105
+ notice.notifier do |notifier|
106
+ notifier.name(notifier_name)
107
+ notifier.version(notifier_version)
108
+ notifier.url(notifier_url)
109
+ end
110
+ notice.error do |error|
111
+ error.tag!('class', error_class)
112
+ error.message(error_message)
113
+ error.backtrace do |backtrace|
114
+ self.backtrace.lines.each do |line|
115
+ backtrace.line(:number => line.number,
116
+ :file => line.file,
117
+ :method => line.method)
118
+ end
119
+ end
120
+ end
121
+ if url ||
122
+ controller ||
123
+ action ||
124
+ !parameters.empty? ||
125
+ !cgi_data.empty? ||
126
+ !session_data.empty?
127
+ notice.request do |request|
128
+ request.url(url)
129
+ request.component(controller)
130
+ request.action(action)
131
+ unless parameters.empty?
132
+ request.params do |params|
133
+ xml_vars_for(params, parameters)
134
+ end
135
+ end
136
+ unless session_data.empty?
137
+ request.session do |session|
138
+ xml_vars_for(session, session_data)
139
+ end
140
+ end
141
+ unless cgi_data.empty?
142
+ request.tag!("cgi-data") do |cgi_datum|
143
+ xml_vars_for(cgi_datum, cgi_data)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ notice.tag!("server-environment") do |env|
149
+ env.tag!("project-root", project_root)
150
+ env.tag!("environment-name", environment_name)
151
+ end
152
+ end
153
+ xml.to_s
154
+ end
155
+
156
+ # Determines if this notice should be ignored
157
+ def ignore?
158
+ ignored_class_names.include?(error_class) ||
159
+ ignore_by_filters.any? {|filter| filter.call(self) }
160
+ end
161
+
162
+ # Allows properties to be accessed using a hash-like syntax
163
+ #
164
+ # @example
165
+ # notice[:error_message]
166
+ # @param [String] method The given key for an attribute
167
+ # @return The attribute value, or self if given +:request+
168
+ def [](method)
169
+ case method
170
+ when :request
171
+ self
172
+ else
173
+ send(method)
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
180
+ :backtrace_filters, :parameters, :params_filters,
181
+ :environment_filters, :session_data, :project_root, :url, :ignore,
182
+ :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
183
+ :component, :action, :cgi_data, :environment_name
184
+
185
+ # Arguments given in the initializer
186
+ attr_accessor :args
187
+
188
+ # Gets a property named +attribute+ of an exception, either from an actual
189
+ # exception or a hash.
190
+ #
191
+ # If an exception is available, #from_exception will be used. Otherwise,
192
+ # a key named +attribute+ will be used from the #args.
193
+ #
194
+ # If no exception or hash key is available, +default+ will be used.
195
+ def exception_attribute(attribute, default = nil, &block)
196
+ (exception && from_exception(attribute, &block)) || args[attribute] || default
197
+ end
198
+
199
+ # Gets a property named +attribute+ from an exception.
200
+ #
201
+ # If a block is given, it will be used when getting the property from an
202
+ # exception. The block should accept and exception and return the value for
203
+ # the property.
204
+ #
205
+ # If no block is given, a method with the same name as +attribute+ will be
206
+ # invoked for the value.
207
+ def from_exception(attribute)
208
+ if block_given?
209
+ yield(exception)
210
+ else
211
+ exception.send(attribute)
212
+ end
213
+ end
214
+
215
+ # Removes non-serializable data from the given attribute.
216
+ # See #clean_unserializable_data
217
+ def clean_unserializable_data_from(attribute)
218
+ self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
219
+ end
220
+
221
+ # Removes non-serializable data. Allowed data types are strings, arrays,
222
+ # and hashes. All other types are converted to strings.
223
+ # TODO: move this onto Hash
224
+ def clean_unserializable_data(data)
225
+ if data.respond_to?(:to_hash)
226
+ data.inject({}) do |result, (key, value)|
227
+ result.merge(key => clean_unserializable_data(value))
228
+ end
229
+ elsif data.respond_to?(:to_ary)
230
+ data.collect do |value|
231
+ clean_unserializable_data(value)
232
+ end
233
+ else
234
+ data.to_s
235
+ end
236
+ end
237
+
238
+ # Replaces the contents of params that match params_filters.
239
+ # TODO: extract this to a different class
240
+ def clean_params(from=nil)
241
+ clean_unserializable_data_from(:parameters)
242
+ from ||= parameters
243
+ if params_filters
244
+ from.keys.each do |key|
245
+ from[key] = "[FILTERED]" if params_filters.any? do |filter|
246
+ key.to_s.include?(filter)
247
+ end
248
+ # Recurse inside nested param hashes
249
+ clean_params(from[key]) if from[key].respond_to?(:keys)
250
+ end
251
+ end
252
+ end
253
+
254
+ def find_session_data
255
+ self.session_data = args[:session_data] || args[:session] || {}
256
+ self.session_data = session_data[:data] if session_data[:data]
257
+ end
258
+
259
+ # Converts the mixed class instances and class names into just names
260
+ # TODO: move this into Configuration or another class
261
+ def ignored_class_names
262
+ ignore.collect do |string_or_class|
263
+ if string_or_class.respond_to?(:name)
264
+ string_or_class.name
265
+ else
266
+ string_or_class
267
+ end
268
+ end
269
+ end
270
+
271
+ def xml_vars_for(builder, hash)
272
+ hash.each do |key, value|
273
+ if value.respond_to?(:to_hash)
274
+ builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) }
275
+ elsif value.respond_to?(:to_ary)
276
+ builder.var("[" << (value.map { |v| v.to_s } * ", ") << "]", :key => key)
277
+ else
278
+ builder.var(value.to_s, :key => key)
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,70 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ module Ribbit
5
+ # Sends out the notice to Hoptoad
6
+ class Sender
7
+
8
+ NOTICES_URI = '/notifier_api/v2/notices/'.freeze
9
+ HEADERS = {
10
+ 'Content-type' => 'text/xml',
11
+ 'Accept' => 'text/xml, application/xml'
12
+ }
13
+
14
+ def initialize(options = {})
15
+ [:proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol,
16
+ :host, :port, :secure, :http_open_timeout, :http_read_timeout].each do |option|
17
+ instance_variable_set("@#{option}", options[option])
18
+ end
19
+ end
20
+
21
+ # Sends the notice data off to Hoptoad for processing.
22
+ #
23
+ # @param [String] data The XML notice to be sent off
24
+ def send_to_hoptoad(data)
25
+ log :debug, "Sending request to #{url.to_s}:\n#{data}"
26
+
27
+ http =
28
+ Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass).
29
+ new(url.host, url.port)
30
+
31
+ http.read_timeout = http_read_timeout
32
+ http.open_timeout = http_open_timeout
33
+ http.use_ssl = secure
34
+
35
+ response = begin
36
+ http.post(url.path, data, HEADERS)
37
+ rescue TimeoutError => e
38
+ log :error, "Timeout while contacting the Hoptoad server."
39
+ nil
40
+ end
41
+
42
+ case response
43
+ when Net::HTTPSuccess then
44
+ log :info, "Success: #{response.class}"
45
+ log :debug, "Response Body: #{response.body}"
46
+ else
47
+ log :error, "Failure: #{response.class}"
48
+ log :debug, "Response Body: #{response.body}"
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol,
55
+ :host, :port, :secure, :http_open_timeout, :http_read_timeout
56
+
57
+ def url
58
+ URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URI)
59
+ end
60
+
61
+ def log(level, message)
62
+ logger.send level, LOG_PREFIX + message if logger
63
+ end
64
+
65
+ def logger
66
+ Ribbit.logger
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Ribbit do
4
+
5
+ it "should have a name for the client field in the API" do
6
+ Ribbit.client_name.should_not be_nil
7
+ end
8
+
9
+ it "should have a version number for the client" do
10
+ Ribbit.client_version.should_not be_nil
11
+ end
12
+
13
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'ribbit'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hoptoad-ribbit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Glen Mailer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-05 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: builder
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.9
34
+ version:
35
+ description: "\n Provides a Module to handle sending notifications to hoptoad.\n\n Currently includes a Merb adapter, but the intention is to add more later.\n "
36
+ email: glenjamin@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - hoptoad-ribbit.gemspec
52
+ - lib/ribbit.rb
53
+ - lib/ribbit/adapters.rb
54
+ - lib/ribbit/adapters/adapter.rb
55
+ - lib/ribbit/adapters/merb.rb
56
+ - lib/ribbit/adapters/none.rb
57
+ - lib/ribbit/backtrace.rb
58
+ - lib/ribbit/configuration.rb
59
+ - lib/ribbit/notice.rb
60
+ - lib/ribbit/sender.rb
61
+ - spec/ribbit_spec.rb
62
+ - spec/spec.opts
63
+ - spec/spec_helper.rb
64
+ has_rdoc: true
65
+ homepage: http://github.com/glenjamin/hoptoad-notifier
66
+ licenses: []
67
+
68
+ post_install_message:
69
+ rdoc_options:
70
+ - --charset=UTF-8
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ requirements: []
86
+
87
+ rubyforge_project:
88
+ rubygems_version: 1.3.5
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Generic Hoptoad Notifications and an adapter for merb
92
+ test_files:
93
+ - spec/ribbit_spec.rb
94
+ - spec/spec_helper.rb