--- 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');
}
--- /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 <http://www.gnu.org/licenses/>.
+
+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 => <<EOF;
+Usage: APPLICATION get_trace file ...
+EOF
+
+sub run {
+ my ($self, @args) = @_;
+
+ if(scalar @args < 1) {
+ say $self->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;
--- /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 <http://www.gnu.org/licenses/>.
+
+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;
--- /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 <http://www.gnu.org/licenses/>.
+
+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(
+<<EOF
+<div class="progress stats">
+ <div class="progress-bar" role="progressbar" aria-valuenow="$val" aria-valuemin="0" aria-valuemax="$max" style="min-width: 2em; width: $pct%;">
+ $val
+ </div>
+</div>
+EOF
+);
+}
+
+1;
--- /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 <http://www.gnu.org/licenses/>.
+
+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;
--- /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 <http://www.gnu.org/licenses/>.
+
+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;
--- /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 <http://www.gnu.org/licenses/>.
+
+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;
--- 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(
--- 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:
--- /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 @@
+<a data-toggle="collapse" href="#stats-by-product-and-version"><h3>Crashs by product and version</h3></a>
+<div id="stats-by-product-and-version" class="collapse in">
+%= t table => (class => 'table table-striped table-hover table-bordered table-condensed') => begin
+<thead>
+ <tr>
+ <th>Product</th>
+ <th>Version</th>
+ <th>Count</th>
+ </tr>
+</thead>
+<tbody>
+% 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
+% }
+</tbody>
+% end
+</div>
+
--- /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';
+<h2>Crashs for <%= $group->{title} %></h2>
+
+%= 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
--- /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
+<thead>
+ <tr>
+ <th>Title</th>
+ <th>Count</th>
+ <th>First version seen</th>
+ <th>Last version seen</th>
+ <th>Products</th>
+ </tr>
+</thead>
+% 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);
+% }