forked from Mike/freno-dev
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
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import type { APIEvent } from "@solidjs/start/server";
|
|
import { getRequestIP } from "vinxi/http";
|
|
import {
|
|
logVisit,
|
|
enrichAnalyticsEntry,
|
|
flushAnalytics
|
|
} from "~/server/analytics";
|
|
import { parseUtm } from "~/server/utm";
|
|
import { downloadPath } from "~/lib/download-asset";
|
|
|
|
/**
|
|
* Record one download event under the canonical path `/api/downloads/<asset>`.
|
|
*
|
|
* Every download door funnels through here so the analytics page has a single
|
|
* label per asset to group on: the tRPC click proxy, the S3 file route that
|
|
* serves Sparkle updates, and the Nook door.
|
|
*
|
|
* Best-effort by design — a failed analytics write must never block a
|
|
* download. Flushed rather than merely buffered: a download is low-volume and
|
|
* the 30s batch timer is not guaranteed to survive the response on serverless.
|
|
*
|
|
* `event.request` is expected to be a Fetch `Request` (true for Vinxi route
|
|
* handlers and browser tRPC calls). A server-side `createCaller` hands over a
|
|
* Node request, whose headers have no `get` — that throw is swallowed below,
|
|
* so such a click is not recorded. No server-side caller downloads today.
|
|
*/
|
|
export async function trackDownload(
|
|
event: APIEvent,
|
|
asset: string
|
|
): Promise<void> {
|
|
const { request } = event;
|
|
|
|
try {
|
|
const referrer =
|
|
request.headers.get("referer") ?? request.headers.get("referrer");
|
|
const forwarded = request.headers.get("x-forwarded-for");
|
|
|
|
await logVisit(
|
|
enrichAnalyticsEntry({
|
|
path: downloadPath(asset),
|
|
method: "GET",
|
|
referrer,
|
|
userAgent: request.headers.get("user-agent"),
|
|
ipAddress:
|
|
getRequestIP(event.nativeEvent) ??
|
|
(forwarded ? forwarded.split(",")[0]!.trim() : null),
|
|
// The click carries its own tags when a campaign links straight at the
|
|
// door; otherwise the landing page's tags ride along in the referer.
|
|
...parseUtm(request.url, referrer),
|
|
// Edge header country, overridable with ?c= for manual verification.
|
|
country:
|
|
new URL(request.url).searchParams.get("c") ??
|
|
request.headers.get("x-vercel-ip-country") ??
|
|
request.headers.get("cf-ipcountry")
|
|
})
|
|
);
|
|
await flushAnalytics();
|
|
} catch (error) {
|
|
console.error(`Failed to log download of ${asset}:`, error);
|
|
}
|
|
}
|