Skip to content
3 changes: 3 additions & 0 deletions app/models/aggregate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Aggregate < ActiveRecord::Base
belongs_to :grouped_issue
end
18 changes: 1 addition & 17 deletions app/models/grouped_issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class GroupedIssue < ActiveRecord::Base
has_many :subscribers, -> { uniq }, through: :issues, foreign_key: 'group_id'
has_many :issues, foreign_key: 'group_id', dependent: :destroy
has_many :messages, through: :issues
has_many :aggregates
enumerize :level, in: [:debug, :error, :fatal, :info, :warning], default: :error
# enumerize :issue_logger, in: { javascript: 1, php: 2 }, default: :javascript
enumerize :status, in: { muted: 1, resolved: 2, unresolved: 3 }, default: :unresolved, predicates: true, scope: true
friendly_id :message, use: :slugged
before_save :check_fields
Expand All @@ -16,22 +16,6 @@ def users_affected
subscribers.count
end

def aggregations (attribute)
data = []
self.issues.each do |issue|
value = issue.public_send(attribute)
unless value.nil?
found = data.index { |x| x[attribute] == value }
if found
data[found]["count"] += 1
else
data.push( { attribute => value, "count" => 0, "created_at" => issue.created_at, "updated_at" => issue.updated_at } )
end
end
end
data
end

private

def check_fields
Expand Down
73 changes: 66 additions & 7 deletions app/models/issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
accepts_nested_attributes_for :messages
validates :message, presence: true
after_create :issue_created
after_commit :refresh_aggregates

def error
ErrorStore.find(self)
Expand All @@ -20,6 +21,10 @@ def data
decode_and_decompress(super)
end

def refresh_aggregates
AggregatesWorker.perform_async(self.id)
end

def get_interfaces(interface = nil)
all_interaces = error._get_interfaces
return all_interaces if interface.nil?
Expand All @@ -40,7 +45,7 @@ def breadcrumbs_stacktrace

def http_data(key = nil)
all_data = get_interfaces(:http).try(:_data)
return false if all_data.blank?
return nil if all_data.blank?
return all_data if key.nil?
all_data[key]
end
Expand All @@ -57,7 +62,7 @@ def get_platform_frames
end

def get_frames(frame = nil)
frames = get_platform_frames.first
frames = get_platform_frames.try(:first)
return frames if frame.nil?
frames._data[frame]
end
Expand All @@ -66,18 +71,72 @@ def environment
error.data[:environment]
end

def browser
headers = error.data.try(:[], :interfaces).try(:[], :http).try(:[], :headers)
unless headers.nil?
headers.each do |hash|
return UserAgent.parse(hash[:user_agent]).browser if hash[:user_agent]
def version
modules = error.data[:modules]
unless modules.nil?
case self.platform
when 'ruby'
key = 'rails'
end
"#{key.capitalize}/#{modules[key.to_sym]}" if defined? key
end
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def notifier_remote_address
http_data(:env).try(:[], :REMOTE_ADDR)
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def server_hostnames
error.data.try(:[], :server_name)
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def file
get_frames.try(:get_culprit_string, with_lineno: get_frames.try(:_data).try(:[], :lineno).present?)
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def url
url_string = http_data(:url)
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def browser_platform
user_agent = get_headers(:user_agent)
return UserAgent.parse(user_agent).platform unless user_agent.nil?
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def browser
user_agent = get_headers(:user_agent)
return UserAgent.parse(user_agent).browser unless user_agent.nil?
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def user
user_data = get_interfaces(:user)
return "##{user_data._data[:id]} #{user_data._data[:user_name]} #{user_data._data[:email]}" unless user_data.nil?
rescue => e
Raven.capture_exception(e)
'Could not parse data!'
end

