FIX: makes dashboard more resilient to errors (#6217)

This commit is an attempt to limit cases where the dashboard will generate a full exception page and also make it easier to track the error.
This commit is contained in:
Joffrey JAFFEUX 2018-07-31 21:23:28 -04:00 committed by GitHub
parent 7d8286e7ad
commit 2b2a506a7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 135 additions and 76 deletions

View File

@ -110,7 +110,9 @@ export default Ember.Component.extend({
unregisterTooltip($(".info[data-tooltip]")); unregisterTooltip($(".info[data-tooltip]"));
}, },
showTimeoutError: Ember.computed.alias("model.timeout"), showError: Ember.computed.or("showTimeoutError", "showExceptionError"),
showTimeoutError: Ember.computed.equal("model.error", "timeout"),
showExceptionError: Ember.computed.equal("model.error", "exception"),
hasData: Ember.computed.notEmpty("model.data"), hasData: Ember.computed.notEmpty("model.data"),

View File

@ -66,7 +66,9 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
this.setProperties({ this.setProperties({
dashboardFetchedAt: new Date(), dashboardFetchedAt: new Date(),
model: adminDashboardNextModel, model: adminDashboardNextModel,
reports: adminDashboardNextModel.reports.map(x => Report.create(x)) reports: Ember.makeArray(adminDashboardNextModel.reports).map(x =>
Report.create(x)
)
}); });
}) })
.catch(e => { .catch(e => {

View File

@ -12,7 +12,7 @@ import { renderAvatar } from "discourse/helpers/user-avatar";
// Change this line each time report format change // Change this line each time report format change
// and you want to ensure cache is reset // and you want to ensure cache is reset
export const SCHEMA_VERSION = 1; export const SCHEMA_VERSION = 2;
const Report = Discourse.Model.extend({ const Report = Discourse.Model.extend({
average: false, average: false,

View File

@ -1,11 +1,5 @@
{{#if isEnabled}} {{#if isEnabled}}
{{#conditional-loading-section isLoading=isLoading}} {{#conditional-loading-section isLoading=isLoading}}
{{#if showTimeoutError}}
<div class="alert alert-error">
{{i18n "admin.dashboard.timeout_error"}}
</div>
{{/if}}
{{#if showHeader}} {{#if showHeader}}
<div class="report-header"> <div class="report-header">
{{#if showTitle}} {{#if showTitle}}
@ -66,17 +60,29 @@
{{/if}} {{/if}}
<div class="report-body"> <div class="report-body">
{{#unless showTimeoutError}} {{#unless showError}}
{{#if hasData}} {{#if hasData}}
{{#if currentMode}} {{#if currentMode}}
{{component modeComponent model=model options=options}} {{component modeComponent model=model options=options}}
{{/if}} {{/if}}
{{else}} {{else}}
<div class="alert alert-info no-data-alert"> <div class="alert alert-info no-data">
{{d-icon "pie-chart"}} {{d-icon "pie-chart"}}
{{i18n 'admin.dashboard.reports.no_data'}} {{i18n 'admin.dashboard.reports.no_data'}}
</div> </div>
{{/if}} {{/if}}
{{else}}
{{#if showTimeoutError}}
<div class="alert alert-error report-error timeout">
{{i18n "admin.dashboard.timeout_error"}}
</div>
{{/if}}
{{#if showExceptionError}}
<div class="alert alert-error report-error exception">
{{i18n "admin.dashboard.exception_error"}}
</div>
{{/if}}
{{/unless}} {{/unless}}
{{#if showFilteringUI}} {{#if showFilteringUI}}

View File

@ -1,12 +1,22 @@
.admin-report { .admin-report {
.no-data-alert { .report-error,
background: $secondary; .no-data {
border: 1px solid $primary-low; width: 100%;
color: $primary-low-mid;
width: 100%; width: 100%;
align-self: flex-start; align-self: flex-start;
text-align: center; text-align: center;
padding: 3em; padding: 3em;
}
.report-error {
color: $danger;
border: 1px solid $danger;
}
.no-data {
background: $secondary;
border: 1px solid $primary-low;
color: $primary-low-mid;
.d-icon-pie-chart { .d-icon-pie-chart {
color: currentColor; color: currentColor;

View File

@ -5,10 +5,6 @@ class AdminDashboardNextData
@opts = opts @opts = opts
end end
def self.fetch_stats
self.class.new.as_json
end
def self.fetch_stats def self.fetch_stats
new.as_json new.as_json
end end

View File

@ -3,14 +3,14 @@ require_dependency 'topic_subtype'
class Report class Report
# Change this line each time report format change # Change this line each time report format change
# and you want to ensure cache is reset # and you want to ensure cache is reset
SCHEMA_VERSION = 1 SCHEMA_VERSION = 2
attr_accessor :type, :data, :total, :prev30Days, :start_date, attr_accessor :type, :data, :total, :prev30Days, :start_date,
:end_date, :category_id, :group_id, :labels, :async, :end_date, :category_id, :group_id, :labels, :async,
:prev_period, :facets, :limit, :processing, :average, :percent, :prev_period, :facets, :limit, :processing, :average, :percent,
:higher_is_better, :icon, :modes, :category_filtering, :higher_is_better, :icon, :modes, :category_filtering,
:group_filtering, :prev_data, :prev_start_date, :prev_end_date, :group_filtering, :prev_data, :prev_start_date, :prev_end_date,
:dates_filtering, :timeout :dates_filtering, :error
def self.default_days def self.default_days
30 30
@ -113,7 +113,7 @@ class Report
group_filtering: self.group_filtering, group_filtering: self.group_filtering,
modes: self.modes, modes: self.modes,
}.tap do |json| }.tap do |json|
json[:timeout] = self.timeout if self.timeout json[:error] = self.error if self.error
json[:total] = self.total if self.total json[:total] = self.total if self.total
json[:prev_period] = self.prev_period if self.prev_period json[:prev_period] = self.prev_period if self.prev_period
json[:prev30Days] = self.prev30Days if self.prev30Days json[:prev30Days] = self.prev30Days if self.prev30Days
@ -160,6 +160,7 @@ class Report
def self.find(type, opts = nil) def self.find(type, opts = nil)
clear_cache clear_cache
begin
report = _get(type, opts) report = _get(type, opts)
report_method = :"report_#{type}" report_method = :"report_#{type}"
@ -170,6 +171,14 @@ class Report
else else
return nil return nil
end end
rescue Exception => e
report.error = :exception
# given reports can be added by plugins we dont want dashboard failures
# on report computation, however we do want to log which report is provoking
# an error
Rails.logger.error("Error while computing report `#{report.type}`: #{e.message}")
end
report report
end end
@ -596,7 +605,7 @@ class Report
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 } options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
result = nil result = nil
report.timeout = wrap_slow_query do report.error = wrap_slow_query do
result = IncomingLinksReport.find(:top_referred_topics, options) result = IncomingLinksReport.find(:top_referred_topics, options)
report.data = result.data report.data = result.data
end end
@ -622,7 +631,7 @@ class Report
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 } options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
result = nil result = nil
report.timeout = wrap_slow_query do report.error = wrap_slow_query do
result = IncomingLinksReport.find(:top_traffic_sources, options) result = IncomingLinksReport.find(:top_traffic_sources, options)
report.data = result.data report.data = result.data
end end

View File

@ -2792,6 +2792,7 @@ en:
moderation_tab: "Moderation" moderation_tab: "Moderation"
disabled: Disabled disabled: Disabled
timeout_error: Sorry, query is taking too long, please pick a shorter interval timeout_error: Sorry, query is taking too long, please pick a shorter interval
exception_error: Sorry, an error occurred while executing the query
reports: reports:
trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period." trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period."

View File

@ -475,6 +475,8 @@ describe Report do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
before do before do
freeze_time
PostAction.act(flagger, post, PostActionType.types[:spam], message: 'bad') PostAction.act(flagger, post, PostActionType.types[:spam], message: 'bad')
end end
@ -507,6 +509,8 @@ describe Report do
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
before do before do
freeze_time
post.revise(editor, raw: 'updated body', edit_reason: 'not cool') post.revise(editor, raw: 'updated body', edit_reason: 'not cool')
end end
@ -674,4 +678,38 @@ describe Report do
end end
end end
end end
describe "exception report" do
before(:each) do
class Report
def self.report_exception_test(report)
report.data = x
end
end
end
it "returns a report with an exception error" do
report = Report.find("exception_test")
expect(report.error).to eq(:exception)
end
end
describe "timeout report" do
before(:each) do
freeze_time
class Report
def self.report_timeout_test(report)
report.error = wrap_slow_query(1) do
ActiveRecord::Base.connection.execute("SELECT pg_sleep(5)")
end
end
end
end
it "returns a report with a timeout error" do
report = Report.find("timeout_test")
expect(report.error).to eq(:timeout)
end
end
end end

View File

@ -112,7 +112,7 @@ componentTest("timeout", {
template: "{{admin-report dataSourceName='signups_timeout'}}", template: "{{admin-report dataSourceName='signups_timeout'}}",
test(assert) { test(assert) {
assert.ok(exists(".alert-error"), "it displays a timeout error"); assert.ok(exists(".alert-error.timeout"), "it displays a timeout error");
} }
}); });
@ -120,6 +120,14 @@ componentTest("no data", {
template: "{{admin-report dataSourceName='posts'}}", template: "{{admin-report dataSourceName='posts'}}",
test(assert) { test(assert) {
assert.ok(exists(".no-data-alert"), "it displays a no data alert"); assert.ok(exists(".no-data"), "it displays a no data alert");
}
});
componentTest("exception", {
template: "{{admin-report dataSourceName='signups_exception'}}",
test(assert) {
assert.ok(exists(".alert-error.exception"), "it displays an error");
} }
}); });

View File

@ -0,0 +1,11 @@
import signups from "fixtures/signups";
const signupsExceptionKey = "/admin/reports/signups_exception";
const signupsKey = "/admin/reports/signups";
let fixture = {};
fixture[signupsExceptionKey] = JSON.parse(JSON.stringify(signups[signupsKey]));
fixture[signupsExceptionKey].report.error = "exception";
export default fixture;

View File

@ -1,35 +1,11 @@
export default { import signups from "fixtures/signups";
"/admin/reports/signups_timeout": {
report: { const signupsTimeoutKey = "/admin/reports/signups_timeout";
type: "signups", const signupsKey = "/admin/reports/signups";
title: "Signups",
xaxis: "Day", let fixture = {};
yaxis: "Number of signups",
description: "New account registrations for this period", fixture[signupsTimeoutKey] = JSON.parse(JSON.stringify(signups[signupsKey]));
data: null, fixture[signupsTimeoutKey].report.error = "timeout";
start_date: "2018-06-16T00:00:00Z",
end_date: "2018-07-16T23:59:59Z", export default fixture;
prev_data: null,
prev_start_date: "2018-05-17T00:00:00Z",
prev_end_date: "2018-06-17T00:00:00Z",
category_id: null,
group_id: null,
prev30Days: null,
dates_filtering: true,
report_key: "reports:signups_timeout::20180616:20180716::[:prev_period]:",
labels: [
{ type: "date", properties: ["x"], title: "Day" },
{ type: "number", properties: ["y"], title: "Count" }
],
processing: false,
average: false,
percent: false,
higher_is_better: true,
category_filtering: false,
group_filtering: true,
modes: ["table", "chart"],
prev_period: 961,
timeout: true
}
}
};