exceptional 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/exceptional ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ args = ARGV.dup
4
+ ARGV.clear
5
+ command = args.shift.strip rescue 'help'
6
+
7
+ case command
8
+ when 'help'
9
+ puts <<USAGE
10
+ help # show this usage
11
+ test # send a test exception to exceptional
12
+ install <api_key> # create config/exceptional.yml with your api_key. Overrites existing one.
13
+ USAGE
14
+ when 'test'
15
+ puts "Loading Rails environment."
16
+ require(File.join('config', 'boot'))
17
+ require(File.join(RAILS_ROOT, 'config', 'environment'))
18
+ require "exceptional/integration/tester"
19
+ unless Exceptional::Config.api_key.blank?
20
+ Exceptional::Integration.test
21
+ end
22
+ when 'install'
23
+ api_key = args[0]
24
+ if (api_key.nil?)
25
+ puts 'missing required paramater <api-key>. Check your app configuration at http://getexceptional.com'
26
+ else
27
+ unless File.file?('config/environment.rb')
28
+ puts "Run this command from the root of your rails application"
29
+ else
30
+ config_file = File.open('config/exceptional.yml', 'w')
31
+ config_file.puts("api-key: #{api_key}\n")
32
+ config_file.close
33
+ puts "Config file written as config/exceptional.yml"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |s|
3
+ s.name = %q{exceptional}
4
+ s.version = "2.0.0"
5
+ s.authors = ["Contrast"]
6
+ s.summary = %q{Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)}
7
+ s.description = %q{Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service). Use it to find out about errors that happen in your live app. It captures lots of helpful information to help you fix the errors.}
8
+ s.email = %q{hello@contrast.ie}
9
+ s.files = Dir['lib/**/*'] + Dir['spec/**/*'] + Dir['spec/**/*'] + Dir['rails/**/*'] + Dir['tasks/**/*'] + Dir['*.rb'] + ["exceptional.gemspec"]
10
+ s.homepage = %q{http://getexceptional.com/}
11
+ s.require_paths = ["lib"]
12
+ s.executables << 'exceptional'
13
+ s.rubyforge_project = %q{exceptional}
14
+ s.add_dependency('json', ">= 1.0.0")
15
+ end
data/init.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'exceptional'
2
+
3
+ # If old plugin still installed then we don't want to install this one.
4
+ # In production environments we should continue to work as before, but in development/test we should
5
+ # advise how to correct the problem and exit
6
+ if defined?(Exceptional::VERSION::STRING) && %w(development test).include?(RAILS_ENV)
7
+ message = %Q(
8
+ ***********************************************************************
9
+ You seem to still have an old version of the exceptional plugin installed.
10
+ Remove it from /vendor/plugins and try again.
11
+ ***********************************************************************
12
+ )
13
+ puts message
14
+ exit -1
15
+ else
16
+ begin
17
+ Exceptional::Config.load(File.join(RAILS_ROOT, "/config/exceptional.yml"))
18
+ Exceptional::Startup.announce
19
+ require File.join('exceptional', 'integration', 'rails')
20
+ rescue => e
21
+ STDERR.puts "Problem starting Exceptional Plugin. Your app will run as normal."
22
+ STDERR.puts e
23
+ end
24
+ end
data/install.rb ADDED
@@ -0,0 +1,19 @@
1
+ # This is the post install hook for when Exceptional is installed as a plugin.
2
+ require 'ftools'
3
+
4
+ # puts IO.read(File.join(File.dirname(__FILE__), 'README'))
5
+
6
+ config_file = File.expand_path("#{File.dirname(__FILE__)}/../../../config/exceptional.yml")
7
+ example_config_file = "#{File.dirname(__FILE__)}/exceptional.yml"
8
+
9
+ if File::exists? config_file
10
+ puts "Exceptional config file already exists. Please ensure it is up-to-date with the current format."
11
+ puts "See #{example_config_file}"
12
+ else
13
+ puts "Installing default Exceptional config"
14
+ puts " From #{example_config_file}"
15
+ puts "For exceptional to work you need to configure your API Key"
16
+ puts " See #{example_config_file}"
17
+ puts "If you don't have an API Key, get one at http://getexceptional.com/"
18
+ File.copy example_config_file, config_file
19
+ end
@@ -0,0 +1,33 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'exceptional/catcher'
4
+ require 'exceptional/startup'
5
+ require 'exceptional/log_factory'
6
+ require 'exceptional/config'
7
+ require 'exceptional/application_environment'
8
+ require 'exceptional/exception_data'
9
+ require 'exceptional/remote'
10
+ require 'exceptional/integration/rack'
11
+
12
+ module Exceptional
13
+ PROTOCOL_VERSION = 5
14
+ VERSION = '0.2.0'
15
+ CLIENT_NAME = 'getexceptional-rails-plugin'
16
+
17
+ def self.logger
18
+ ::Exceptional::LogFactory.logger
19
+ end
20
+
21
+ def self.configure(api_key)
22
+ Exceptional::Config.api_key = api_key
23
+ end
24
+
25
+ def self.rescue(&block)
26
+ begin
27
+ block.call
28
+ rescue Exception => e
29
+ Exceptional::Catcher.handle(e)
30
+ raise(e)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,62 @@
1
+ require 'digest/md5'
2
+
3
+ module Exceptional
4
+ class ApplicationEnvironment
5
+ def self.to_hash
6
+ hash = {
7
+ 'client' => {
8
+ 'name' => Exceptional::CLIENT_NAME,
9
+ 'version' => Exceptional::VERSION,
10
+ 'protocol_version' => Exceptional::PROTOCOL_VERSION
11
+ },
12
+ 'application_environment' => {
13
+ 'environment' => environment,
14
+ 'env' => extract_environment(ENV),
15
+ 'host' => get_hostname,
16
+ 'run_as_user' => get_username,
17
+ 'application_root_directory' => application_root,
18
+ 'language' => 'ruby',
19
+ 'language_version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} #{RUBY_RELEASE_DATE} #{RUBY_PLATFORM}",
20
+ 'framework' => framework,
21
+ 'libraries_loaded' => libraries_loaded
22
+ }
23
+ }
24
+ hash
25
+ end
26
+
27
+ def self.framework
28
+ defined?(RAILS_ENV) ? "rails" : nil
29
+ end
30
+
31
+ def self.environment
32
+ Config.application_environment
33
+ end
34
+
35
+ def self.application_root
36
+ Config.application_root
37
+ end
38
+
39
+ def self.extract_environment(env)
40
+ env.reject{|k, v| k =~ /^HTTP_/}
41
+ end
42
+
43
+ def self.get_hostname
44
+ require 'socket' unless defined?(Socket)
45
+ Socket.gethostname
46
+ rescue
47
+ 'UNKNOWN'
48
+ end
49
+
50
+ def self.get_username
51
+ ENV['LOGNAME'] || ENV['USER'] || ENV['USERNAME'] || ENV['APACHE_RUN_USER'] || 'UNKNOWN'
52
+ end
53
+
54
+ def self.libraries_loaded
55
+ begin
56
+ return Hash[*Gem.loaded_specs.map{|name, gem_specification| [name, gem_specification.version.to_s]}.flatten]
57
+ rescue
58
+ end
59
+ {}
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ module Exceptional
2
+ class Catcher
3
+ class << self
4
+ def handle(exception, controller=nil, request=nil)
5
+ if Config.should_send_to_api?
6
+ data = ExceptionData.new(exception, controller, request)
7
+ Remote.error(data)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,85 @@
1
+ module Exceptional
2
+ class Config
3
+ class << self
4
+ DEFAULTS = {
5
+ :ssl_enabled => false,
6
+ :remote_host_http => 'api.getexceptional.com',
7
+ :remote_port_http => 80,
8
+ :remote_host_https => 'getexceptional.appspot.com',
9
+ :remote_port_https => 443,
10
+ :http_open_timeout => 2,
11
+ :http_read_timeout => 4,
12
+ :disabled_by_default => %w(development test)
13
+ }
14
+
15
+ attr_accessor :api_key
16
+ attr_accessor :http_proxy_host, :http_proxy_port, :http_proxy_username, :http_proxy_password
17
+ attr_writer :ssl_enabled
18
+
19
+ def load(config_file=nil)
20
+ if (config_file && File.file?(config_file))
21
+ begin
22
+ config = YAML::load(File.open(config_file))
23
+ env_config = config[application_environment] || {}
24
+ @api_key = config['api-key'] || env_config['api-key']
25
+
26
+ @http_proxy_host = config['http-proxy-host']
27
+ @http_proxy_port = config['http-proxy-port']
28
+ @http_proxy_username = config['http-proxy-username']
29
+ @http_proxy_password = config['http-proxy-password']
30
+ @http_open_timeout = config['http-open-timeout']
31
+ @http_read_timeout = config['http-read-timeout']
32
+
33
+ @ssl_enabled = config['ssl'] || env_config['ssl']
34
+ @enabled = env_config['enabled']
35
+ @remote_port = config['remote-port'].to_i unless config['remote-port'].nil?
36
+ @remote_host = config['remote-host'] unless config['remote-host'].nil?
37
+ rescue Exception => e
38
+ raise ConfigurationException.new("Unable to load configuration #{config_file} for environment #{application_environment} : #{e.message}")
39
+ end
40
+ end
41
+ end
42
+
43
+ def api_key
44
+ return @api_key unless @api_key.nil?
45
+ @api_key ||= ENV['EXCEPTIONAL_API_KEY'] unless ENV['EXCEPTIONAL_API_KEY'].nil?
46
+ end
47
+
48
+ def application_environment
49
+ ENV['RACK_ENV'] || ENV['RAILS_ENV']|| 'development'
50
+ end
51
+
52
+ def should_send_to_api?
53
+ @enabled ||= DEFAULTS[:disabled_by_default].include?(application_environment) ? false : true
54
+ end
55
+
56
+ def application_root
57
+ defined?(RAILS_ROOT) ? RAILS_ROOT : Dir.pwd
58
+ end
59
+
60
+ def ssl_enabled?
61
+ @ssl_enabled ||= DEFAULTS[:ssl_enabled]
62
+ end
63
+
64
+ def remote_host
65
+ @remote_host ||= ssl_enabled? ? DEFAULTS[:remote_host_https] : DEFAULTS[:remote_host_http]
66
+ end
67
+
68
+ def remote_port
69
+ @remote_port ||= ssl_enabled? ? DEFAULTS[:remote_port_https] : DEFAULTS[:remote_port_http]
70
+ end
71
+
72
+ def reset
73
+ @enabled = @ssl_enabled = @remote_host = @remote_port = @api_key = nil
74
+ end
75
+
76
+ def http_open_timeout
77
+ @http_open_timeout ||= DEFAULTS[:http_open_timeout]
78
+ end
79
+
80
+ def http_read_timeout
81
+ @http_read_timeout ||= DEFAULTS[:http_read_timeout]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,95 @@
1
+ require 'digest/md5'
2
+
3
+ module Exceptional
4
+ class ExceptionData
5
+ def initialize(exception, controller=nil, request=nil)
6
+ @exception = exception
7
+ @request = request
8
+ @controller = controller
9
+ end
10
+
11
+ def to_hash
12
+ hash = ::Exceptional::ApplicationEnvironment.to_hash
13
+ hash.merge!({
14
+ 'exception' => {
15
+ 'exception_class' => @exception.class.to_s,
16
+ 'message' => @exception.message,
17
+ 'backtrace' => @exception.backtrace,
18
+ 'occurred_at' => Time.now.strftime("%Y%m%d %H:%M:%S %Z")
19
+ }
20
+ })
21
+ unless @request.nil?
22
+ hash.merge!({
23
+ 'request' => {
24
+ 'url' => "#{@request.protocol}#{@request.host}#{@request.request_uri}",
25
+ 'controller' => @controller.class.to_s,
26
+ 'action' => @request.parameters['action'],
27
+ 'parameters' => filter_paramaters(@request.parameters),
28
+ 'request_method' => @request.request_method.to_s,
29
+ 'remote_ip' => @request.remote_ip,
30
+ 'headers' => extract_http_headers(@request.env),
31
+ 'session' => Exceptional::ExceptionData.sanitize_session(@request)
32
+ }
33
+ })
34
+ end
35
+ hash
36
+ end
37
+
38
+ def to_json
39
+ to_hash.to_json
40
+ end
41
+
42
+ def uniqueness_hash
43
+ return nil if @exception.backtrace.blank?
44
+ Digest::MD5.hexdigest(@exception.backtrace.join)
45
+ end
46
+
47
+ def filter_paramaters(hash)
48
+ if @controller.respond_to?(:filter_parameters)
49
+ @controller.send(:filter_parameters, hash)
50
+ else
51
+ hash
52
+ end
53
+ end
54
+
55
+ def extract_http_headers(env)
56
+ headers = {}
57
+ env.select{|k, v| k =~ /^HTTP_/}.each do |name, value|
58
+ proper_name = name.sub(/^HTTP_/, '').split('_').map{|upper_case| upper_case.capitalize}.join('-')
59
+ headers[proper_name] = value
60
+ end
61
+ unless headers['Cookie'].nil?
62
+ headers['Cookie'] = headers['Cookie'].sub(/_session=\S+/, '_session=[FILTERED]')
63
+ end
64
+ headers
65
+ end
66
+
67
+ def self.sanitize_hash(hash)
68
+ case hash
69
+ when Hash
70
+ hash.inject({}) do |result, (key, value)|
71
+ result.update(key => sanitize_hash(value))
72
+ end
73
+ when Fixnum, Array, String, Bignum
74
+ hash
75
+ else
76
+ hash.to_s
77
+ end
78
+ rescue
79
+ {}
80
+ end
81
+
82
+ def self.sanitize_session(request)
83
+ session = request.session
84
+ session_hash = {}
85
+ session_hash['session_id'] = request.session_options ? request.session_options[:id] : nil
86
+ session_hash['session_id'] ||= session.respond_to?(:session_id) ? session.session_id : session.instance_variable_get("@session_id")
87
+ session_hash['data'] = session.respond_to?(:to_hash) ? session.to_hash : session.instance_variable_get("@data") || {}
88
+ session_hash['session_id'] ||= session_hash['data'][:session_id]
89
+ session_hash['data'].delete(:session_id)
90
+ ExceptionData.sanitize_hash(session_hash)
91
+ rescue
92
+ {}
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,14 @@
1
+ module Exceptional
2
+ class Rack
3
+ def initialize(app, api_key)
4
+ @app = app
5
+ @api_key= api_key
6
+ end
7
+
8
+ def call(env)
9
+ @app.call(env)
10
+ rescue Exception => e
11
+ raise e
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # force Rails < 2.0 to use quote keys as per the JSON standard...
2
+ if defined?(ActiveSupport) && defined?(ActiveSupport::JSON) && ActiveSupport::JSON.respond_to?(:unquote_hash_key_identifiers)
3
+ ActiveSupport::JSON.unquote_hash_key_identifiers = false
4
+ end
5
+
6
+ if defined? ActionController
7
+ module ActionController
8
+ class Base
9
+ def rescue_action_with_exceptional(exception)
10
+ unless exception_handled_by_rescue_from?(exception)
11
+ Exceptional::Catcher.handle(exception, self, request)
12
+ end
13
+ rescue_action_without_exceptional exception
14
+ end
15
+
16
+ alias_method :rescue_action_without_exceptional, :rescue_action
17
+ alias_method :rescue_action, :rescue_action_with_exceptional
18
+ protected :rescue_action
19
+
20
+ private
21
+ def exception_handled_by_rescue_from?(exception)
22
+ respond_to?(:handler_for_rescue) && handler_for_rescue(exception)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Exceptional
2
+ module Integration
3
+ class ExceptionalTestException <StandardError;
4
+ end
5
+
6
+ def self.test
7
+ data = Exceptional::ExceptionData.new(ExceptionalTestException.new)
8
+ unless Exceptional::Remote.error(data)
9
+ puts "Problem sending error to Exceptional. Check your api key"
10
+ else
11
+ puts "Exception sent successfully"
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+
@@ -0,0 +1,39 @@
1
+ require 'logger'
2
+
3
+ module Exceptional
4
+ class LogFactory
5
+ def self.logger
6
+ @logger ||= create_logger_with_fallback
7
+ end
8
+
9
+ private
10
+ def self.create_logger_with_fallback
11
+ begin
12
+ log_dir = File.join(Config.application_root, 'log')
13
+ Dir.mkdir(log_dir) unless File.directory?(log_dir)
14
+ log_path = File.join(log_dir, "/exceptional.log")
15
+ log = Logger.new(log_path)
16
+ log.level = Logger::INFO
17
+ def log.format_message(severity, timestamp, progname, msg)
18
+ "[#{severity.upcase}] (#{[Kernel.caller[2].split('/').last]}) #{timestamp.utc.to_s} - #{msg2str(msg).gsub(/\n/, '').lstrip}\n"
19
+ end
20
+ def log.msg2str(msg)
21
+ case msg
22
+ when ::String
23
+ msg
24
+ when ::Exception
25
+ "#{ msg.message } (#{ msg.class }): " <<
26
+ (msg.backtrace || []).join(" | ")
27
+ else
28
+ msg.inspect
29
+ end
30
+ end
31
+ log
32
+ rescue
33
+ return Rails.logger if defined?(Rails) && defined?(Rails.logger)
34
+ return RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)
35
+ return Logger.new(STDERR)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ require 'zlib'
2
+ require 'cgi'
3
+ require 'net/http'
4
+ require 'digest/md5'
5
+
6
+ module Exceptional
7
+ class Remote
8
+ class << self
9
+ def startup_announce(startup_data)
10
+ url = "/api/announcements?api_key=#{::Exceptional::Config.api_key}&protocol_version=#{::Exceptional::PROTOCOL_VERSION}"
11
+ compressed = Zlib::Deflate.deflate(startup_data.to_json, Zlib::BEST_SPEED)
12
+ call_remote(url, compressed)
13
+ end
14
+
15
+ def error(exception_data)
16
+ Exceptional.logger.info "Notifying Exceptional about an error"
17
+ uniqueness_hash = exception_data.uniqueness_hash
18
+ hash_param = uniqueness_hash.nil? ? nil: "&hash=#{uniqueness_hash}"
19
+ url = "/api/errors?api_key=#{::Exceptional::Config.api_key}&protocol_version=#{::Exceptional::PROTOCOL_VERSION}#{hash_param}"
20
+ compressed = Zlib::Deflate.deflate(exception_data.to_json, Zlib::BEST_SPEED)
21
+ call_remote(url, compressed)
22
+ end
23
+
24
+ def call_remote(url, data)
25
+ config = Exceptional::Config
26
+ optional_proxy = Net::HTTP::Proxy(config.http_proxy_host,
27
+ config.http_proxy_port,
28
+ config.http_proxy_username,
29
+ config.http_proxy_password)
30
+ client = optional_proxy.new(config.remote_host, config.remote_port)
31
+ client.open_timeout = config.http_open_timeout
32
+ client.read_timeout = config.http_read_timeout
33
+ client.use_ssl = config.ssl_enabled?
34
+ begin
35
+ response = client.post(url, data)
36
+ case response
37
+ when Net::HTTPSuccess
38
+ Exceptional.logger.info('Successful')
39
+ return true
40
+ else
41
+ Exceptional.logger.error('Failed')
42
+ end
43
+ rescue Exception => e
44
+ Exceptional.logger.error('Problem notifying Exceptional about the error')
45
+ Exceptional.logger.error(e)
46
+ end
47
+ nil
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ module Exceptional
2
+ class StartupException < StandardError;
3
+ end
4
+ class Startup
5
+ class << self
6
+ def announce
7
+ if Config.api_key.blank?
8
+ raise StartupException, 'API Key must be configured (/config/exceptional.yml)'
9
+ end
10
+ Remote.startup_announce(::Exceptional::ApplicationEnvironment.to_hash)
11
+ end
12
+ end
13
+ end
14
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__) , '../init.rb')
data/spec/bin/ginger ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ begin
5
+ require 'ginger'
6
+ rescue LoadError
7
+ puts <<-INSTALL_GINGER
8
+ Install ginger to test plugin against multiple environments:
9
+
10
+ sudo gem install freelancing-god-ginger --source=http://gems.github.com
11
+
12
+ More details: http://github.com/freelancing-god/ginger
13
+ INSTALL_GINGER
14
+ exit 0
15
+ end
16
+
17
+ require 'rake'
18
+
19
+ if ARGV.length == 0
20
+ puts <<-USAGE
21
+ ginger #{Ginger::Version::String}
22
+ Use ginger to run specs for each scenario defined. Scenarios must be set out in
23
+ a file called ginger_scenarios.rb wherever this tool is run. Once they're
24
+ defined, then you can run this tool and provide the rake task that would
25
+ normally be called.
26
+
27
+ Examples:
28
+ ginger spec
29
+ ginger test
30
+ ginger spec:models
31
+ USAGE
32
+ exit 0
33
+ end
34
+
35
+ file_path = File.join Dir.pwd, ".ginger"
36
+
37
+ File.delete(file_path) if File.exists?(file_path)
38
+
39
+ scenarios = Ginger::Configuration.instance.scenarios
40
+ puts "No Ginger Scenarios defined" if scenarios.empty?
41
+
42
+ scenarios.each_with_index do |scenario, index|
43
+ puts <<-SCENARIO
44
+
45
+ -------------------
46
+ Ginger Scenario: #{scenario.name || index+1}
47
+ -------------------
48
+ SCENARIO
49
+
50
+ File.open('.ginger', 'w') { |f| f.write index.to_s }
51
+ system("rake #{ARGV.join(" ")}") || system("rake.bat #{ARGV.join(" ")}")
52
+ end
53
+
54
+ File.delete(file_path) if File.exists?(file_path)
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Exceptional::Catcher do
4
+ it "should create exception_data object and send json to the api" do
5
+ Exceptional::Config.should_receive(:should_send_to_api?).and_return(true)
6
+ exception = mock('exception')
7
+ controller = mock('controller')
8
+ request = mock('request')
9
+ Exceptional::ExceptionData.should_receive(:new).with(exception,controller,request).and_return(data = mock('exception_data'))
10
+ Exceptional::Remote.should_receive(:error).with(data)
11
+ Exceptional::Catcher.handle(exception,controller,request)
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Exceptional::Config, 'defaults' do
4
+ before :each do
5
+ Exceptional::Config.reset
6
+ end
7
+ it "have sensible defaults" do
8
+ Exceptional::Config.ssl_enabled?.should == false
9
+ Exceptional::Config.remote_host.should == 'api.getexceptional.com'
10
+ Exceptional::Config.remote_port.should == 80
11
+ Exceptional::Config.application_root.should == Dir.pwd
12
+ Exceptional::Config.http_proxy_host.should be_nil
13
+ Exceptional::Config.http_proxy_port.should be_nil
14
+ Exceptional::Config.http_proxy_username.should be_nil
15
+ Exceptional::Config.http_proxy_password.should be_nil
16
+ Exceptional::Config.http_open_timeout.should == 2
17
+ Exceptional::Config.http_read_timeout.should == 4
18
+ end
19
+ it "have correct defaults when ssl_enabled" do
20
+ Exceptional::Config.ssl_enabled = true
21
+ Exceptional::Config.remote_host.should == 'getexceptional.appspot.com'
22
+ Exceptional::Config.remote_port.should == 443
23
+ end
24
+ it "be enabled based on environment by default" do
25
+ %w(development test).each do |env|
26
+ Exceptional::Config.stub!(:application_environment).and_return(env)
27
+ Exceptional::Config.should_send_to_api?.should == false
28
+ end
29
+ %w(production staging).each do |env|
30
+ Exceptional::Config.stub!(:application_environment).and_return(env)
31
+ Exceptional::Config.should_send_to_api?.should == true
32
+ end
33
+ end
34
+ context 'production environment' do
35
+ before :each do
36
+ Exceptional::Config.stub!(:application_environment).and_return('production')
37
+ end
38
+ it "allow a new simpler format for exception.yml" do
39
+ Exceptional::Config.load('spec/fixtures/exceptional.yml')
40
+ Exceptional::Config.api_key.should == 'abc123'
41
+ Exceptional::Config.ssl_enabled?.should == true
42
+ Exceptional::Config.remote_host.should == 'example.com'
43
+ Exceptional::Config.remote_port.should == 123
44
+ Exceptional::Config.should_send_to_api?.should == true
45
+ Exceptional::Config.http_proxy_host.should == 'annoying-proxy.example.com'
46
+ Exceptional::Config.http_proxy_port.should == 1066
47
+ Exceptional::Config.http_proxy_username.should == 'bob'
48
+ Exceptional::Config.http_proxy_password.should == 'jack'
49
+ Exceptional::Config.http_open_timeout.should == 5
50
+ Exceptional::Config.http_read_timeout.should == 10
51
+ end
52
+ it "allow olded format for exception.yml" do
53
+ Exceptional::Config.load('spec/fixtures/exceptional_old.yml')
54
+ Exceptional::Config.api_key.should == 'abc123'
55
+ Exceptional::Config.ssl_enabled?.should == true
56
+ Exceptional::Config.should_send_to_api?.should == true
57
+ end
58
+ it "load api_key from environment variable" do
59
+ ENV.should_receive(:[]).with('EXCEPTIONAL_API_KEY').any_number_of_times.and_return('98765')
60
+ Exceptional::Config.api_key.should == '98765'
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,123 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'digest/md5'
3
+
4
+ class Exceptional::FunkyError < StandardError
5
+ def backtrace
6
+ 'backtrace'
7
+ end
8
+ end
9
+
10
+ describe Exceptional::ExceptionData, 'when no request/controller/params' do
11
+ before :each do
12
+ ENV['LOGNAME'] = 'bob'
13
+ ENV['SOMEVAR'] = 'something'
14
+ ENV['HTTP_SOMETHING'] = 'should be stripped'
15
+ RAILS_ENV = 'test' unless defined?(RAILS_ENV)
16
+ Time.stub!(:now).and_return(Time.mktime(1970,1,1))
17
+ error = Exceptional::FunkyError.new('some message')
18
+ data = Exceptional::ExceptionData.new(error)
19
+ @hash = data.to_hash
20
+ end
21
+
22
+ it "capture exception details" do
23
+ error_hash = @hash['exception']
24
+ error_hash['exception_class'].should == 'Exceptional::FunkyError'
25
+ error_hash['message'].should == 'some message'
26
+ error_hash['backtrace'].should == 'backtrace'
27
+ error_hash['occurred_at'].should == Time.now.strftime("%Y%m%d %H:%M:%S %Z")
28
+ client_hash = @hash['client']
29
+ client_hash['name'].should == Exceptional::CLIENT_NAME
30
+ client_hash['version'].should == Exceptional::VERSION
31
+ client_hash['protocol_version'].should == Exceptional::PROTOCOL_VERSION
32
+ end
33
+
34
+ it "capture application_environment" do
35
+ application_env_hash = @hash['application_environment']
36
+ application_env_hash['environment'].should == 'test'
37
+ application_env_hash['env'].should_not be_nil
38
+ application_env_hash['env']['SOMEVAR'].should == 'something'
39
+ application_env_hash['host'].should == `hostname`.strip
40
+ application_env_hash['run_as_user'].should == 'bob'
41
+ application_env_hash['application_root_directory'].should == Dir.pwd
42
+ application_env_hash['language'].should == 'ruby'
43
+ application_env_hash['language_version'].should == "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} #{RUBY_RELEASE_DATE} #{RUBY_PLATFORM}"
44
+ application_env_hash['framework'].should == "rails"
45
+ application_env_hash['libraries_loaded']['rails'].should =~ /\d\.\d\.\d/
46
+ end
47
+ end
48
+
49
+ describe Exceptional::ExceptionData, 'with request/controller/params' do
50
+ class Exceptional::SomeController < ActionController::Base
51
+ filter_parameter_logging :filter_me
52
+ end
53
+
54
+ before :each do
55
+ @controller = Exceptional::SomeController.new
56
+ @request = ActionController::TestRequest.new({'action' => 'some_action' })
57
+ @request.request_uri = '/some_path?var1=abc'
58
+ @request.stub!(:parameters).and_return({'var1' => 'abc', 'action' => 'some_action', 'filter_me' => 'private'})
59
+ @request.stub!(:request_method).and_return(:get)
60
+ @request.stub!(:remote_ip).and_return('1.2.3.4')
61
+ @request.stub!(:env).and_return({'SOME_VAR' => 'abc', 'HTTP_CONTENT_TYPE' => 'text/html'})
62
+ error = Exceptional::FunkyError.new('some message')
63
+ data = Exceptional::ExceptionData.new(error, @controller, @request)
64
+ @hash = data.to_hash
65
+ end
66
+
67
+ it "captures request" do
68
+ request_hash = @hash['request']
69
+ request_hash['url'].should == 'http://test.host/some_path?var1=abc'
70
+ request_hash['controller'].should == 'Exceptional::SomeController'
71
+ request_hash['action'].should == 'some_action'
72
+ request_hash['parameters'].should == {'var1' => 'abc', 'action' => 'some_action', 'filter_me' => '[FILTERED]'}
73
+ request_hash['request_method'].should == 'get'
74
+ request_hash['remote_ip'].should == '1.2.3.4'
75
+ request_hash['headers'].should == {'Content-Type' => 'text/html'}
76
+ end
77
+
78
+ it "filter out objects that aren't jsonable" do
79
+ class Crazy
80
+ def initialize
81
+ @bar = self
82
+ end
83
+ end
84
+ crazy = Crazy.new
85
+ input = {'crazy' => crazy, :simple => '123', :some_hash => {'1' => '2'}, :array => ['1','2']}
86
+ Exceptional::ExceptionData.sanitize_hash(input).should == {'crazy' => crazy.to_s, :simple => '123', :some_hash => {'1' => '2'}, :array => ['1','2']}
87
+ end
88
+
89
+ it "handles session objects with various interfaces" do
90
+ class SessionWithInstanceVariables
91
+ def initialize
92
+ @data = {'a' => '1'}
93
+ @session_id = '123'
94
+ end
95
+ end
96
+ request = ActionController::TestRequest.new
97
+ session = SessionWithInstanceVariables.new
98
+ request.stub!(:session).and_return(session)
99
+ request.stub!(:session_options).and_return({})
100
+ Exceptional::ExceptionData.sanitize_session(request).should == {'session_id' => '123', 'data' => {'a' => '1'}}
101
+ session = mock('session', :session_id => '123', :instance_variable_get => {'a' => '1'})
102
+ request.stub!(:session).and_return(session)
103
+ Exceptional::ExceptionData.sanitize_session(request).should == {'session_id' => '123', 'data' => {'a' => '1'}}
104
+ session = mock('session', :session_id => nil, :to_hash => {:session_id => '123', 'a' => '1'})
105
+ request.stub!(:session).and_return(session)
106
+ Exceptional::ExceptionData.sanitize_session(request).should == {'session_id' => '123', 'data' => {'a' => '1'}}
107
+ request.stub!(:session_options).and_return({:id => 'xyz'})
108
+ Exceptional::ExceptionData.sanitize_session(request).should == {'session_id' => 'xyz', 'data' => {'a' => '1'}}
109
+ end
110
+
111
+ it "filter session cookies from headers" do
112
+ @request.stub!(:env).and_return({'SOME_VAR' => 'abc', 'HTTP_COOKIE' => '_something_else=faafsafafasfa; _myapp-lick-nation_session=BAh7DDoMbnVtYmVyc1sJaQZpB2kIaQk6FnNvbWVfY3Jhenlfb2JqZWN0bzobU3Bpa2VDb250cm9sbGVyOjpDcmF6eQY6CUBiYXJABzoTc29tZXRoaW5nX2Vsc2UiCGNjYzoKYXBwbGUiDUJyYWVidXJuOgloYXNoewdpBmkHaQhpCToPc2Vzc2lvbl9pZCIlMmJjZTM4MjVjMThkNzYxOWEyZDA4NTJhNWY1NGQzMmU6C3RvbWF0byIJQmVlZg%3D%3D--66fb4606851f06bf409b8bc4ba7aea47a0259bf7'})
113
+ @hash = Exceptional::ExceptionData.new(Exceptional::FunkyError.new('some message'), @controller, @request).to_hash
114
+ @hash['request']['headers'].should == {'Cookie' => '_something_else=faafsafafasfa; _myapp-lick-nation_session=[FILTERED]'}
115
+ end
116
+
117
+ it "creates a uniqueness_hash from backtrace" do
118
+ myException = Exception.new
119
+ myException.stub!(:backtrace).and_return(['123'])
120
+ data = Exceptional::ExceptionData.new(myException)
121
+ data.uniqueness_hash.should == Digest::MD5.hexdigest('123')
122
+ end
123
+ end
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'zlib'
3
+ require 'digest/md5'
4
+
5
+ describe Exceptional::Remote do
6
+ before :each do
7
+ Exceptional::Config.reset
8
+ Exceptional::Config.api_key = 'abc123'
9
+ end
10
+
11
+ it "calls remote with api_key, protocol_version and json" do
12
+ expected_url = "/api/errors?api_key=abc123&protocol_version=#{Exceptional::PROTOCOL_VERSION}"
13
+ expected_data = mock('data',:uniqueness_hash => nil, :to_json => '123')
14
+ Exceptional::Remote.should_receive(:call_remote).with(expected_url, Zlib::Deflate.deflate(expected_data.to_json,Zlib::BEST_SPEED))
15
+ Exceptional::Remote.error(expected_data)
16
+ end
17
+
18
+ it "adds hash of backtrace as paramater if it is present" do
19
+ expected_url = "/api/errors?api_key=abc123&protocol_version=#{Exceptional::PROTOCOL_VERSION}&hash=blah"
20
+ expected_data = mock('data',:uniqueness_hash => 'blah', :to_json => '123')
21
+ Exceptional::Remote.should_receive(:call_remote).with(expected_url, Zlib::Deflate.deflate(expected_data.to_json,Zlib::BEST_SPEED))
22
+ Exceptional::Remote.error(expected_data)
23
+ end
24
+
25
+ it "calls remote for startup" do
26
+ expected_url = "/api/announcements?api_key=abc123&protocol_version=#{Exceptional::PROTOCOL_VERSION}"
27
+ startup_data = mock('data',:to_json => '123')
28
+ Exceptional::Remote.should_receive(:call_remote).with(expected_url, Zlib::Deflate.deflate(startup_data.to_json,Zlib::BEST_SPEED))
29
+ Exceptional::Remote.startup_announce(startup_data)
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Exceptional::Startup, 'announce_and_authenticate' do
4
+ it "raise StartupException if api_key is nil" do
5
+ Exceptional::Config.api_key = ''
6
+ lambda { Exceptional::Startup.announce }.should raise_error(Exceptional::StartupException, /API Key/)
7
+ end
8
+ it "calls Remote announce" do
9
+ Exceptional::Config.api_key = '123'
10
+ Exceptional::Remote.should_receive(:startup_announce).with(hash_including({'client' => { 'name' => Exceptional::CLIENT_NAME,
11
+ 'version' => Exceptional::VERSION,
12
+ 'protocol_version' => Exceptional::PROTOCOL_VERSION}}))
13
+ Exceptional::Startup.announce
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ context 'resuce errors from within a block' do
4
+ class FunkyException < StandardError; end
5
+ it "send them to catcher and reraise" do
6
+ to_raise = FunkyException.new
7
+ Exceptional::Catcher.should_receive(:handle).with(to_raise)
8
+ begin
9
+ Exceptional.rescue do
10
+ raise to_raise
11
+ end
12
+ fail "expected to raise"
13
+ rescue FunkyException => e
14
+ e.should == to_raise
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ api-key: abc123
2
+ ssl: true
3
+ remote-port: 123
4
+ remote-host: example.com
5
+ http-proxy-host: annoying-proxy.example.com
6
+ http-proxy-port: 1066
7
+ http-proxy-username: bob
8
+ http-proxy-password: jack
9
+ http-open-timeout: 5
10
+ http-read-timeout: 10
@@ -0,0 +1,35 @@
1
+ # here are the settings that are common to all environments
2
+ common: &default_settings
3
+ # You must specify your Exceptional API key here.
4
+ api-key: abc123
5
+ # Exceptional creates a separate log file from your application's logs
6
+ # available levels are debug, info, warn, error, fatal
7
+ log-level: info
8
+ # The exceptional agent sends data via regular http by default
9
+ # Setting this value to true will send data over SSL, increasing security
10
+ # There will be an additional CPU overhead in encrypting the data, however
11
+ # as long as your deployment environment is not Passenger (mod_rails), this
12
+ # happens in the background so as not to incur a page wait for your users.
13
+ ssl: false
14
+
15
+ development:
16
+ <<: *default_settings
17
+ # Normally no reason to collect exceptions in development
18
+ # NOTE: for trial purposes you may want to enable exceptional in development
19
+ enabled: false
20
+
21
+ test:
22
+ <<: *default_settings
23
+ # No reason to collect exceptions when running tests by default
24
+ enabled: false
25
+
26
+ production:
27
+ <<: *default_settings
28
+ enabled: true
29
+ ssl: true
30
+
31
+ staging:
32
+ # It's common development practice to have a staging environment that closely
33
+ # mirrors production, by default catch errors in this environment too.
34
+ <<: *default_settings
35
+ enabled: true
@@ -0,0 +1,39 @@
1
+ require 'ginger'
2
+
3
+ class ScenarioWithName < Ginger::Scenario
4
+ attr_accessor :name
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+ end
9
+
10
+ def create_scenario(version)
11
+ scenario = ScenarioWithName.new("Rails #{version}")
12
+ scenario[/^active_?support$/] = version
13
+ scenario[/^active_?record$/] = version
14
+ scenario[/^action_?pack$/] = version
15
+ scenario[/^action_?controller$/] = version
16
+ scenario[/^rails$/] = version
17
+ scenario
18
+ end
19
+
20
+ Ginger.configure do |config|
21
+ config.aliases["active_record"] = "activerecord"
22
+ config.aliases["active_support"] = "activesupport"
23
+ config.aliases["action_controller"] = "actionpack"
24
+
25
+ rails_1_2_6 = ScenarioWithName.new("Rails 1.2.6")
26
+ rails_1_2_6[/^active_?support$/] = "1.4.4"
27
+ rails_1_2_6[/^active_?record$/] = "1.15.6"
28
+ rails_1_2_6[/^action_?pack$/] = "1.13.6"
29
+ rails_1_2_6[/^action_?controller$/] = "1.13.6"
30
+ rails_1_2_6[/^rails$/] = "1.2.6"
31
+
32
+ config.scenarios << rails_1_2_6
33
+ config.scenarios << create_scenario("2.0.2")
34
+ config.scenarios << create_scenario("2.1.2")
35
+ config.scenarios << create_scenario("2.2.2")
36
+ config.scenarios << create_scenario("2.3.2")
37
+ config.scenarios << create_scenario("2.3.3")
38
+ config.scenarios << create_scenario("2.3.4")
39
+ end
@@ -0,0 +1,68 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'exceptional', 'integration', 'rails')
3
+
4
+ describe Exceptional, 'version number' do
5
+ it "be available proramatically" do
6
+ Exceptional::VERSION.should == '0.2.0'
7
+ end
8
+ end
9
+
10
+ describe ActiveSupport::JSON, 'standards compliant json' do
11
+ it "quote keys" do
12
+ {:a => '123'}.to_json.gsub(/ /,'').should == '{"a":"123"}'
13
+ end
14
+ end
15
+
16
+ class TestingController < ActionController::Base
17
+ def raises_something
18
+ raise StandardError
19
+ end
20
+ end
21
+
22
+ describe TestingController do
23
+ before :each do
24
+ @controller = TestingController.new
25
+ end
26
+
27
+ it 'handle exception with Exceptional::Catcher' do
28
+ Exceptional::Catcher.should_receive(:handle).with(an_instance_of(StandardError), @controller, an_instance_of(ActionController::TestRequest))
29
+ send_request(:raises_something)
30
+ end
31
+
32
+ it "still return an error response to the user" do
33
+ Exceptional::Catcher.stub!(:handle)
34
+ send_request(:raises_something)
35
+ @response.code.should == '500'
36
+ end
37
+ end
38
+
39
+ if ActionController::Base.respond_to?(:rescue_from)
40
+ class CustomError < StandardError; end
41
+ class TestingWithRescueFromController < ActionController::Base
42
+ rescue_from CustomError, :with => :custom_handler
43
+ def raises_custom_error
44
+ raise CustomError.new
45
+ end
46
+ def raises_other_error
47
+ raise StandardError.new
48
+ end
49
+ def custom_handler
50
+ head :ok
51
+ end
52
+ end
53
+
54
+ describe TestingWithRescueFromController do
55
+ before :each do
56
+ @controller = TestingWithRescueFromController.new
57
+ end
58
+
59
+ it 'not handle exception with Exceptional that is dealt with by rescue_from' do
60
+ Exceptional::Catcher.should_not_receive(:handle)
61
+ send_request(:raises_custom_error)
62
+ end
63
+ it 'handle exception with Exceptional that is not dealt with by rescue_from' do
64
+ Exceptional::Catcher.should_receive(:handle)
65
+ send_request(:raises_other_error)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ begin
3
+ require 'ginger'
4
+ rescue LoadError
5
+ raise "cant load ginger"
6
+ end
7
+ gem 'rails'
8
+ require File.dirname(__FILE__) + '/../lib/exceptional' unless defined?(Exceptional)
9
+
10
+ ENV['RAILS_ENV'] = 'test'
11
+
12
+ require 'action_controller'
13
+ require 'action_controller/test_process'
14
+
15
+ def send_request(action = nil)
16
+ @request = ActionController::TestRequest.new
17
+ @request.action = action ? action.to_s : ""
18
+ @response = ActionController::TestResponse.new
19
+ @controller.process(@request, @response)
20
+ end
@@ -0,0 +1,9 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'exceptional', 'integration', 'rails')
3
+
4
+ describe Exceptional do
5
+ it "set the api key" do
6
+ Exceptional.configure('api-key')
7
+ Exceptional::Config.api_key.should == 'api-key'
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ namespace :exceptional do
2
+ desc 'Send a test exception to Exceptional.'
3
+ task :test => :environment do
4
+ unless Exceptional::Config.api_key.blank?
5
+ puts "Sending test exception to Exceptional"
6
+ require "exceptional/integration/tester"
7
+ Exceptional::Integration.test
8
+ puts "Done."
9
+ end
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exceptional
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Contrast
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-13 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.0
24
+ version:
25
+ description: Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service). Use it to find out about errors that happen in your live app. It captures lots of helpful information to help you fix the errors.
26
+ email: hello@contrast.ie
27
+ executables:
28
+ - exceptional
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/exceptional/application_environment.rb
35
+ - lib/exceptional/catcher.rb
36
+ - lib/exceptional/config.rb
37
+ - lib/exceptional/exception_data.rb
38
+ - lib/exceptional/integration/rack.rb
39
+ - lib/exceptional/integration/rails.rb
40
+ - lib/exceptional/integration/tester.rb
41
+ - lib/exceptional/log_factory.rb
42
+ - lib/exceptional/remote.rb
43
+ - lib/exceptional/startup.rb
44
+ - lib/exceptional.rb
45
+ - spec/bin/ginger
46
+ - spec/exceptional/catcher_spec.rb
47
+ - spec/exceptional/config_spec.rb
48
+ - spec/exceptional/exception_data_spec.rb
49
+ - spec/exceptional/remote_spec.rb
50
+ - spec/exceptional/startup_spec.rb
51
+ - spec/exceptional_rescue_spec.rb
52
+ - spec/fixtures/exceptional.yml
53
+ - spec/fixtures/exceptional_old.yml
54
+ - spec/ginger_scenarios.rb
55
+ - spec/rails_integration_spec.rb
56
+ - spec/spec_helper.rb
57
+ - spec/standalone_spec.rb
58
+ - rails/init.rb
59
+ - tasks/exceptional_tasks.rake
60
+ - init.rb
61
+ - install.rb
62
+ - exceptional.gemspec
63
+ has_rdoc: true
64
+ homepage: http://getexceptional.com/
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ requirements: []
85
+
86
+ rubyforge_project: exceptional
87
+ rubygems_version: 1.3.5
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)
91
+ test_files: []
92
+