Add persistent anonymous usage logging

This commit is contained in:
wuyanwanwu
2026-08-14 01:05:50 +08:00
parent b890ce4035
commit 5a312f5039
18 changed files with 169 additions and 48 deletions
+56 -1
View File
@@ -1,10 +1,14 @@
import { createReadStream, existsSync, statSync } from "node:fs";
import { appendFile, createReadStream, existsSync, mkdirSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { createServer } from "node:http";
import { extname, join, normalize } from "node:path";
const packagedRoot = join(process.cwd(), "public");
const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out");
const port = Number(process.env.APP_PORT || 3200);
const dataDirectory = process.env.DATA_DIR || join(process.cwd(), "data");
const usageLogPath = join(dataDirectory, "usage.jsonl");
mkdirSync(dataDirectory, { recursive: true });
const mimeTypes = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
@@ -17,8 +21,59 @@ const mimeTypes = {
".webp": "image/webp",
};
function sendJson(response, status, value) {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(value));
}
function writeUsageLog(request, response) {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (body.length > 16_384) request.destroy();
});
request.on("end", async () => {
try {
const input = JSON.parse(body || "{}");
if (!['page_view', 'conversion'].includes(input.event)) {
sendJson(response, 400, { error: "Invalid event" });
return;
}
const entry = {
timestamp: new Date().toISOString(),
event: input.event,
client_hash: request.headers["x-forwarded-for"] || request.socket.remoteAddress
? createHash("sha256").update(String(request.headers["x-forwarded-for"] || request.socket.remoteAddress)).digest("hex").slice(0, 16)
: undefined,
...(input.event === "conversion" ? {
width: Math.max(8, Math.min(256, Number(input.width) || 0)),
height: Math.max(8, Math.min(256, Number(input.height) || 0)),
sampling_strategy: input.sampling_strategy === "dominant" ? "dominant" : "smooth",
color_limit: Math.max(2, Math.min(64, Number(input.color_limit) || 0)),
actual_colors: Math.max(0, Math.min(221, Number(input.actual_colors) || 0)),
bead_count: Math.max(0, Math.min(65_536, Number(input.bead_count) || 0)),
transparent_cells: Math.max(0, Math.min(65_536, Number(input.transparent_cells) || 0)),
} : {}),
};
await appendFile(usageLogPath, `${JSON.stringify(entry)}\n`, "utf8");
sendJson(response, 202, { ok: true });
} catch {
sendJson(response, 400, { error: "Invalid request" });
}
});
}
createServer((request, response) => {
const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname);
if (pathname === "/api/usage" && request.method === "POST") {
writeUsageLog(request, response);
return;
}
if (pathname.startsWith("/api/")) {
sendJson(response, 404, { error: "Not found" });
return;
}
const relativePath = normalize(pathname).replace(/^([/\\])+/, "");
let filePath = join(root, relativePath || "index.html");