def self.more_than_10_errors(member)
GroupedIssueMailer.more_than_10_errors(member).deliver_later
end
Expand Down
24 changes: 10 additions & 14 deletions app/views/errors/_aggregations.html.haml
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
.panel.panel-default
.panel-heading
%a{"data-parent" => "#accordion", "data-toggle" => "collapse", :href => "#collapse#{panel}"}
%h4.panel-title=title
.panel-collapse.collapse{ id: "collapse#{panel}", class: "#{panel == 'One' ? 'in' : ''}" }
%a{"data-parent" => "#accordion", "data-toggle" => "collapse", :href => "#collapse#{data.name}"}
%h4.panel-title=data.name.humanize
.panel-collapse.collapse{ id: "collapse#{data.name}", class: "#{data.name == @error.aggregates[0].name ? 'in' : ''}" }
.panel-body
%table.table#members-container
%thead
%tr
%th=attribute.capitalize
%th=data.name.humanize
%th # of notices
%th First seen at
%th Last seen at
%tbody.websites
- data.each do |element|
- data.value.keys.each do |key|
%tr
- if defined?(secondary)
%td
=element[attribute][secondary]
- else
%td
=element[attribute]
%td
=element["count"]
=key
%td
=element["created_at"].strftime("%Y-%m-%d %l:%M")
=data.value[key]["count"]
%td
=element["updated_at"].strftime("%Y-%m-%d %l:%M")
=data.value[key]["created_at"].to_time.strftime("%Y-%m-%d %l:%M")
%td
=data.value[key]["updated_at"].to_time.strftime("%Y-%m-%d %l:%M")
7 changes: 3 additions & 4 deletions app/views/errors/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
#aggregations.tab-pane{ class: "#{params[:current_tab] == 'aggregations' ? 'active' : ''}" }
- if params[:current_tab] == 'aggregations'
#accordion.panel-group
= render partial: 'errors/aggregations', locals: { title: 'Messages', attribute: "message", data: @error.aggregations("message"), panel: "One" }
// use secondary if the returned item is a hash
= render partial: 'errors/aggregations', locals: { title: 'Subscribers', attribute: "subscriber", secondary: "name", data: @error.aggregations("subscriber"), panel: "Two" }
= render partial: 'errors/aggregations', locals: { title: 'Browsers', attribute: "browser", data: @error.aggregations("browser"), panel: "Three" }
- unless @error.aggregates.blank?
= render partial: 'errors/aggregations', collection: @error.aggregates, as: :data
d
7 changes: 7 additions & 0 deletions app/workers/aggregates_worker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AggregatesWorker
include Sidekiq::Worker
def perform(issue_id)
record = Issue.find(issue_id)
ErrorStore::Aggregates.new(record).handle_aggregates
end
end
11 changes: 11 additions & 0 deletions db/migrate/20160818125259_create_aggregations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateAggregations < ActiveRecord::Migration
def change
create_table :aggregates do |t|
t.belongs_to :grouped_issue, index: true, foreign_key: { references: :grouped_issues, on_update: :restrict, on_delete: :cascade }
t.string :name
t.jsonb :value, default: {}

t.timestamps null: false
end
end
end
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20160808131218) do
ActiveRecord::Schema.define(version: 20160818125259) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -73,6 +73,14 @@
end
add_index "grouped_issues", ["website_id", "checksum"], :name=>"index_grouped_issues_on_website_id_and_checksum", :unique=>true

create_table "aggregates", force: :cascade do |t|
t.integer "grouped_issue_id", :index=>{:name=>"index_aggregates_on_grouped_issue_id"}, :foreign_key=>{:references=>"grouped_issues", :name=>"fk_aggregates_grouped_issue_id", :on_update=>:restrict, :on_delete=>:cascade}
t.string "name"
t.jsonb "value", :default=>{}
t.datetime "created_at", :null=>false
t.datetime "updated_at", :null=>false
end

create_table "integrations", force: :cascade do |t|
t.integer "website_id", :index=>{:name=>"index_integrations_on_website_id"}, :foreign_key=>{:references=>"websites", :name=>"fk_integrations_website_id", :on_update=>:restrict, :on_delete=>:cascade}
t.string "provider", :null=>false
Expand Down
39 changes: 39 additions & 0 deletions lib/error_store/aggregates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module ErrorStore
class Aggregates < StoreError

def initialize(issue)
@issue = issue
end

ATTRIBUTES = [
'message',
'subscriber',
'browser',
'browser_platform',
'user',
'url',
'version',
'file',
'server_hostnames',
'notifier_remote_address'
]

