212 lines
6.3 KiB
D
212 lines
6.3 KiB
D
|
module report.gen;
|
||
|
|
||
|
import utils;
|
||
|
import std.stdio;
|
||
|
import std.datetime;
|
||
|
import std.algorithm;
|
||
|
import std.array;
|
||
|
import std.file;
|
||
|
import d2sqlite3;
|
||
|
import data;
|
||
|
import std.json;
|
||
|
|
||
|
import report.output;
|
||
|
|
||
|
int makeReport(string[] args) {
|
||
|
if (args.length == 1 && (args[0] == "-h" || args[0] == "--help")) {
|
||
|
printHelp();
|
||
|
return 0;
|
||
|
}
|
||
|
string[] sitesToReport = parseSites(args);
|
||
|
if (sitesToReport.length == 0) {
|
||
|
writeln("No sites to report.");
|
||
|
return 1;
|
||
|
}
|
||
|
ReportPeriod period = parseReportPeriod(args);
|
||
|
ReportOutputGenerator outputGen = parseReportFormat(args);
|
||
|
SiteReport[] reports;
|
||
|
foreach (site; sitesToReport) {
|
||
|
reports ~= generateReport(site, period);
|
||
|
}
|
||
|
outputGen.generate(reports);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void printHelp() {
|
||
|
writeln(q"TXT
|
||
|
sitestat report help information:
|
||
|
|
||
|
The "report" command is used to generate aggregate reports for one or more
|
||
|
sites tracked by sitestat, over a given time period. The following arguments
|
||
|
are accepted:
|
||
|
|
||
|
-s <sitename> | --site <sitename>
|
||
|
Specify the name of a site to include in the report. If no sites are
|
||
|
specified, then all tracked sites will be included in reports.
|
||
|
|
||
|
--start <timestamp>
|
||
|
Set the starting timestamp of the reporting period, using an ISO-8601
|
||
|
formatted date and time. For example, 2023-11-05T12:00:00. Timestamps are
|
||
|
always interpretted as UTC timezone. Defaults to 1970-01-01.
|
||
|
|
||
|
--end <timestamp>
|
||
|
Set the end timestamp of the reporting period, using an ISO-8601 formatted
|
||
|
date and time. Timestamps are always interpretted as UTC timezone. Defaults
|
||
|
to the current time.
|
||
|
|
||
|
-f <format> | --format <format>
|
||
|
Specify the desired output format for the report(s) generated by this
|
||
|
command. The following formats are available: text, json, csv.
|
||
|
Defaults to the "text" format if none is specified.
|
||
|
TXT");
|
||
|
}
|
||
|
|
||
|
string[] parseSites(string[] args) {
|
||
|
string[] sites;
|
||
|
for (size_t i = 0; i < args.length; i++) {
|
||
|
if (i + 1 < args.length) {
|
||
|
if (args[i] == "-s" || args[i] == "--site") {
|
||
|
string site = args[i + 1];
|
||
|
if (!std.file.exists(dbPath(site))) {
|
||
|
throw new Exception("site " ~ site ~ " doesn't exist.");
|
||
|
}
|
||
|
sites ~= site;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (sites.length == 0) return listAllOrigins();
|
||
|
return sites;
|
||
|
}
|
||
|
|
||
|
ReportPeriod parseReportPeriod(string[] args) {
|
||
|
SysTime start = SysTime(Date(1970, 1, 1), UTC());
|
||
|
SysTime end = Clock.currTime(UTC());
|
||
|
|
||
|
/// Helper function to parse a timestamp from a string.
|
||
|
SysTime tryParseTime(string s) {
|
||
|
try {
|
||
|
return SysTime.fromISOExtString(s, UTC());
|
||
|
} catch (DateTimeException e) {
|
||
|
throw new Exception(
|
||
|
"Failed to parse timestamp. Expected an ISO-8601 formatted datetime string: YYYY-MM-DDTHH:MM:SS",
|
||
|
e
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool startParsed = false;
|
||
|
bool endParsed = false;
|
||
|
for (size_t i = 0; i < args.length; i++) {
|
||
|
if (i + 1 < args.length) {
|
||
|
if (args[i] == "--start" && !startParsed) {
|
||
|
start = tryParseTime(args[i + 1]);
|
||
|
startParsed = true;
|
||
|
} else if (args[i] == "--end" && !endParsed) {
|
||
|
end = tryParseTime(args[i + 1]);
|
||
|
endParsed = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ReportPeriod(start, end);
|
||
|
}
|
||
|
|
||
|
string createSiteNamesSummary(string[] sites) {
|
||
|
if (sites.length == 0) return "all sites";
|
||
|
if (sites.length == 1) return "site " ~ sites[0];
|
||
|
import std.array;
|
||
|
auto app = appender!string();
|
||
|
app ~= "sites ";
|
||
|
foreach (i, site; sites) {
|
||
|
app ~= site;
|
||
|
if (i + 2 == sites.length) {
|
||
|
app ~= ", and ";
|
||
|
} else if (i + 1 < sites.length) {
|
||
|
app ~= ", ";
|
||
|
}
|
||
|
}
|
||
|
return app[];
|
||
|
}
|
||
|
|
||
|
ReportOutputGenerator parseReportFormat(string[] args) {
|
||
|
for (size_t i = 0; i < args.length; i++) {
|
||
|
if (i + 1 < args.length) {
|
||
|
if (args[i] == "-f" || args[i] == "--format") {
|
||
|
import std.uni : toLower;
|
||
|
import std.string : strip;
|
||
|
string formatStr = toLower(strip(args[i + 1]));
|
||
|
if (formatStr == "text") return new ReportTextOutputGenerator();
|
||
|
if (formatStr == "json") return new ReportJsonOutputGenerator();
|
||
|
if (formatStr == "csv") return new ReportCsvOutputGenerator();
|
||
|
throw new Exception("Invalid report output format: " ~ args[i + 1]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return new ReportTextOutputGenerator();
|
||
|
}
|
||
|
|
||
|
immutable struct ReportPeriod {
|
||
|
SysTime start;
|
||
|
SysTime end;
|
||
|
}
|
||
|
|
||
|
struct SiteReport {
|
||
|
string siteName;
|
||
|
ReportPeriod period;
|
||
|
|
||
|
ulong totalSessions;
|
||
|
double meanSessionDurationSeconds;
|
||
|
double meanEventsPerSession;
|
||
|
ulong[string] userAgents;
|
||
|
|
||
|
this(string siteName, ReportPeriod period) {
|
||
|
this.siteName = siteName;
|
||
|
this.period = period;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SiteReport generateReport(string site, ReportPeriod period) {
|
||
|
SiteReport report = SiteReport(site, period);
|
||
|
immutable string TS_START = formatSqliteTimestamp(period.start);
|
||
|
immutable string TS_END = formatSqliteTimestamp(period.end);
|
||
|
Database db = getOrCreateDatabase(site);
|
||
|
|
||
|
report.totalSessions = db.execute(q"SQL
|
||
|
SELECT COUNT(id)
|
||
|
FROM session
|
||
|
WHERE start_timestamp >= ? AND end_timestamp <= ?
|
||
|
SQL",
|
||
|
TS_START, TS_END
|
||
|
).oneValue!ulong();
|
||
|
|
||
|
report.meanEventsPerSession = db.execute(q"SQL
|
||
|
SELECT AVG(event_count)
|
||
|
FROM session
|
||
|
WHERE start_timestamp >= ? AND end_timestamp <= ?
|
||
|
SQL",
|
||
|
TS_START, TS_END
|
||
|
).oneValue!double();
|
||
|
|
||
|
report.meanSessionDurationSeconds = db.execute(q"SQL
|
||
|
SELECT AVG((julianday(end_timestamp) - julianday(start_timestamp)) * 24 * 60 * 60)
|
||
|
FROM session
|
||
|
WHERE start_timestamp >= ? AND end_timestamp <= ?
|
||
|
SQL",
|
||
|
TS_START, TS_END
|
||
|
).oneValue!double();
|
||
|
|
||
|
ResultRange userAgentsResult = db.execute(q"SQL
|
||
|
SELECT user_agent, COUNT(user_agent) AS c
|
||
|
FROM session
|
||
|
GROUP BY user_agent
|
||
|
ORDER BY c DESC
|
||
|
SQL");
|
||
|
foreach (Row row; userAgentsResult) {
|
||
|
string userAgent = row["user_agent"].as!string;
|
||
|
ulong count = row["c"].as!ulong;
|
||
|
report.userAgents[userAgent] = count;
|
||
|
}
|
||
|
|
||
|
return report;
|
||
|
}
|