From 40611258bfc08bf573020bd85d2f72d6b5052d72 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jun 2026 15:46:27 -0400 Subject: [PATCH] add DNS traffic to stats page --- .gitignore | 2 +- src/SMAPI.Web/BackgroundService.cs | 1 + src/SMAPI.Web/Views/Index/Stats.cshtml | 11 +- src/SMAPI.Web/wwwroot/Content/js/stats.js | 125 +++++++++++++++++++++- 4 files changed, 134 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 04164d1d..7bf9b226 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ _ReSharper*/ appsettings.Development.json # auto-generated files -src/SMAPI.Web/wwwroot/Content/data/*.jsonl +src/SMAPI.Web/wwwroot/Content/data src/SMAPI.Web/Properties/PublishProfiles src/SMAPI.Web/Properties/ServiceDependencies diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 8f4a2b5e..75c38c29 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -280,6 +280,7 @@ internal class BackgroundService : IHostedService, IDisposable File.Copy(BackgroundService.ModDatasetRepo.GetFilePath("stats/mods by type.jsonl"), copyToPath, overwrite: true); File.Copy(BackgroundService.ModDatasetRepo.GetFilePath("stats/Content Patcher packs by format version.jsonl"), Path.Combine(BackgroundService.WebRootPath, "Content", "data", "content-packs-by-format.jsonl"), overwrite: true); File.Copy(BackgroundService.ModDatasetRepo.GetFilePath("reference-data/SMAPI costs.jsonl"), Path.Combine(BackgroundService.WebRootPath, "Content", "data", "smapi-costs.jsonl"), overwrite: true); + File.Copy(BackgroundService.ModDatasetRepo.GetFilePath("reference-data/SMAPI DNS queries.json"), Path.Combine(BackgroundService.WebRootPath, "Content", "data", "smapi-dns-queries.json"), overwrite: true); context.WriteLine("Done!"); } diff --git a/src/SMAPI.Web/Views/Index/Stats.cshtml b/src/SMAPI.Web/Views/Index/Stats.cshtml index 11bbde10..1df835a8 100644 --- a/src/SMAPI.Web/Views/Index/Stats.cshtml +++ b/src/SMAPI.Web/Views/Index/Stats.cshtml @@ -20,7 +20,8 @@ smapi.statsPage({ modsByTypeUri: @this.ForJson(Url.ContentWithCacheBust("~/Content/data/mods-by-type.jsonl")), contentPacksByFormatUri: @this.ForJson(Url.ContentWithCacheBust("~/Content/data/content-packs-by-format.jsonl")), - smapiCostsUri: @this.ForJson(Url.ContentWithCacheBust("~/Content/data/smapi-costs.jsonl")) + smapiCostsUri: @this.ForJson(Url.ContentWithCacheBust("~/Content/data/smapi-costs.jsonl")), + smapiDnsQueriesUri: @this.ForJson(Url.ContentWithCacheBust("~/Content/data/smapi-dns-queries.json")) }); }); @@ -35,6 +36,7 @@
  • Mods by type
  • New mods
  • Content Patcher packs
  • +
  • Web traffic
  • SMAPI costs
  • @@ -101,6 +103,13 @@
    + @* web traffic *@ +

    Web traffic

    +
    Based on data from {{dnsQueries.lastUpdated}}.
    + +
    SMAPI's web services received about {{(dnsQueries.latestCount / 1000000).toFixed(1)}} million DNS queries in {{dnsQueries.lastUpdated}}. This serves as a rough indicator of player and mod author activity, since SMAPI doesn't collect any telemetry.
    +
    + @* SMAPI costs *@

    SMAPI costs

    Based on data from {{costs.lastUpdated}}. This information is delayed due to billing cycles.
    diff --git a/src/SMAPI.Web/wwwroot/Content/js/stats.js b/src/SMAPI.Web/wwwroot/Content/js/stats.js index 4bfd4e47..e66e3ce7 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/stats.js +++ b/src/SMAPI.Web/wwwroot/Content/js/stats.js @@ -67,6 +67,20 @@ smapi.statsPage = function (options) { } }, + // configure the 'web traffic' stats + dnsQueries: { + // notable events which may affect traffic, indexed by their date in smapi-dns-queries.json + notableEvents: { + "2018-05": "Stardew Valley 1.3 beta", // 30 April 2018 + "2018-08": "Stardew Valley 1.3", // 01 August 2018 + "2019-12": "Stardew Valley 1.4", // 26 November 2019 + "2020-12": "Stardew Valley 1.5", // 21 December 2020 + "2021-08": "Stardew Valley 1.5.5", // 17 August 2021 + "2024-03": "Stardew Valley 1.6", // 19 March 2024 + "2024-11": "Stardew Valley 1.6.9" // 04 November 2024 + } + }, + // configure the 'SMAPI costs' stats costs: { // colors for each service (omitted entries cycle through the colorPalette) @@ -110,6 +124,10 @@ smapi.statsPage = function (options) { xLabels: [], versions: [] }, + dnsQueries: { + xLabels: [], + values: [] + }, costs: { rows: [], serviceKeys: [], @@ -147,6 +165,10 @@ smapi.statsPage = function (options) { total: 0, topVersions: [] }, + dnsQueries: { + lastUpdated: null, + latestCount: 0 + }, costs: { lastUpdated: null, previousCost: 0, @@ -187,12 +209,13 @@ smapi.statsPage = function (options) { /********* ** Fetch data files *********/ - const [modsByTypeRows, contentPacksRows, costsRows] = await Promise.all([ + const [modsByTypeRows, contentPacksRows, costsRows, dnsData] = await Promise.all([ fetchJsonLinesAsync(options.modsByTypeUri), fetchJsonLinesAsync(options.contentPacksByFormatUri), - fetchJsonLinesAsync(options.smapiCostsUri) + fetchJsonLinesAsync(options.smapiCostsUri), + fetchJsonAsync(options.smapiDnsQueriesUri) ]); - if (!modsByTypeRows || !contentPacksRows || !costsRows) { + if (!modsByTypeRows || !contentPacksRows || !costsRows || !dnsData) { this.isDataFailed = true; return; } @@ -319,6 +342,25 @@ smapi.statsPage = function (options) { } + /********* + ** Process DNS queries + *********/ + { + const xLabels = Object.keys(dnsData); + const values = Object.values(dnsData); + + data.dnsQueries = { + xLabels, + values + }; + + this.dnsQueries = { + lastUpdated: formatMonthYear(xLabels[xLabels.length - 1]), + latestCount: values[values.length - 1] + }; + } + + /********* ** Process SMAPI costs *********/ @@ -858,6 +900,70 @@ smapi.statsPage = function (options) { } + /********* + ** 'DNS queries over time' bar chart + *********/ + { + const curData = data.dnsQueries; + + const chart = echarts.init(document.getElementById("dnsQueriesOverTime")); + + chart.setOption({ + title: { + text: "Web traffic", + textStyle: createTitleStyle() + }, + grid: { + top: 55, + bottom: this.showAdvancedControls ? 80 : 60, + left: 70, + right: 20 + }, + xAxis: { + type: "category", + data: curData.xLabels, + axisLabel: { + rotate: 90, + fontSize: 11 + } + }, + yAxis: { + type: "value", + name: "DNS queries (millions)", + axisLabel: { + formatter: value => (value / 1_000_000).toFixed(0) + } + }, + series: [ + { + type: "bar", + data: curData.values, + color: "#3388cc", + markLine: createMarkLine(p => p.name, config.dnsQueries.notableEvents) + } + ], + tooltip: { + trigger: "axis", + formatter: params => { + const value = params[0].value; + + const lines = [ + params[0].axisValue, // date + value > 1_000_000 + ? `${(Math.round(value / 100_000) / 10).toLocaleString()} million` + : value.toLocaleString() + ]; + + return lines.join("
    "); + } + }, + toolbox: createToolbox(true, this.showAdvancedControls) + }); + + charts.push(chart); + } + + /********* ** 'SMAPI costs over time' stacked bar chart *********/ @@ -960,6 +1066,19 @@ smapi.statsPage = function (options) { return rawJsonLines.trim().split(/\r?\n/).filter(row => !!row).map(JSON.parse); } + /** + * Fetch and parse data from a JSON URI. + * @param {string} fetchUri The URI from which to fetch the JSON data. + * @returns {null|object} Returns the parsed data if it was found, else `null`. + */ + async function fetchJsonAsync(fetchUri) { + const response = await fetch(fetchUri); + if (!response.ok) + return null; + + return await response.json(); + } + /** * Get a formatted date string. * @param {Date|string} date The ISO 8601 date-only string to format.