What the site does with a report.
The site's repository is private, and you can read what your server sends but could not otherwise read what is kept. This is the receiver, printed from the source this site was built from.
src/app/api/usage-ping/route.ts
import { clientIp, errorResponse, fail, readBody } from "@/lib/link/machine"
import { LinkError } from "@/lib/link/protocol"
import { allowReport } from "@/lib/usage/limit"
import { parseUsageReport } from "@/lib/usage/report"
import { recordReport } from "@/lib/usage/totals"
/* A dashboard whose administrator said yes reports here once a UTC day. The
address is used for the in-memory limit and then forgotten: only the three
fields reach the database, and only as additions to that day's totals. */
export async function POST(req: Request) {
try {
if (!allowReport(clientIp(req))) fail(429, "rate_limited", "too many reports, slow down")
const { json } = await readBody(req)
await recordReport(parseUsageReport(json))
return new Response(null, { status: 204 })
} catch (err) {
if (err instanceof LinkError) return errorResponse(err)
console.error("[usage] report failed:", err)
return Response.json(
{ error: { code: "internal", message: "internal error" } },
{ status: 500 },
)
}
}
src/lib/usage/report.ts
import { LinkError } from "@/lib/link/protocol"
export type UsageReport = { version: string; usedToday: boolean; usedThisWeek: boolean }
/* Release numbers only. Anyone can send a report and the version is printed on
a public page, so it cannot be free text. */
const VERSION = /^\d{1,4}(\.\d{1,4}){1,3}$/
function refuse(message: string): never {
throw new LinkError(400, "bad_report", message)
}
/* The exact shape a dashboard sends and nothing else: a fourth field is
refused rather than ignored, so the report cannot grow without this changing. */
export function parseUsageReport(json: unknown): UsageReport {
if (typeof json !== "object" || json === null || Array.isArray(json))
refuse("the report must be a JSON object")
if (Object.keys(json).length !== 3)
refuse("the report must be exactly version, usedToday and usedThisWeek")
const { version, usedToday, usedThisWeek } = json as Record<string, unknown>
if (typeof version !== "string" || !VERSION.test(version))
refuse("version must be a release number")
if (typeof usedToday !== "boolean" || typeof usedThisWeek !== "boolean")
refuse("usedToday and usedThisWeek must be booleans")
// Used in the last day is also used in the last week.
if (usedToday && !usedThisWeek) refuse("usedToday without usedThisWeek cannot happen")
return { version, usedToday, usedThisWeek }
}
src/lib/usage/limit.ts
import { createHmac, randomBytes } from "node:crypto"
import { tokenBucket } from "@/lib/link/rate-limit"
/* Ten at once, then one a minute, which leaves room for several servers behind
one address. The buckets are keyed by a keyed hash under a salt that dies
with the process, so not even this process's memory holds a list of the
addresses that reported. */
const salt = randomBytes(32)
const take = tokenBucket(1, 10)
export function allowReport(ip: string | null) {
return take(
createHmac("sha256", salt)
.update(ip ?? "unknown")
.digest("base64url"),
)
}
src/lib/usage/totals.ts
import { sql } from "@/lib/db"
import type { UsageReport } from "@/lib/usage/report"
export type UsageTotals = { servers: number; used_today: number; used_this_week: number }
export type UsageDay = UsageTotals & { day: Date }
export type UsageVersion = UsageTotals & { version: string }
export async function recordReport(report: UsageReport) {
const s = await sql()
await s`
INSERT INTO usage_days (day, version, servers, used_today, used_this_week)
VALUES (
(now() AT TIME ZONE 'utc')::date, ${report.version}, 1,
${report.usedToday ? 1 : 0}, ${report.usedThisWeek ? 1 : 0}
)
ON CONFLICT (day, version) DO UPDATE SET
servers = usage_days.servers + 1,
used_today = usage_days.used_today + EXCLUDED.used_today,
used_this_week = usage_days.used_this_week + EXCLUDED.used_this_week`
}
/* Today can still be filling up, so the number to quote is yesterday's, unless
today is all there is. `days` is oldest first and ends today. */
export function headlineDay(days: UsageDay[]) {
const today = days[days.length - 1]
const yesterday = days[days.length - 2]
return yesterday.servers || !today.servers
? { ...yesterday, partial: false }
: { ...today, partial: true }
}
/* The biggest versions, then the rest as one row. Anyone can send a report, so
a page cannot promise to print every version it was told about. */
export function topVersions(rows: UsageVersion[], keep = 8): UsageVersion[] {
const sorted = [...rows].sort((a, b) => b.servers - a.servers)
const rest = sorted.slice(keep)
if (!rest.length) return sorted
return [
...sorted.slice(0, keep),
{
version: "Other",
servers: rest.reduce((n, r) => n + r.servers, 0),
used_today: rest.reduce((n, r) => n + r.used_today, 0),
used_this_week: rest.reduce((n, r) => n + r.used_this_week, 0),
},
]
}
/* `range` days ending today, UTC, with the empty ones filled in, and the
versions behind the number the page leads with. */
export async function usageSummary(range: number) {
const s = await sql()
const days = await s<UsageDay[]>`
SELECT d::date AS day,
coalesce(sum(u.servers), 0)::int AS servers,
coalesce(sum(u.used_today), 0)::int AS used_today,
coalesce(sum(u.used_this_week), 0)::int AS used_this_week
FROM generate_series(
(now() AT TIME ZONE 'utc')::date::timestamp - ${range - 1}::int * interval '1 day',
(now() AT TIME ZONE 'utc')::date::timestamp,
interval '1 day'
) AS d
LEFT JOIN usage_days u ON u.day = d::date
GROUP BY d ORDER BY d`
const headline = headlineDay(days)
const versions = await s<UsageVersion[]>`
SELECT version, servers, used_today, used_this_week FROM usage_days
WHERE day = ${headline.day.toISOString().slice(0, 10)}::date`
return { days, headline, versions: topVersions(versions) }
}
src/lib/usage/schema.ts
import type postgres from "postgres"
/* Everything the usage count keeps (docs/README.md): one row per UTC day and
version, holding only totals. There is no per-report row, no timestamp and
no address, so the table cannot say which server sent what. */
export async function usageSchema(s: postgres.Sql) {
await s`
CREATE TABLE IF NOT EXISTS usage_days (
day date NOT NULL,
version text NOT NULL,
servers int NOT NULL,
used_today int NOT NULL,
used_this_week int NOT NULL,
PRIMARY KEY (day, version)
)`
}