Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Breaking Airbrake

  • Loading branch information...
commit 7e2afbe83b9b9f8908b39dcee461f676eb57cbed 1 parent 83d84fb
@iain authored
Showing with 506 additions and 8 deletions.
  1. +5 −0 Gemfile
  2. +15 −0 Gemfile.lock
  3. +5 −0 app/assets/stylesheets/_layout.sass
  4. +10 −0 app/controllers/airbrake/errors_controller.rb
  5. +10 −0 app/controllers/airbrake/projects_controller.rb
  6. +1 −1  app/controllers/charts_controller.rb
  7. +4 −0 app/controllers/errors_controller.rb
  8. +5 −0 app/models/airbrake.rb
  9. +17 −0 app/models/airbrake/error.rb
  10. +16 −0 app/models/airbrake/for_project.rb
  11. +17 −0 app/models/airbrake/project.rb
  12. +2 −0  app/models/project.rb
  13. +10 −0 app/views/errors/index.html.haml
  14. +2 −0  app/views/projects/_header.html.haml
  15. +4 −0 app/views/projects/edit.html.haml
  16. +3 −3 app/views/projects/index.html.haml
  17. +8 −1 config/routes.rb
  18. +12 −0 db/migrate/20120116134746_create_airbrake_projects.rb
  19. +21 −0 db/migrate/20120116165047_create_airbrake_errors.rb
  20. +25 −1 db/schema.rb
  21. +6 −0 lib/airbrake/deploys.rb
  22. +6 −0 lib/airbrake/errors.rb
  23. +48 −0 lib/charts/errors.rb
  24. +102 −0 script/airbrake_errors.rb
  25. +65 −0 script/airbrake_projects.rb
  26. +35 −0 script/daemon
  27. +5 −0 spec/controllers/airbrake/errors_controller_spec.rb
  28. +5 −0 spec/controllers/airbrake/projects_controller_spec.rb
  29. +5 −0 spec/controllers/errors_controller_spec.rb
  30. +12 −0 spec/factories/airbrake_errors.rb
  31. +10 −0 spec/factories/airbrake_projects.rb
  32. +5 −0 spec/models/airbrake/error_spec.rb
  33. +7 −0 spec/models/airbrake/project_spec.rb
  34. +3 −2 spec/models/project_spec.rb