def handle_aggregates
unless @issue.nil?
ATTRIBUTES.each do |attribute|
unless (data = @issue.public_send(attribute)).nil?
record = @issue.group.aggregates.find_or_create_by(name: attribute)
data = data.name if attribute == 'subscriber'
if record.value[data].nil?
record.value[data] = { :count => 1, :created_at => @issue.created_at, :updated_at => @issue.updated_at }
else
record.value[data]['count'] += 1
record.value[data]['updated_at'] = @issue.updated_at
end
record.save
end
end
end
end
end
end
76 changes: 76 additions & 0 deletions spec/lib/error_store/aggregates_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'rails_helper'

RSpec.describe ErrorStore::Aggregates do
let(:user) { create :user }
let(:website) { create :website }
let!(:website_member) { create :website_member, website: website, user: user }
let!(:grouped_issue) { create :grouped_issue, website: website }
let(:subscriber) { create :subscriber, website: website }
let!(:issue_error) { create :issue, subscriber: subscriber, group: grouped_issue }
let(:mozilla_browser) {
'{"server_name":"sergiu-Lenovo-IdeaPad-Y510P","modules":{"rake":"10.4.2","i18n":"0.7.0","json":"1.8.3","minitest":"5.8.2","thread_safe":"0.3.5","tzinfo":"1.2.2","activesupport":"4.2.1","builder":"3.2.2","erubis":"2.7.0","mini_portile":"0.6.2","nokogiri":"1.6.6.2","rails-deprecated_sanitizer":"1.0.3","rails-dom-testing":"1.0.7","loofah":"2.0.3","rails-html-sanitizer":"1.0.2","actionview":"4.2.1","rack":"1.6.4","rack-test":"0.6.3","actionpack":"4.2.1","globalid":"0.3.6","activejob":"4.2.1","mime-types":"2.6.2","mail":"2.6.3","actionmailer":"4.2.1","activemodel":"4.2.1","arel":"6.0.3","activerecord":"4.2.1","debug_inspector":"0.0.2","binding_of_caller":"0.7.2","bundler":"1.11.2","coderay":"1.1.0","coffee-script-source":"1.10.0","execjs":"2.6.0","coffee-script":"2.4.1","thor":"0.19.1","railties":"4.2.1","coffee-rails":"4.1.0","multipart-post":"2.0.0","faraday":"0.9.2","multi_json":"1.11.2","jbuilder":"2.3.2","jquery-rails":"4.0.5","method_source":"0.8.2","slop":"3.6.0","pry":"0.10.3","sprockets":"3.4.0","sprockets-rails":"2.3.3","rails":"4.2.1","rdoc":"4.2.0","sass":"3.4.19","tilt":"2.0.1","sass-rails":"5.0.4","sdoc":"0.4.1","sentry-raven":"0.15.2","spring":"1.4.1","sqlite3":"1.3.11","turbolinks":"2.5.3","uglifier":"2.7.2","web-console":"2.2.1"},"extra":{},"tags":{},"errors":[{"type":"invalid_data","name":"timestamp","value":"2016-02-15T06:01:29"}],"interfaces":{"exception":{"values":[{"type":"ZeroDivisionError","value":"\"divided by 0\"","module":"","stacktrace":{"frames":[{"abs_path":"\/home\/sergiu\/.rvm\/rubies\/ruby-2.2.2\/lib\/ruby\/2.2.0\/webrick\/server.rb","filename":"webrick\/server.rb","function":"block in start_thread","context_line":" block ? block.call(sock) : run(sock)\n","pre_context":["module ActionController\n"," module ImplicitRender\n"," def send_action(method, *args)\n"],"post_context":[" default_render unless performed?\n"," ret\n"," end\n"],"lineno":4},{"abs_path":"\/home\/sergiu\/ravenapp\/app\/controllers\/home_controller.rb","filename":"app\/controllers\/home_controller.rb","function":"index","context_line":" 1\/0\n","pre_context":[" # Prevent CSRF attacks by raising an exception.\n"," # For APIs, you may want to use :null_session instead.\n"," def index\n"],"post_context":[" end\n","end\n",""],"lineno":5},{"abs_path":"\/home\/sergiu\/ravenapp\/app\/controllers\/home_controller.rb","filename":"app\/controllers\/home_controller.rb","function":"\/","context_line":" 1\/0\n","pre_context":[" # Prevent CSRF attacks by raising an exception.\n"," # For APIs, you may want to use :null_session instead.\n"," def index\n"],"post_context":[" end\n","end\n",""],"lineno":5}],"frames_omitted":null,"has_frames":true}}],"exc_omitted":null},"http":{"env":{"REMOTE_ADDR":"127.0.0.1","SERVER_NAME":"localhost","SERVER_PORT":"3001"},"headers":[{"host":"localhost:3001"},{"connection":"keep-alive"},{"accept":"text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,*\/*;q=0.8"},{"upgrade_insecure_requests":"1"},{"user_agent":"Mozilla\/5.0 (X11; Linux x86_64) Gecko\/20100101 Firefox\/46.0"},{"accept_encoding":"gzip, deflate, sdch"},{"accept_language":"en-US,en;q=0.8"},{"cookie":"currentConfigName=%22default%22; pickedWebsite=1; _epiclogger_session=NTIwU2prYUd2T0dEd3FGQWE0WUFaL3RDY0huRGFnV1Z5TEhOQ3RtWUZZTTVRSzgvRTZvdEI2SFRsSVQ0ajVidHRWQnA5ck9wT3ZQU095N0dkWjEza0dpYWlmekxyRzFJbEFVNk5zRExmMzg3Q2c2MFBWdi9UYVUyWjdkWHVMNUFaWFJzZ2pEMGdwd3JJSGpSNjlEK2o3ZjlWZEhISEprK1hXOFNvaGdRdHg2MkFxN0lrcmlIdUtQazVWUjNGaWJvUGVYTHJncEc2OWhpaHBZbXNqcVhUcjM0ZWQ5bDFnWDBVSGlaOE5rdGxiOHNDU2NUS3BaSjd4eUZSRklzVnU5M3Z0TmJLUzF6ZWxjOGUrRmF2NkZ6ZCtGMUdoQVdFUSt0am9KT2lDODRMckJwbWQ1ZU5hV1hhZmt2bHdDZHZibEFmMExXNTI5Tmt..."},{"version":"HTTP\/1.1"}],"url":"http:\/\/localhost\/\/"}},"site":null,"environment":null,"version":"5"}'
}
let(:issue2) { create :issue, subscriber, group: grouped_issue, data: mozilla_browser }

