forked from Mike/freno-dev
downloads: track every door and surface them on /analytics
The analytics page could not answer "how many downloads, of which
product". Three of the four download doors recorded nothing: the tRPC
click proxy only left the generic request row, whose path is the raw URL
with the asset buried in `?batch=1&input={"asset_name":...}`, and the S3
file route that serves Sparkle updates and the Nook app logged nothing
at all. The one door that did log (the Nook redirect) kept a private copy
of the logVisit/enrich/flush sequence, under its own path.
All doors now go through trackDownload, which writes one row per event
under /api/downloads/<asset> and flushes it inside the request — a click
is low-volume and serverless is not guaranteed to run the 30s batch
timer after the response. The file route maps its prefix allowlist to
asset keys, so a version bump stays one asset rather than one per
release. getAnalyticsSummary groups those rows into a downloads block and
the freno.me panel renders it under the overview cards.
History is recovered to the degree it exists: the Nook door's rows and
the tRPC clicks still carry enough to derive from (asset name in the URL).
scripts/backfill-downloads.ts inserts those derived rows with a
`backfill-<source id>` primary key and INSERT OR IGNORE, leaving the
originals untouched, so re-running is a no-op. Run against the live table
it added 66 rows: gaze 42, thenook 19, inputhalo 4, lineage 1.
Tests: new backfill suite (derivation, visitor detail carried over,
idempotency, empty history) and summariseDownloads cases; the Nook route
test now pins the canonical path, and the NOOK-337 allowlist audit reads
the map keys instead of quoted strings. Red-checked by stubbing the
downloads block back to zeros — the summary case fails, then passes.
Full suite matches master.
Co-Authored-By: nw-flash <noreply@nw-flash.local>
MISC-48
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { ConnectionFactory } from "./database";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type { VisitorAnalytics, AnalyticsQuery } from "~/db/types";
|
||||
import { DOWNLOAD_PATH_PREFIX, summariseDownloads } from "~/lib/download-asset";
|
||||
import { CACHE_CONFIG } from "~/config";
|
||||
import {
|
||||
VISITOR_ANALYTICS_ADDED_COLUMNS,
|
||||
@@ -272,6 +273,10 @@ export async function getAnalyticsSummary(days: number = 30): Promise<{
|
||||
topReferrers: Array<{ referrer: string; count: number }>;
|
||||
deviceTypes: Array<{ type: string; count: number }>;
|
||||
browsers: Array<{ browser: string; count: number }>;
|
||||
downloads: {
|
||||
total: number;
|
||||
byAsset: Array<{ asset: string; count: number }>;
|
||||
};
|
||||
topCampaigns: Array<{
|
||||
source: string | null;
|
||||
medium: string | null;
|
||||
@@ -409,6 +414,24 @@ export async function getAnalyticsSummary(days: number = 30): Promise<{
|
||||
count: row.count as number
|
||||
}));
|
||||
|
||||
// Downloads: every door records one row under `/api/downloads/<asset>` via
|
||||
// `trackDownload`, so the asset is the path suffix and nothing has to be
|
||||
// parsed out of a query string.
|
||||
const downloadsResult = await conn.execute({
|
||||
sql: `SELECT path, COUNT(*) as count FROM VisitorAnalytics
|
||||
WHERE path LIKE '${DOWNLOAD_PATH_PREFIX}%'
|
||||
AND created_at >= datetime('now', '-${days} days')
|
||||
GROUP BY path
|
||||
ORDER BY count DESC`,
|
||||
args: []
|
||||
});
|
||||
const downloads = summariseDownloads(
|
||||
downloadsResult.rows.map((row) => ({
|
||||
path: String(row.path),
|
||||
count: row.count as number
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
totalVisits,
|
||||
totalPageVisits,
|
||||
@@ -420,7 +443,8 @@ export async function getAnalyticsSummary(days: number = 30): Promise<{
|
||||
topReferrers,
|
||||
deviceTypes,
|
||||
browsers,
|
||||
topCampaigns
|
||||
topCampaigns,
|
||||
downloads
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user