From: Roberto Morado Date: Mon, 10 Nov 2025 00:08:39 +0000 (-0500) Subject: Add metrics X-Git-Url: https://git.morado.dev/post/url?a=commitdiff_plain;h=207424a495854b9247a61b792358e28e55947609;p=blog.morado.dev Add metrics --- diff --git a/main.ts b/main.ts index 4f0fd0d..ceb9f68 100644 --- a/main.ts +++ b/main.ts @@ -33,6 +33,8 @@ const config = { title: "God's Witness", }; +export const METRICS_KEY = ["metrics", "visits"] as const; + await Deno.mkdir(STORAGE_DIR, { recursive: true }); const kv = await Deno.openKv(`${STORAGE_DIR}/db`); export function __closeKvForTests() { @@ -115,6 +117,18 @@ export function generateFilename(title: string, id: string): string { return `${slug}-${id}.md`; } +async function incrementVisitCount(): Promise { + const result = await kv.get(METRICS_KEY); + const next = (result.value ?? 0) + 1; + await kv.set(METRICS_KEY, next); + return next; +} + +export async function getVisitCount(): Promise { + const result = await kv.get(METRICS_KEY); + return result.value ?? 0; +} + // Session management function generateSessionId(): string { return crypto.randomUUID(); @@ -398,6 +412,41 @@ function renderHTML(content: string): string { color: #666; margin-bottom: 15px; } + .metrics-section { + background: white; + padding: 25px; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + margin-bottom: 30px; + } + .metrics-section h3 { + margin-bottom: 15px; + font-size: 1.2em; + color: #333; + } + .metrics-grid { + display: flex; + flex-wrap: wrap; + gap: 15px; + } + .metrics-card { + flex: 1; + min-width: 200px; + padding: 15px; + border-radius: 4px; + border: 1px solid #eee; + background: #fafafa; + } + .metrics-label { + font-size: 0.9em; + color: #777; + margin-bottom: 5px; + } + .metrics-value { + font-size: 1.8em; + font-weight: 600; + color: #222; + } .error { background: #f8d7da; color: #721c24; @@ -493,6 +542,7 @@ async function renderPostView(postId: string): Promise { async function renderAdminPage( username: string, csrfToken: string, + visitCount: number, ): Promise { const posts = await getAllPosts(); const sortedPosts = posts.sort((a, b) => b.timestamp - a.timestamp); @@ -516,6 +566,18 @@ async function renderAdminPage( }).join("") : '
No posts yet. Create your first post below!
'; + const metricsHTML = ` +
+

Metrics

+
+
+
Total visits
+
${visitCount.toLocaleString()}
+
+
+
+ `; + return renderHTML(` ${renderHeader(username)} @@ -541,6 +603,8 @@ code block + ${metricsHTML} +
${postsHTML}
@@ -560,9 +624,9 @@ function loginFormResponse(message?: string, status = 200): Response { }); } -async function adminPageResponse(username: string): Promise { +async function adminPageResponse(username: string, visitCount: number): Promise { const csrfToken = generateCsrfToken(); - const html = await renderAdminPage(username, csrfToken); + const html = await renderAdminPage(username, csrfToken, visitCount); return new Response(html, { headers: { "Content-Type": "text/html", @@ -631,6 +695,7 @@ export function parseFormData(body: string): Record { export async function handler(req: Request): Promise { const url = new URL(req.url); + const visitCount = await incrementVisitCount(); // Check authentication const sessionId = getSessionIdFromCookie(req); @@ -671,7 +736,7 @@ export async function handler(req: Request): Promise { }); } - return await adminPageResponse(session.username); + return await adminPageResponse(session.username, visitCount); } // Login page diff --git a/tests/main_test.ts b/tests/main_test.ts index bffe643..d68c1ed 100644 --- a/tests/main_test.ts +++ b/tests/main_test.ts @@ -417,3 +417,29 @@ Deno.test("syncPosts regenerates Markdown files when KV is missing them", async } }); }); + +Deno.test("visit counter increments per request", async () => { + await withTempDir(async () => { + const mod = await import( + new URL("../main.ts", import.meta.url).href + + `?t=${crypto.randomUUID()}` + ); + const { handler, METRICS_KEY, __closeKvForTests } = mod; + + for (let i = 0; i < 3; i++) { + await handler(new Request("http://localhost/")); + } + + const kv = await Deno.openKv("./storage/db"); + const result = await kv.get(METRICS_KEY); + kv.close(); + + if (result.value !== 3) { + throw new Error( + `Expected 3 visits recorded, got ${result.value ?? 0}`, + ); + } + + __closeKvForTests?.(); + }); +});