describe 'intialize' do
it 'assigns provided issue' do
expect( ErrorStore::Aggregates.new(issue_error).instance_variable_get(:@issue) ).to eq(issue_error)
end
end

it 'ATTRIBUTES' do
expect(ErrorStore::Aggregates::ATTRIBUTES).to eq(
[
'message',
'subscriber',
'browser',
'browser_platform',
'user',
'url',
'version',
'file',
'server_hostnames',
'notifier_remote_address'
])
end

describe 'handle_aggregates' do
subject { ErrorStore::Aggregates.new(issue_error).handle_aggregates }

it 'should return nil if no issue' do
expect{ErrorStore::Aggregates.new(nil).handle_aggregates}.not_to change{Aggregate.count}
end

it 'should not create/update record if data is nil' do
subject
expect( Aggregate.find_by_name('user') ).to be_nil
end

it 'should create aggregate if record not found' do
expect{ subject }.to change{ Aggregate.find_by_name('message') }.from(nil)
end

it 'should update existing aggregate' do
record = grouped_issue.aggregates.create( name: 'message', value: { issue_error.message => { :count => 1, :created_at => issue_error.created_at, :updated_at => issue_error.updated_at } } )

expect{
subject
record.reload
}.to change{ record.value[issue_error.message]["count"] }.from(1).to(2)
end

it 'should update with a new value' do
record = grouped_issue.aggregates.create( name: 'message', value: { 'Not good' => { :count => 1, :created_at => issue_error.created_at, :updated_at => issue_error.updated_at } } )

expect{
subject
record.reload
}.to change{ record.value.length }.from(1).to(2)
end

it 'should increase the number of records' do
expect{ subject }.to change{ Aggregate.count }
end

end
end
Loading