View
5 Gemfile
@@ -11,6 +11,11 @@ gem 'simple_form', :git => 'git://github.com/plataformatec/simple_form.git'
gem 'pjax_rails'
gem 'inherited_resources'
+gem 'faraday'
+gem 'faraday_middleware'
+gem 'typhoeus'
+gem 'nokogiri'
+
group :assets do
gem 'compass', '~> 0.12.alpha.4'
gem 'sass-rails'
View
15 Gemfile.lock
@@ -36,6 +36,7 @@ GEM
activesupport (3.2.0.rc2)
i18n (~> 0.6)
multi_json (~> 1.0)
+ addressable (2.2.6)
arel (3.0.0)
builder (3.0.0)
chunky_png (1.2.5)
@@ -59,6 +60,12 @@ GEM
factory_girl_rails (1.5.0)
factory_girl (~> 2.4.0)
railties (>= 3.0.0)
+ faraday (0.7.5)
+ addressable (~> 2.2.6)
+ multipart-post (~> 1.1.3)
+ rack (>= 1.1.0, < 2)
+ faraday_middleware (0.7.0)
+ faraday (~> 0.7.3)
friendly_id (4.0.0)
fssm (0.2.8.1)
haml (3.1.4)
@@ -79,6 +86,8 @@ GEM
treetop (~> 1.4.8)
mime-types (1.17.2)
multi_json (1.0.4)
+ multipart-post (1.1.4)
+ nokogiri (1.5.0)
pjax_rails (0.1.10)
jquery-rails
polyglot (0.3.3)
@@ -137,6 +146,8 @@ GEM
treetop (1.4.10)
polyglot
polyglot (>= 0.3.1)
+ typhoeus (0.3.3)
+ mime-types
tzinfo (0.3.31)
uglifier (1.2.2)
execjs (>= 0.3.0)
@@ -149,10 +160,13 @@ DEPENDENCIES
coffee-rails
compass (~> 0.12.alpha.4)
factory_girl_rails
+ faraday
+ faraday_middleware
friendly_id
haml
inherited_resources
jquery-rails
+ nokogiri
pjax_rails
rails (= 3.2.0.rc2)
rspec-rails
@@ -160,4 +174,5 @@ DEPENDENCIES
shoulda-matchers
simple_form!
sqlite3
+ typhoeus
uglifier
View
5 app/assets/stylesheets/_layout.sass
@@ -18,3 +18,8 @@ iframe
margin-left: 170px
.new
list-style-type: circle
+
+.ellipsis
+ text-overflow: ellipsis
+.nowrap
+ white-space: nowrap
View
10 app/controllers/airbrake/errors_controller.rb
@@ -0,0 +1,10 @@
+class Airbrake::ErrorsController < ApplicationController
+
+ skip_before_filter :verify_authenticity_token, :only => [:create]
+
+ def create
+ Airbrake::Error.import(params[:errors])
+ head :ok
+ end
+
+end
View
10 app/controllers/airbrake/projects_controller.rb
@@ -0,0 +1,10 @@
+class Airbrake::ProjectsController < ApplicationController
+
+ skip_before_filter :verify_authenticity_token, :only => [:create]
+
+ def create
+ Airbrake::Project.import(params[:project])
+ head :ok
+ end
+
+end
View
2  app/controllers/charts_controller.rb
@@ -2,7 +2,7 @@ class ChartsController < ApplicationController
def show
project = Project.find(params[:project_id])
- render :json => Charts::Overview[project]
+ render :json => Charts::Errors[project]
end
end
View
4 app/controllers/errors_controller.rb
@@ -0,0 +1,4 @@
+class ErrorsController < InheritedResources::Base
+ belongs_to :project
+ defaults :resource_class => Airbrake::Error, :collection_name => 'airbrake_errors', :instance_name => 'airbrake_error'
+end
View
5 app/models/airbrake.rb
@@ -0,0 +1,5 @@
+module Airbrake
+ def self.table_name_prefix
+ 'airbrake_'
+ end
+end
View
17 app/models/airbrake/error.rb
@@ -0,0 +1,17 @@
+class Airbrake::Error < ActiveRecord::Base
+
+ validates_presence_of :airbrake_project_id, :group_id, :notice_id
+
+ def self.import(data)
+ data[:notices].each do |notice|
+ error = where(:notice_id => notice[:id]).first || new(:notice_id => notice[:id])
+ error.resolved = data[:resolved]
+ error.airbrake_project_id = data[:project_id]
+ error.error_message = data[:error_message]
+ error.occurred_at = Time.parse(notice[:created_at])
+ error.group_id = data[:id]
+ error.save!
+ end
+ end
+
+end
View
16 app/models/airbrake/for_project.rb
@@ -0,0 +1,16 @@
+module Airbrake::ForProject
+
+ def self.included(project)
+ project.has_one :airbrake_project, :class_name => "Airbrake::Project"
+ project.has_many :airbrake_errors, :through => :airbrake_project, :class_name => "Airbrake::Error"
+ end
+
+ def airbrake_project_id
+ airbrake_project.try(:id)
+ end
+
+ def airbrake_project_id=(id)
+ self.airbrake_project = Airbrake::Project.find_by_id(id)
+ end
+
+end
View
17 app/models/airbrake/project.rb
@@ -0,0 +1,17 @@
+class Airbrake::Project < ActiveRecord::Base
+ belongs_to :project
+
+ has_many :airbrake_errors, :foreign_key => "airbrake_project_id", :primary_key => :airbrake_id
+
+ def to_label
+ "#{name} (##{airbrake_id})"
+ end
+
+ def self.import(data)
+ project = where(:airbrake_id => data[:id]).first || new(:airbrake_id => data[:id])
+ project.name = data[:name]
+ project.api_key = data[:api_key]
+ project.save!
+ end
+
+end
View
2  app/models/project.rb
@@ -12,4 +12,6 @@ def self.list
order(:name)
end
+ include Airbrake::ForProject
+
end
View
10 app/views/errors/index.html.haml
@@ -0,0 +1,10 @@
+= render "projects/header", :project => @project, :tab => "errors"
+
+#foozing{data("chart", project_chart_path(@project, :errors))}
+
+%table.zebra-striped
+ %tbody
+ - @airbrake_errors.order(:occurred_at).reverse_order.each do |error|
+ %tr
+ %td.ellipsis= error.error_message
+ %td.nowrap= I18n.l error.occurred_at
View
2  app/views/projects/_header.html.haml
@@ -7,6 +7,8 @@
%ul.tabs
%li{active_when(tab == "overview")}= link_to "Overview", project
+
+ %li{active_when(tab == "errors")}= link_to "Errors", project_errors_path(project)
- project.iframes.list.each do |iframe|
%li{active_when(tab == iframe.to_param)}= link_to iframe.name, [ project, iframe ]
%li{active_when(tab == "edit")}= link_to "Edit", edit_project_path(project)
View
4 app/views/projects/edit.html.haml
@@ -14,6 +14,10 @@
(#{iframe.url.truncate(100)})
%li.new= link_to "new iframe", new_project_iframe_path(@project)
+ %fieldset
+ %legend Airbrake
+ = f.input :airbrake_project_id, :collection => Airbrake::Project.all
+
= render "shared/actions", :f => f
.well
View
6 app/views/projects/index.html.haml
@@ -14,6 +14,6 @@
- projects.each do |project|
%tr[project]
%td= link_to project.name, project
- %td 1
- %td 1
- %td 1
+ %td= project.airbrake_errors.count
+ %td -
+ %td -
View
9 config/routes.rb
@@ -4,9 +4,16 @@
resources :projects do
- resources :iframes
+ resources :iframes, :except => [ :index ]
get "/chart/:id" => "charts#show", :as => :chart
+ resources :errors
+
+ end
+
+ namespace :airbrake do
+ resources :errors
+ resources :projects
end
end
View
12 db/migrate/20120116134746_create_airbrake_projects.rb
@@ -0,0 +1,12 @@
+class CreateAirbrakeProjects < ActiveRecord::Migration
+ def change
+ create_table :airbrake_projects do |t|
+ t.integer :project_id
+ t.string :name
+ t.integer :airbrake_id
+ t.string :api_key
+
+ t.timestamps
+ end
+ end
+end
View
21 db/migrate/20120116165047_create_airbrake_errors.rb
@@ -0,0 +1,21 @@
+class CreateAirbrakeErrors < ActiveRecord::Migration
+ def change
+ create_table :airbrake_errors do |t|
+
+ t.integer :airbrake_project_id, :null => false
+ t.integer :group_id, :null => false
+ t.integer :notice_id, :null => false
+
+ t.text :error_message
+ t.boolean :resolved, :default => false
+ t.datetime :occurred_at
+
+ t.timestamps
+ end
+
+ add_index :airbrake_errors, :airbrake_project_id
+ add_index :airbrake_errors, :notice_id
+ add_index :airbrake_errors, :group_id
+
+ end
+end
View
26 db/schema.rb
@@ -11,7 +11,31 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20120115232627) do
+ActiveRecord::Schema.define(:version => 20120116165047) do
+
+ create_table "airbrake_errors", :force => true do |t|
+ t.integer "airbrake_project_id", :null => false
+ t.integer "group_id", :null => false
+ t.integer "notice_id", :null => false
+ t.text "error_message"
+ t.boolean "resolved", :default => false
+ t.datetime "occurred_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
+ add_index "airbrake_errors", ["airbrake_project_id"], :name => "index_airbrake_errors_on_airbrake_project_id"
+ add_index "airbrake_errors", ["group_id"], :name => "index_airbrake_errors_on_group_id"
+ add_index "airbrake_errors", ["notice_id"], :name => "index_airbrake_errors_on_notice_id"
+
+ create_table "airbrake_projects", :force => true do |t|
+ t.integer "project_id"
+ t.string "name"
+ t.integer "airbrake_id"
+ t.string "api_key"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
create_table "iframes", :force => true do |t|
t.string "name"
View
6 lib/airbrake/deploys.rb
@@ -0,0 +1,6 @@
+module Airbrake
+ class Deploys
+ def self.store(data)
+ end
+ end
+end
View
6 lib/airbrake/errors.rb
@@ -0,0 +1,6 @@
+module Airbrake
+ class Errors
+ def self.store(data)
+ end
+ end
+end
View
48 lib/charts/errors.rb
@@ -0,0 +1,48 @@
+module Charts
+
+ class Errors
+
+ def self.[](project)
+ query = project.airbrake_errors.group("date").select("COUNT(airbrake_errors.id) AS count, DATE(airbrake_errors.occurred_at) AS date")
+ series = { false => "Unresolved", true => "Resolved" }.map do |v, name|
+ data = query.where(:resolved => v).map { |x| [ Time.parse(x[:date]).to_i * 1000, x[:count] ] }
+ { :name => name, :data => data, :pointInterval => 24 * 3600 * 1000 }
+ end
+ {
+ :chart => {
+ :renderTo => 'chart',
+ :type => 'column',
+ :zoomType => 'x',
+ :plotBorderWidth => 1
+ },
+ :credits => {
+ :enabled => false
+ },
+ :plotOptions => {
+ :series => { :animation => { :duration => 100, :easing => :linear } },
+ :column => { :shadow => false, :groupPadding => 0, :pointPadding => 0, :borderWidth => 0, :stacking => "normal" }
+ },
+ :title => {
+ :text => "Errors"
+ },
+ :xAxis => {
+ :type => 'datetime',
+ :maxZoom => 24 * 3600000,
+ :tickWidth => 1,
+ :lineWitdh => 1,
+ :max => 1.day.from_now.to_i * 1000,
+ :minorTickInterval => 'auto'
+ },
+ :yAxis => {
+ :title => { :text => "Amount" },
+ :min => 0,
+ :tickWidth => 1,
+ :lineWitdh => 1
+ },
+ :series => series
+ }
+ end
+
+ end
+
+end
View
102 script/airbrake_errors.rb
@@ -0,0 +1,102 @@
+URL = "https://finalist.airbrake.io"
+KEY = ENV["AIRBRAKE_KEY"]
+STOTS = "http://localhost:3000"
+
+require 'faraday'
+require 'faraday_middleware'
+require 'typhoeus'
+require 'nokogiri'
+require 'time'
+
+starting_time = Time.now
+
+def puts(message)
+ STDOUT.puts("[#{Time.now}] #{message}")
+end
+
+$connection = Faraday.new(:url => URL) do |builder|
+# builder.use Faraday::Response::RaiseError
+ builder.adapter :typhoeus
+end
+
+$local_connection = Faraday.new(:url => STOTS) do |builder|
+ builder.use Faraday::Request::UrlEncoded
+# builder.use Faraday::Response::RaiseError
+ builder.adapter :typhoeus
+end
+
+puts "Getting error listing from Airbrake"
+
+go_again = true
+
+def get_errors(page = 1)
+
+ groups = 0
+
+ response = $connection.get do |request|
+ request.url "/errors.xml"
+ request.params[:auth_token] = KEY
+ request.params[:page] = page
+ request.params[:show_resolved] = true
+ end
+
+ response.on_complete do
+
+ doc = Nokogiri::XML(response.body)
+
+ groups = doc.search("//groups/group")
+
+ puts "Got errors listing for #{groups.size} errors I'm now going to fetch."
+
+ groups.each do |group|
+
+ id = group.search("id").first.content
+
+ res = $connection.get do |request|
+ request.url "/errors/#{id}/notices.xml"
+ request.params[:auth_token] = KEY
+ end
+
+ res.on_complete do
+
+ puts "Got notices for #{id}"
+
+ data = {
+ :id => id,
+ :resolved => group.search("resolved").first.content == "true",
+ :project_id => group.search("project-id").first.content,
+ :error_message => group.search("error-message").first.content,
+ :notices => Nokogiri::XML(res.body).search("//notices/notice").map { |notice|
+ { :id => notice.search("id").first.content, :created_at => notice.search("created-at").first.content }
+ }
+ }
+
+ local_response = $local_connection.post do |request|
+ request.url "/airbrake/errors"
+ request.body = { :errors => data }
+ end
+
+ local_response.on_complete do
+
+ puts "Handled notices for #{id}"
+
+ end
+
+ end
+
+ end
+
+ get_errors(page + 1) if groups.size == 30 && ENV["GET_MORE"] == "true"
+
+ end
+
+end
+
+
+$connection.in_parallel(Typhoeus::Hydra.hydra) do
+ $local_connection.in_parallel(Typhoeus::Hydra.hydra) do
+ get_errors
+ end
+end
+
+puts "Done. It took me %.03f seconds" % (Time.now - starting_time)
View
65 script/airbrake_projects.rb
@@ -0,0 +1,65 @@
+URL = "https://finalist.airbrake.io"
+KEY = ENV["AIRBRAKE_KEY"]
+STOTS = "http://localhost:3000"
+
+require 'faraday'
+require 'faraday_middleware'
+require 'typhoeus'
+require 'nokogiri'
+require 'time'
+
+starting_time = Time.now
+
+def puts(message)
+ STDOUT.puts("[#{Time.now}] #{message}")
+end
+
+$connection = Faraday.new(:url => URL) do |builder|
+ builder.use Faraday::Response::RaiseError
+ builder.adapter :typhoeus
+end
+
+$local_connection = Faraday.new(:url => STOTS) do |builder|
+ builder.use Faraday::Request::UrlEncoded
+ builder.use Faraday::Response::RaiseError
+ builder.adapter :typhoeus
+end
+
+puts "Getting project listing from Airbrake"
+
+
+$connection.in_parallel(Typhoeus::Hydra.hydra) do
+ $local_connection.in_parallel(Typhoeus::Hydra.hydra) do
+ response = $connection.get do |request|
+ request.url "/data_api/v1/projects.xml"
+ request.params[:auth_token] = KEY
+ end
+
+ response.on_complete do
+
+ puts "Got projects from Airbrake"
+
+ doc = Nokogiri::XML(response.body)
+ doc.search("//projects/project").each do |project|
+ data = {
+ :id => project.search("id").first.content,
+ :name => project.search("name").first.content,
+ :api_key => project.search("api-key").first.content
+ }
+
+ local_response = $local_connection.post do |request|
+ request.url "/airbrake/projects"
+ request.body = { :project => data }
+ end
+
+ local_response.on_complete do
+ puts "Saved project #{data[:name]}"
+ end
+
+ end
+
+ end
+ end
+end
+
+puts "Done. It took me %.03f seconds" % (Time.now - starting_time)
View
35 script/daemon
@@ -0,0 +1,35 @@
+#!/usr/bin/env ruby
+
+threads = []
+
+threads << Thread.new do
+ loop do
+ puts "Running airbrake error fetcher"
+ `touch log/airbrake.log`
+ success = `ruby script/airbrake_errors.rb >> log/airbrake.log`
+ if success
+ puts "Airbrake error fetcher went OK"
+ sleep 1
+ else
+ puts "Airbrake error fetcher failed"
+ sleep 100
+ end
+ end
+end
+
+threads << Thread.new do
+ loop do
+ puts "Running airbrake project fetcher"
+ `touch log/airbrake.log`
+ success = `ruby script/airbrake_projects.rb >> log/airbrake.log`
+ if success
+ puts "Airbrake projects fetcher went OK"
+ sleep 10
+ else
+ puts "Airbrake projects fetcher failed"
+ sleep 100
+ end
+ end
+end
+
+threads.map(&:join)
View
5 spec/controllers/airbrake/errors_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Airbrake::ErrorsController do
+
+end
View
5 spec/controllers/airbrake/projects_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Airbrake::ProjectsController do
+
+end
View
5 spec/controllers/errors_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ErrorsController do
+
+end
View
12 spec/factories/airbrake_errors.rb
@@ -0,0 +1,12 @@
+# Read about factories at http://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :error do
+ airbrake_project_id 1
+ error_message "MyText"
+ resolved false
+ notice_id 1
+ occurred_at "2012-01-16 17:50:47"
+ group_id 1
+ end
+end
View
10 spec/factories/airbrake_projects.rb
@@ -0,0 +1,10 @@
+# Read about factories at http://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :airbrake_project do
+ project_id 1
+ name "MyString"
+ airbrake_id 1
+ api_key "MyString"
+ end
+end
View
5 spec/models/airbrake/error_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Airbrake::Error do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
View
7 spec/models/airbrake/project_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe Airbrake::Project do
+
+ it { should belong_to :project }
+
+end
View
5 spec/models/project_spec.rb
@@ -2,9 +2,10 @@
describe Project do
- it { should have_many(:iframes) }
+ it { should have_many :iframes }
+ it { should have_one :airbrake_project }
- it { should validate_presence_of(:name) }
+ it { should validate_presence_of :name }
specify do
create :project
Please sign in to comment.
Something went wrong with that request. Please try again.