Add persistent anonymous usage logging
This commit is contained in:
+56
-1
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user