# HG changeset patch # User Vincent Tondellier # Date 1451340503 -3600 # Node ID c82f5589db1101313b6975a464c6edbe135a394e # Parent 03caeafba2ff2d15efe46d09cb9791fd763be533 Add first cut of the crash grouping feature diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest.pm --- a/lib/CrashTest.pm Mon Dec 28 23:07:16 2015 +0100 +++ b/lib/CrashTest.pm Mon Dec 28 23:08:23 2015 +0100 @@ -4,6 +4,7 @@ use CrashTest::Model::Storage; use CrashTest::Model::StackFilter; use CrashTest::Model::CrashReport; +use CrashTest::Model::CrashGroup; use CrashTest::Model::CrashProcessor; # This method will run once at server start @@ -27,9 +28,11 @@ $self->plugin("CrashTest::Helper::DateTime"); $self->plugin("CrashTest::Helper::Backtrace"); $self->plugin("CrashTest::Helper::XmlEscape"); + $self->plugin("CrashTest::Helper::Stats"); $self->helper(crash_reports => sub { state $crash_reports = CrashTest::Model::CrashReport->new (app => $self); }); + $self->helper(crash_groups => sub { state $crash_groups = CrashTest::Model::CrashGroup->new (app => $self); }); $self->helper(crash_processor => sub { state $crash_processor = CrashTest::Model::CrashProcessor->new (app => $self, config => $self->config); }); $self->helper(stackfilter => sub { state $crash_reports = CrashTest::Model::StackFilter->new (app => $self, config => $self->config); }); @@ -45,7 +48,10 @@ # Normal route to controller $r->get('/')->to('crash_reports#index')->name('index'); - $r->get('/report/:uuid' => [ uuid => qr/[0-9a-fA-F-]+/ ])->to('crash_reports#report')->name('report'); + $r->get('/reports')->to('crash_reports#index')->name('reports'); + $r->get('/groups')->to('crash_groups#index')->name('groups'); + $r->get('/groups/:uuid' => [ uuid => qr/[0-9a-fA-F-]+/ ])->to('crash_groups#show')->name('group'); + $r->get('/report/:uuid' => [ uuid => qr/[0-9a-fA-F-]+/ ])->to('crash_reports#show')->name('report'); $r->post('/submit')->to('crash_inserter#insert'); } diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Command/get_trace.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Command/get_trace.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,55 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Command::get_trace; +use Mojo::Base 'Mojolicious::Command'; +use Mojo::JSON qw/decode_json/; +use Mojo::Util qw/slurp/; +use File::Spec::Functions qw/catdir catfile/; +use File::Basename; + +use CrashTest::Model::Thread; +use CrashTest::Plugin::CrashSignatureExtractor::C_Cpp; + +# Short description +has description => 'Get crash signature'; + +# Short usage message +has usage => <usage; + exit 0; + } + + foreach my $jsonfile(@args) { + my $j = decode_json(slurp($jsonfile)); + + my $raw_thread = $j->{threads}->[$j->{crashing_thread}->{threads_index}]; + my $thread = CrashTest::Model::Thread->new($raw_thread); + + my $sig_extract = CrashTest::Plugin::CrashSignatureExtractor::C_Cpp->new( + app => $self->app, + config => $self->app->config->{Processor}->{CrashSignatureExtractor}->{C_Cpp} + ); + say join "\n", @{$sig_extract->extract_signature($thread)}; + } + +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Controller/CrashGroups.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Controller/CrashGroups.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,70 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Controller::CrashGroups; +use Mojo::Base 'Mojolicious::Controller'; +use Mojo::Util qw/dumper/; + +sub index { + my $self = shift; + + my $page = 1; + my $crashs_per_page = 25; + + $self->validation->required('page')->like(qr/^[0-9]+$/); + $page = scalar $self->validation->param("page") if $self->validation->is_valid('page'); + + my ($results, $pager) = $self->crash_groups->index($page, $crashs_per_page, $self->req->param('search')); + + #$self->app->log->debug(dumper $results); + + $self->stash(crashs => $results); + $self->stash(pager => $pager); + $self->stash(extra_columns => []); + + $self->render("groups/index"); +} + +sub show { + my $self = shift; + + my $uuid = $self->param('uuid'); + + my $group = $self->app->crash_groups->get($uuid); + $self->stash(group => $group->{group}); + $self->stash(stats_by_product_and_version => $group->{stats_by_product_and_version}); + + my $page = 1; + my $crashs_per_page = 20; + $self->validation->required('page')->like(qr/^[0-9]+$/); + $page = scalar $self->validation->param("page") if $self->validation->is_valid('page'); + + my $search = $self->req->param('search'); + if(defined($search) && $search ne "") { + $search .= " AND " . "group_id=$group->{group}->{id}"; + } else { + $search = "group_id=$group->{group}->{id}"; + } + + my ($results, $pager) = $self->crash_reports->index($page, $crashs_per_page, $search); + + #$self->app->log->debug(dumper $results); + + $self->stash(crashs => $results); + $self->stash(pager => $pager); + $self->stash(extra_columns => $self->app->config->{WebInterface}->{ExtraColumns}->{Index}); + + $self->render("group/show"); +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Helper/Stats.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Helper/Stats.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,39 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Helper::Stats; +use Mojo::Base 'Mojolicious::Plugin'; +use Mojo::ByteStream qw/b/; + +sub register { + my ($self, $app, $conf) = @_; + + $app->helper(stats_bar => sub { $self->_stats_bar(@_) } ); +} + +sub _stats_bar { + my ($self, $c, $val, $max) = @_; + my $pct = sprintf("%.2f", $val / $max * 100.0); + + return b( +< +
+ $val +
+ +EOF +); +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Model/CrashGroup.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Model/CrashGroup.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,31 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Model::CrashGroup; +use Mojo::Base -base; + +has [ qw/app config/ ]; + +sub index { + my ($self, $page, $nperpage, $search_str) = @_; + + return $self->app->storage->first("CrashGroup::index", $page, $nperpage, $search_str); +} + +sub get { + my ($self, $uuid) = @_; + + return $self->app->storage->first("CrashGroup::get", $uuid); +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Plugin/CrashSignatureExtractor/C_Cpp.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Plugin/CrashSignatureExtractor/C_Cpp.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,115 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Plugin::CrashSignatureExtractor::C_Cpp; +use Mojo::Base -base; +use Mojo::Util qw/dumper/; +use Text::Balanced qw/extract_bracketed/; + +use CrashTest::Model::Thread; + +has [ qw/app config/ ]; + +sub extract_signature { + my ($self, $thread) = @_; + + my $frames = $thread->frames; + + $frames = _strip_top_frames($frames, $self->config->{TopIrrelevant}, $self->config->{TopFrame}); + + #my @rev_frames = reverse @$frames; + # $frames = _strip_top_frames(\@rev_frames, $self->config->{BottomIrrelevant}, $self->config->{BottomFrame}); + + #my @new_frames = reverse @$frames; + my @new_frames = @$frames; + + my @short_frames; + foreach my $frame(@new_frames) { + my $short = _extract_function_name($frame->function); + $short =~ s/^$_// foreach (@{$self->config->{RemoveNamespace}}); + push @short_frames, $short; + # . "@" . $frame->function_offset; + } + + return \@short_frames; +} + +sub extract_signature_title { + my ($self, $thread) = @_; + + return $self->extract_signature($thread)->[0]; +} + +sub _extract_function_name { + my $signature = shift; + + return "" if(!defined($signature) || $signature eq ""); + + my $short_signature = ""; + my $text = $signature; + do { + my ($str, $next, $prefix) = extract_bracketed($text, '<(', '[^<(]*'); + if($str) { + $short_signature .= $prefix; + $text = $next; + } else { + $short_signature .= $next; + $text = undef; + } + } while($text); + + return $short_signature; +} + +sub _smartmatch { + my ($text, $pat) = @_; + return $text =~ $pat if(ref($pat) eq "Regexp"); + return $text eq $pat; +} + +sub _strip_top_frames { + my ($frames, $skip, $stop) = @_; + + my $first_frame = 0; + my $i = -1; + TOPFRAME: foreach my $frame(@$frames) { + my $f = _extract_function_name($frame->function); + $i += 1; + + # if matching, mark first frame, and stop + foreach my $m(@{$stop}) { + if(_smartmatch($f, $m)) { + $first_frame = $i; + last TOPFRAME; + } + } + # if matching, mark next frame as first, and continue + my $skip_matched = 0; + foreach my $m(@{$skip}) { + if(_smartmatch($f, $m)) { + $first_frame = $i + 1; + $skip_matched = 1; + } + } + # else, stop + unless($skip_matched) { + last TOPFRAME; + } + } + + my $last = scalar(@$frames) - 1; + my @new_frames = @$frames[$first_frame .. $last]; + return \@new_frames; +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Plugin/Storage/Sql/Model/CrashGroup.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/CrashTest/Plugin/Storage/Sql/Model/CrashGroup.pm Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,194 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +package CrashTest::Plugin::Storage::Sql::Model::CrashGroup; +use Mojo::Base -base; +#use DBI::Log; +#$DBI::Log::trace = 0; + +use Storable 'dclone'; +use CrashTest::Model::Thread; +use CrashTest::Plugin::CrashSignatureExtractor::C_Cpp; + + +has [ qw/instance app config/ ]; + +has qw/db/; + +sub new { + my $self = shift->SUPER::new(@_); + + $self->db($self->instance->dbh->db); + + return $self; +} + +sub _build_query_from_search_string { + my ($self, $search) = @_; + + my @values; + + # define a callback to collect values safely (without magic markers) + my $cb = sub { + my ($column, $this_op, $value) = @_; + if($column->type eq "fuzzy") { + $this_op = " ILIKE "; + $value = "%$value%"; + } + push @values, $value; + return join('', $column->stringify, $this_op, '?'); + }; + + my $search_fields = { + user_id => { callback => $cb, name => 'crash_user.user_id' }, + product => { callback => $cb, name => 'product.name' }, + version => { callback => $cb, name => 'product.version' }, + channel => { callback => $cb, name => 'product.release_channel' }, + function => { callback => $cb, name => 'crash_group.crash_thread_signature_bt', type => "fuzzy" }, + }; + + my $parser = Search::QueryParser::SQL->new( + columns => $search_fields, + default_column => "function", + strict => 1, + ); + + my $query = $parser->parse($search, 1) + or die "Error in query: " . $parser->err; + + # reset before calling dbi + @values = (); + my $dbi = $query->dbi(); + + return [ $dbi->[0], \@values ]; +} + +sub index { + my ($self, $pagen, $nperpage, $search_str) = @_; + + my $where = ""; + my @values = (); + if(defined($search_str) && $search_str ne "") { + my $q = $self->_build_query_from_search_string($search_str); + $where = "WHERE " . $q->[0]; + @values = @{$q->[1]}; + } + + my $count = $self->db->query(" + SELECT count(distinct(crash_group_id)) AS total + FROM crash_reports + JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id + JOIN products AS product ON crash_reports.product_id = product.id + JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id + $where + ", @values)->hash; + + my $pager = Data::Page->new(); + $pager->total_entries($count->{total}); + $pager->entries_per_page($nperpage); + $pager->current_page($pagen); + + my $results = $self->db->query(" + SELECT crash_groups.uuid, title, group_by_count.* + FROM crash_groups, + ( SELECT crash_group_id AS id, + min(version) AS first_version, + max(version) AS last_version, + string_agg(distinct(name), ', ') AS product_names, + count(*) AS crash_count + FROM crash_reports + JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id + JOIN products AS product ON crash_reports.product_id = product.id + JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id + $where + GROUP BY crash_group_id ) AS group_by_count + WHERE group_by_count.id = crash_groups.id + ORDER BY crash_count DESC, crash_groups.id DESC + OFFSET (?) ROWS + FETCH NEXT (?) ROWS ONLY + ", + @values, $pager->skipped, $pager->entries_per_page + )->hashes; + + return ($results, $pager); +} + +sub get { + my ($self, $uuid) = @_; + + my $result = $self->db->query(" + SELECT * FROM crash_groups WHERE uuid = ? + ", + $uuid + )->hash; + + return { + group => $result, + stats_by_product_and_version => $self->_stats_by_product_and_version($result->{id}) + }; +} + +sub _stats_by_product_and_version { + my ($self, $crash_id) = @_; + my $results = $self->db->query(" + SELECT + name AS product_name, + version, + count(*) AS crash_count + FROM crash_reports + JOIN products ON product_id = products.id + WHERE crash_group_id = ? + GROUP BY product_name, version + ORDER BY crash_count DESC, product_name, version; + ", + $crash_id + )->hashes; + return $results; +} + + +sub find_or_create { + my ($self, $uuid, $pjson) = @_; + + my $max_dist = $self->app->config->{Processor}->{CrashSignatureExtractor}->{C_Cpp}->{GroupMaxDistance} || 0.1; + + my $sig_extract = CrashTest::Plugin::CrashSignatureExtractor::C_Cpp->new( + app => $self->app, + config => $self->app->config->{Processor}->{CrashSignatureExtractor}->{C_Cpp} + ); + + my $raw_thread = dclone $pjson->{threads}->[$pjson->{crashing_thread}->{threads_index}]; + my $thread = CrashTest::Model::Thread->new($raw_thread); + + my $sig = join "\n", @{$sig_extract->extract_signature($thread)}; + + my $crash_group; + if($sig ne "") { + $crash_group = $self->db->query( + "SELECT id, crash_thread_signature_bt <-> ? AS dist FROM crash_groups ORDER BY dist LIMIT 1", + $sig + )->hash; + + # race condition here ... + + if(!$crash_group || $crash_group->{dist} > $max_dist) { + $crash_group = $self->db->query( + "INSERT INTO crash_groups (uuid, crash_thread_signature_bt, title) VALUES (?, ?, ?) RETURNING id", + $uuid, $sig, $sig_extract->extract_signature_title($thread) + )->hash; + } + } + return $crash_group; +} + +1; diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm --- a/lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm Mon Dec 28 23:07:16 2015 +0100 +++ b/lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm Mon Dec 28 23:08:23 2015 +0100 @@ -20,14 +20,18 @@ #$DBI::Log::trace = 0; use Search::QueryParser::SQL; +use CrashTest::Plugin::Storage::Sql::Model::CrashGroup; + has [ qw/instance app config/ ]; has qw/db/; +has qw/crash_groups/; sub new { my $self = shift->SUPER::new(@_); $self->db($self->instance->dbh->db); + $self->crash_groups(CrashTest::Plugin::Storage::Sql::Model::CrashGroup->new(@_)); return $self; } @@ -40,7 +44,6 @@ # define a callback to collect values safely (without magic markers) my $cb = sub { my ($column, $this_op, $value) = @_; - say $column; if($column->type eq "fuzzy") { $this_op = " ILIKE "; $value = "%$value%"; @@ -50,11 +53,12 @@ }; my $search_fields = { - user_id => { callback => $cb, name => 'u.user_id' }, - product => { callback => $cb, name => 'p.name' }, - version => { callback => $cb, name => 'p.version' }, - channel => { callback => $cb, name => 'p.release_channel' }, - function => { callback => $cb, name => 'extract_crashing_functions(d.processed)', type => "fuzzy" }, + user_id => { callback => $cb, name => 'crash_user.user_id' }, + product => { callback => $cb, name => 'product.name' }, + version => { callback => $cb, name => 'product.version' }, + channel => { callback => $cb, name => 'product.release_channel' }, + group_id => { callback => $cb, name => 'crash_reports.crash_group_id' }, + function => { callback => $cb, name => 'crash_group.crash_thread_signature_bt', type => "fuzzy" }, }; my $parser = Search::QueryParser::SQL->new( @@ -68,8 +72,9 @@ # reset before calling dbi @values = (); + my $dbi = $query->dbi(); - return [ $query->dbi->[0], \@values ]; + return [ $dbi->[0], \@values ]; } sub index { @@ -85,9 +90,9 @@ my $count = $self->db->query(" SELECT count(crash_reports.id) AS total FROM crash_reports - JOIN crash_users AS u ON crash_reports.crash_user_id = u.id - JOIN products AS p ON crash_reports.product_id = p.id - JOIN crash_report_datas AS d ON crash_reports.id = d.crash_report_id + JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id + JOIN products AS product ON crash_reports.product_id = product.id + JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id $where ", @values)->hash; @@ -96,14 +101,21 @@ $pager->entries_per_page($nperpage); $pager->current_page($pagen); + my @extra_cols; + foreach my $extra_col(@{$self->app->config->{WebInterface}->{ExtraColumns}->{Index}}) { + push @extra_cols, $extra_col->{db_column} . " AS " . $extra_col->{id}; + } + my $extra_columns = join(",", @extra_cols); + my $results = $self->db->query(" SELECT crash_reports.*, - p.distributor AS p_distributor, p.name AS p_name, p.version AS p_version, p.release_channel AS p_release_channel, - u.os AS u_os, u.cpu_arch AS u_cpu_arch, u.cpu_count AS u_cpu_count, u.extra_info AS u_extra_info + product.distributor AS p_distributor, product.name AS p_name, product.version AS p_version, product.release_channel AS p_release_channel, + crash_user.os AS u_os, crash_user.cpu_arch AS u_cpu_arch, crash_user.cpu_count AS u_cpu_count, crash_user.extra_info AS u_extra_info, + $extra_columns FROM crash_reports - JOIN crash_users AS u ON crash_reports.crash_user_id = u.id - JOIN products AS p ON crash_reports.product_id = p.id - JOIN crash_report_datas AS d ON crash_reports.id = d.crash_report_id + JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id + JOIN products AS product ON crash_reports.product_id = product.id + JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id $where ORDER BY crash_time DESC OFFSET (?) ROWS @@ -144,6 +156,8 @@ $crash_time = DateTime->from_epoch(epoch => $client_info->{CrashTime}); } + chomp($client_info->{ProductName}); + chomp($client_info->{Version}) if $client_info->{Version}; my @product_values = ( $client_info->{Distributor}, $client_info->{ProductName}, @@ -176,6 +190,8 @@ )->hash; } + my $crash_group = $self->crash_groups->find_or_create($uuid, $pjson); + my $main_module; { my $i = $pjson->{main_module}; @@ -183,8 +199,11 @@ } my $dbcrash = $self->db->query( - "INSERT INTO crash_reports (start_time, crash_time, uuid, main_module, product_id, crash_user_id) VALUES (?, ?, ?, ?, ?, ?) RETURNING id", - $start_time, $crash_time, $uuid, $main_module, $dbproduct->{id}, $dbuser->{id} + "INSERT INTO crash_reports (start_time, crash_time, uuid, main_module, product_id, crash_user_id, crash_group_id, crash_group_distance) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", + $start_time, $crash_time, $uuid, $main_module, $dbproduct->{id}, $dbuser->{id}, + $crash_group ? $crash_group->{id} : undef, + $crash_group ? ($crash_group->{dist} || 0) : undef, )->hash; $self->db->query( diff -r 03caeafba2ff -r c82f5589db11 lib/CrashTest/Plugin/Storage/Sql/migrations_pg.sql --- a/lib/CrashTest/Plugin/Storage/Sql/migrations_pg.sql Mon Dec 28 23:07:16 2015 +0100 +++ b/lib/CrashTest/Plugin/Storage/Sql/migrations_pg.sql Mon Dec 28 23:08:23 2015 +0100 @@ -59,7 +59,7 @@ $$ LANGUAGE sql IMMUTABLE; --- This extension is the contrib modules +-- This extension is in contrib/ CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX crash_report_datas_idx_extract_crashing_functions ON crash_report_datas USING gist ( @@ -75,5 +75,35 @@ DROP TABLE crash_users CASCADE; DROP FUNCTION extract_crashing_functions (processed_json jsonb); +-- ########################################################################### +-- 2 up +-- ########################################################################### + +CREATE TABLE "crash_groups" ( + "id" serial NOT NULL, + "uuid" uuid NOT NULL, + "title" character varying NOT NULL, + "crash_thread_signature_bt" text NOT NULL, + CONSTRAINT "crash_groups_uuid_idx" UNIQUE ("uuid"), + PRIMARY KEY ("id") +); + +DROP INDEX crash_report_datas_idx_extract_crashing_functions; +CREATE INDEX crash_groups_idx_crash_thread_signature_bt ON crash_groups USING gist ( + crash_thread_signature_bt gist_trgm_ops +); + +ALTER TABLE "crash_reports" ADD COLUMN crash_group_id integer; +ALTER TABLE "crash_reports" ADD COLUMN crash_group_distance real; +ALTER TABLE "crash_reports" ADD CONSTRAINT "crash_reports_fk_crash_group_id" FOREIGN KEY ("crash_group_id") + REFERENCES "crash_groups" ("id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; + +-- ########################################################################### +-- 2 down +-- ########################################################################### + +ALTER TABLE "crash_reports" DROP COLUMN crash_group_id; +ALTER TABLE "crash_reports" DROP COLUMN crash_group_distance; +DROP TABLE "crash_groups" CASCADE; -- vim:ft=pgsql: diff -r 03caeafba2ff -r c82f5589db11 templates/group/_stats.html.ep --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/group/_stats.html.ep Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,26 @@ +

Crashs by product and version

+
+%= t table => (class => 'table table-striped table-hover table-bordered table-condensed') => begin + + + Product + Version + Count + + + +% my $max; +% foreach my $stat(@{$stats_by_product_and_version}) { + % $max = $stat->{crash_count} unless defined($max); + %= t tr => begin + %= t td => $stat->{product_name} + %= t td => $stat->{version} + %= t td => begin + %= stats_bar($stat->{crash_count}, $max) + % end + % end +% } + +% end +
+ diff -r 03caeafba2ff -r c82f5589db11 templates/group/show.html.ep --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/group/show.html.ep Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,23 @@ +% title "Crash for \"$group->{title}\""; +% layout 'main'; +

Crashs for <%= $group->{title} %>

+ +%= t div => (class => 'tabbable') => begin + %= t ul => (class => 'nav nav-tabs', id => 'report-tabs') => begin + %= t li => (class => 'active') => begin + %= t a => (href => '#stats', 'data-toggle' => 'tab') => 'Statistics' + % end + %= t li => begin + %= t a => (href => '#reports', 'data-toggle' => 'tab') => 'Reports' + % end + % end + + %= t div => (class => 'tab-content') => begin + %= t div => (class => 'tab-pane active', id => 'stats') => begin + %= include('group/_stats', stats_by_product_and_version => $stats_by_product_and_version); + % end + %= t div => (class => 'tab-pane', id => 'reports') => begin + %= include('reports/_list', crashs => $crashs, extra_columns => $extra_columns, pager => $pager); + % end + % end +% end diff -r 03caeafba2ff -r c82f5589db11 templates/groups/index.html.ep --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/groups/index.html.ep Mon Dec 28 23:08:23 2015 +0100 @@ -0,0 +1,28 @@ +% title 'Top crashs'; +% layout 'main'; +%= t table => (class => 'table table-striped table-hover table-bordered table-condensed') => begin + + + Title + Count + First version seen + Last version seen + Products + + +% foreach my $crash(@$crashs) { + %= t tr => begin + %= t td => begin + %= link_to $crash->{title} => url_for('group', uuid => $crash->{uuid}) + % end + %= t td => $crash->{crash_count} + %= t td => $crash->{first_version} + %= t td => $crash->{last_version} + %= t td => $crash->{product_names} + % end +% } +% end + +% if($pager->first_page != $pager->last_page) { + %= bootstrap_pagination($pager->current_page, $pager->last_page); +% }