mirror of
https://github.com/discourse/discourse.git
synced 2024-12-04 08:53:49 +08:00
14b436923c
### UI changes
All of the UI changes described are gated behind the `use_legacy_pageviews`
site setting.
This commit changes the admin dashboard pageviews report to
use the "Consolidated Pageviews with Browser Detection" report
introduced in 2f2da72747
with
the following changes:
* The report name is changed to "Site traffic"
* The pageview count on the dashboard is counting only using the new method
* The old "Consolidated Pageviews" report is renamed as "Consolidated Legacy Pageviews"
* By default "known crawlers" and "other" sources of pageviews are hidden on the report
When `use_legacy_pageviews` is `true`, we do not show or allow running
the "Site traffic" report for admins. When `use_legacy_pageviews` is `false`,
we do not show or allow running the following legacy reports:
* consolidated_page_views
* consolidated_page_views_browser_detection
* page_view_anon_reqs
* page_view_logged_in_reqs
### Historical data changes
Also part of this change is that, since we introduced our new "Consolidated
Pageviews with Browser Detection" report, some admins are confused at either:
* The lack of data before a certain date , which didn’t exist before
we started collecting it
* Comparing this and the current "Consolidated Pageviews" report data,
which rolls up "Other Pageviews" into "Anonymous Browser" and so it
appears inaccurate
All pageview data in the new report before the date where the _first_
anon or logged in browser pageview was recorded is now hidden.
429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
import Component from "@ember/component";
|
|
import EmberObject, { action, computed } from "@ember/object";
|
|
import { alias, and, equal, notEmpty, or } from "@ember/object/computed";
|
|
import { next } from "@ember/runloop";
|
|
import { isPresent } from "@ember/utils";
|
|
import { classNameBindings, classNames } from "@ember-decorators/component";
|
|
import { exportEntity } from "discourse/lib/export-csv";
|
|
import { outputExportResult } from "discourse/lib/export-result";
|
|
import ReportLoader from "discourse/lib/reports-loader";
|
|
import { isTesting } from "discourse-common/config/environment";
|
|
import { makeArray } from "discourse-common/lib/helpers";
|
|
import discourseComputed from "discourse-common/utils/decorators";
|
|
import I18n from "discourse-i18n";
|
|
import Report, { DAILY_LIMIT_DAYS, SCHEMA_VERSION } from "admin/models/report";
|
|
|
|
const TABLE_OPTIONS = {
|
|
perPage: 8,
|
|
total: true,
|
|
limit: 20,
|
|
formatNumbers: true,
|
|
};
|
|
|
|
const CHART_OPTIONS = {};
|
|
|
|
@classNameBindings(
|
|
"isHidden:hidden",
|
|
"isHidden::is-visible",
|
|
"isEnabled",
|
|
"isLoading",
|
|
"dasherizedDataSourceName"
|
|
)
|
|
@classNames("admin-report")
|
|
export default class AdminReport extends Component {
|
|
isEnabled = true;
|
|
disabledLabel = I18n.t("admin.dashboard.disabled");
|
|
isLoading = false;
|
|
rateLimitationString = null;
|
|
dataSourceName = null;
|
|
report = null;
|
|
model = null;
|
|
reportOptions = null;
|
|
forcedModes = null;
|
|
showAllReportsLink = false;
|
|
filters = null;
|
|
showTrend = false;
|
|
showHeader = true;
|
|
showTitle = true;
|
|
showFilteringUI = false;
|
|
|
|
@alias("model.dates_filtering") showDatesOptions;
|
|
|
|
@or("showDatesOptions", "model.available_filters.length") showRefresh;
|
|
|
|
@and("showTrend", "model.prev_period") shouldDisplayTrend;
|
|
|
|
endDate = null;
|
|
startDate = null;
|
|
|
|
@or("showTimeoutError", "showExceptionError", "showNotFoundError") showError;
|
|
@equal("model.error", "not_found") showNotFoundError;
|
|
@equal("model.error", "timeout") showTimeoutError;
|
|
@equal("model.error", "exception") showExceptionError;
|
|
@notEmpty("model.data") hasData;
|
|
|
|
_reports = [];
|
|
|
|
@computed("siteSettings.dashboard_hidden_reports")
|
|
get isHidden() {
|
|
return (this.siteSettings.dashboard_hidden_reports || "")
|
|
.split("|")
|
|
.filter(Boolean)
|
|
.includes(this.dataSourceName);
|
|
}
|
|
|
|
didReceiveAttrs() {
|
|
super.didReceiveAttrs(...arguments);
|
|
|
|
let startDate = moment();
|
|
if (this.filters && isPresent(this.filters.startDate)) {
|
|
startDate = moment(this.filters.startDate, "YYYY-MM-DD");
|
|
}
|
|
this.set("startDate", startDate);
|
|
|
|
let endDate = moment();
|
|
if (this.filters && isPresent(this.filters.endDate)) {
|
|
endDate = moment(this.filters.endDate, "YYYY-MM-DD");
|
|
}
|
|
this.set("endDate", endDate);
|
|
|
|
if (this.filters) {
|
|
this.set("currentMode", this.filters.mode);
|
|
}
|
|
|
|
if (this.report) {
|
|
this._renderReport(this.report, this.forcedModes, this.currentMode);
|
|
} else if (this.dataSourceName) {
|
|
this._fetchReport();
|
|
}
|
|
}
|
|
|
|
@discourseComputed("dataSourceName", "model.type")
|
|
dasherizedDataSourceName(dataSourceName, type) {
|
|
return (dataSourceName || type || "undefined").replace(/_/g, "-");
|
|
}
|
|
|
|
@discourseComputed("dataSourceName", "model.type")
|
|
dataSource(dataSourceName, type) {
|
|
dataSourceName = dataSourceName || type;
|
|
return `/admin/reports/${dataSourceName}`;
|
|
}
|
|
|
|
@discourseComputed("displayedModes.length")
|
|
showModes(displayedModesLength) {
|
|
return displayedModesLength > 1;
|
|
}
|
|
|
|
@discourseComputed("currentMode")
|
|
isChartMode(currentMode) {
|
|
return currentMode === "chart";
|
|
}
|
|
|
|
@action
|
|
changeGrouping(grouping) {
|
|
this.send("refreshReport", {
|
|
chartGrouping: grouping,
|
|
});
|
|
}
|
|
|
|
@discourseComputed("currentMode", "model.modes", "forcedModes")
|
|
displayedModes(currentMode, reportModes, forcedModes) {
|
|
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
|
|
|
return makeArray(modes).map((mode) => {
|
|
const base = `btn-default mode-btn ${mode}`;
|
|
const cssClass = currentMode === mode ? `${base} btn-primary` : base;
|
|
|
|
return {
|
|
mode,
|
|
cssClass,
|
|
icon: mode === "table" ? "table" : "signal",
|
|
};
|
|
});
|
|
}
|
|
|
|
@discourseComputed("currentMode")
|
|
modeComponent(currentMode) {
|
|
return `admin-report-${currentMode.replace(/_/g, "-")}`;
|
|
}
|
|
|
|
@discourseComputed(
|
|
"dataSourceName",
|
|
"startDate",
|
|
"endDate",
|
|
"filters.customFilters"
|
|
)
|
|
reportKey(dataSourceName, startDate, endDate, customFilters) {
|
|
if (!dataSourceName || !startDate || !endDate) {
|
|
return null;
|
|
}
|
|
|
|
startDate = startDate.toISOString(true).split("T")[0];
|
|
endDate = endDate.toISOString(true).split("T")[0];
|
|
|
|
let reportKey = "reports:";
|
|
reportKey += [
|
|
dataSourceName,
|
|
isTesting() ? "start" : startDate.replace(/-/g, ""),
|
|
isTesting() ? "end" : endDate.replace(/-/g, ""),
|
|
"[:prev_period]",
|
|
this.get("reportOptions.table.limit"),
|
|
// Convert all filter values to strings to ensure unique serialization
|
|
customFilters
|
|
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
|
|
: null,
|
|
SCHEMA_VERSION,
|
|
]
|
|
.filter((x) => x)
|
|
.map((x) => x.toString())
|
|
.join(":");
|
|
|
|
return reportKey;
|
|
}
|
|
|
|
@discourseComputed("options.chartGrouping", "model.chartData.length")
|
|
chartGroupings(grouping, count) {
|
|
const options = ["daily", "weekly", "monthly"];
|
|
|
|
return options.map((id) => {
|
|
return {
|
|
id,
|
|
disabled: id === "daily" && count >= DAILY_LIMIT_DAYS,
|
|
label: `admin.dashboard.reports.${id}`,
|
|
class: `chart-grouping ${grouping === id ? "active" : "inactive"}`,
|
|
};
|
|
});
|
|
}
|
|
|
|
@action
|
|
onChangeDateRange(range) {
|
|
this.setProperties({
|
|
startDate: range.from,
|
|
endDate: range.to,
|
|
});
|
|
}
|
|
|
|
@action
|
|
applyFilter(id, value) {
|
|
let customFilters = this.get("filters.customFilters") || {};
|
|
|
|
if (typeof value === "undefined") {
|
|
delete customFilters[id];
|
|
} else {
|
|
customFilters[id] = value;
|
|
}
|
|
|
|
this.send("refreshReport", {
|
|
filters: customFilters,
|
|
});
|
|
}
|
|
|
|
@action
|
|
refreshReport(options = {}) {
|
|
if (!this.onRefresh) {
|
|
return;
|
|
}
|
|
|
|
this.onRefresh({
|
|
type: this.get("model.type"),
|
|
mode: this.currentMode,
|
|
chartGrouping: options.chartGrouping,
|
|
startDate:
|
|
typeof options.startDate === "undefined"
|
|
? this.startDate
|
|
: options.startDate,
|
|
endDate:
|
|
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
|
|
filters:
|
|
typeof options.filters === "undefined"
|
|
? this.get("filters.customFilters")
|
|
: options.filters,
|
|
});
|
|
}
|
|
|
|
@action
|
|
exportCsv() {
|
|
const args = {
|
|
name: this.get("model.type"),
|
|
start_date: this.startDate.toISOString(true).split("T")[0],
|
|
end_date: this.endDate.toISOString(true).split("T")[0],
|
|
};
|
|
|
|
const customFilters = this.get("filters.customFilters");
|
|
if (customFilters) {
|
|
Object.assign(args, customFilters);
|
|
}
|
|
|
|
exportEntity("report", args).then(outputExportResult);
|
|
}
|
|
|
|
@action
|
|
onChangeMode(mode) {
|
|
this.set("currentMode", mode);
|
|
|
|
this.send("refreshReport", {
|
|
chartGrouping: null,
|
|
});
|
|
}
|
|
|
|
_computeReport() {
|
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
return;
|
|
}
|
|
|
|
if (!this._reports || !this._reports.length) {
|
|
return;
|
|
}
|
|
|
|
// on a slow network _fetchReport could be called multiple times between
|
|
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
|
// to avoid any inconsistencies we filter by period and make sure
|
|
// the array contains only unique values
|
|
let filteredReports = this._reports.uniqBy("report_key");
|
|
let report;
|
|
|
|
const sort = (r) => {
|
|
if (r.length > 1) {
|
|
return r.findBy("type", this.dataSourceName);
|
|
} else {
|
|
return r;
|
|
}
|
|
};
|
|
|
|
if (!this.startDate || !this.endDate) {
|
|
report = sort(filteredReports)[0];
|
|
} else {
|
|
report = sort(
|
|
filteredReports.filter((r) => r.report_key.includes(this.reportKey))
|
|
)[0];
|
|
|
|
if (!report) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (report.error === "not_found") {
|
|
this.set("showFilteringUI", false);
|
|
}
|
|
|
|
this._renderReport(report, this.forcedModes, this.currentMode);
|
|
}
|
|
|
|
_renderReport(report, forcedModes, currentMode) {
|
|
const modes = forcedModes ? forcedModes.split(",") : report.modes;
|
|
currentMode = currentMode || (modes ? modes[0] : null);
|
|
|
|
this.setProperties({
|
|
model: report,
|
|
currentMode,
|
|
options: this._buildOptions(currentMode, report),
|
|
});
|
|
}
|
|
|
|
_fetchReport() {
|
|
this.setProperties({ isLoading: true, rateLimitationString: null });
|
|
|
|
next(() => {
|
|
let payload = this._buildPayload(["prev_period"]);
|
|
|
|
const callback = (response) => {
|
|
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
return;
|
|
}
|
|
|
|
this.set("isLoading", false);
|
|
|
|
if (response === 429) {
|
|
this.set(
|
|
"rateLimitationString",
|
|
I18n.t("admin.dashboard.too_many_requests")
|
|
);
|
|
} else if (response === 500) {
|
|
this.set("model.error", "exception");
|
|
} else if (response) {
|
|
this._reports.push(this._loadReport(response));
|
|
this._computeReport();
|
|
}
|
|
};
|
|
|
|
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
|
|
});
|
|
}
|
|
|
|
_buildPayload(facets) {
|
|
let payload = { data: { facets } };
|
|
|
|
if (this.startDate) {
|
|
payload.data.start_date = moment(this.startDate)
|
|
.toISOString(true)
|
|
.split("T")[0];
|
|
}
|
|
|
|
if (this.endDate) {
|
|
payload.data.end_date = moment(this.endDate)
|
|
.toISOString(true)
|
|
.split("T")[0];
|
|
}
|
|
|
|
if (this.get("reportOptions.table.limit")) {
|
|
payload.data.limit = this.get("reportOptions.table.limit");
|
|
}
|
|
|
|
if (this.get("filters.customFilters")) {
|
|
payload.data.filters = this.get("filters.customFilters");
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
_buildOptions(mode, report) {
|
|
if (mode === "table") {
|
|
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
|
|
return EmberObject.create(
|
|
Object.assign(tableOptions, this.get("reportOptions.table") || {})
|
|
);
|
|
} else if (mode === "chart") {
|
|
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
|
return EmberObject.create(
|
|
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
|
|
chartGrouping:
|
|
this.get("reportOptions.chartGrouping") ||
|
|
Report.groupingForDatapoints(report.chartData.length),
|
|
})
|
|
);
|
|
} else if (mode === "stacked-chart") {
|
|
return this.get("reportOptions.stackedChart") || {};
|
|
}
|
|
}
|
|
|
|
_loadReport(jsonReport) {
|
|
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
|
|
|
|
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
|
|
jsonReport.chartData = jsonReport.chartData.map((chartData) => {
|
|
if (chartData.length > 40) {
|
|
return {
|
|
data: chartData.data,
|
|
req: chartData.req,
|
|
label: chartData.label,
|
|
color: chartData.color,
|
|
};
|
|
} else {
|
|
return chartData;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (jsonReport.prev_data) {
|
|
Report.fillMissingDates(jsonReport, {
|
|
filledField: "prevChartData",
|
|
dataField: "prev_data",
|
|
starDate: jsonReport.prev_startDate,
|
|
endDate: jsonReport.prev_endDate,
|
|
});
|
|
}
|
|
|
|
return Report.create(jsonReport);
|
|
}
|
|
}
|