module report.gen; import utils; import std.stdio; import std.datetime; import std.algorithm; import std.array; import std.file; import std.conv : to; 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 | --site 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 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 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. --last-days A convenience to show stats from the last N days. If provided, overrides any --start or --end timestamps given. -f | --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; } else if (args[i] == "--last-days") { end = Clock.currTime(UTC()); start = end - days(args[i + 1].to!uint); return ReportPeriod(start, end); } } } 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 WHERE start_timestamp >= ? AND end_timestamp <= ? GROUP BY user_agent ORDER BY c DESC SQL", TS_START, TS_END ); foreach (Row row; userAgentsResult) { string userAgent = row["user_agent"].as!string; ulong count = row["c"].as!ulong; report.userAgents[userAgent] = count; } return report; }