From c0429a8e8e1e35a76d52f261dce749229367f0af Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sun, 9 Aug 2026 21:35:37 +0700 Subject: [PATCH] perf(home): parallelize private visitor tracking --- app/page.tsx | 38 +++++++++-------------------- lib/config/server.ts | 8 ++++++ lib/visitor/unique-visitors.test.ts | 31 +++++++++++++++++++++++ lib/visitor/unique-visitors.ts | 22 +++++++++++++++++ lib/visitor/visitor-identifiers.ts | 15 ++++++++++++ 5 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 lib/visitor/unique-visitors.test.ts create mode 100644 lib/visitor/unique-visitors.ts create mode 100644 lib/visitor/visitor-identifiers.ts diff --git a/app/page.tsx b/app/page.tsx index 2f26b28..6d1a9f7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,39 +3,23 @@ import { Card, CardHeader } from "@/components/ui/card"; import Image from "next/image"; import { getFollowerCounts } from "@/lib/followers"; import { LinkCard } from "@/components/LinkCard"; -import { getRedisClient } from "@/lib/redis"; import { headers } from "next/headers"; -import { connection } from "next/server"; import { Users } from "lucide-react"; import { Kbd } from "@/components/ui/kbd"; import Background from "@/public/background/space.webp"; +import { trackUniqueVisitor } from "@/lib/visitor/unique-visitors"; export default async function Home() { - await connection(); - - const { counts } = await getFollowerCounts(); - - const headersList = await headers(); - const ip = headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || "unknown"; - - let visitorCount = 0; - try { - const redis = await getRedisClient(); - if (ip !== "unknown") { - const added = await redis.sadd("erika:unique_visitors", ip); - if (added > 0) { - // New visitor — refresh the count - visitorCount = await redis.scard("erika:unique_visitors"); - } else { - // Existing visitor — use cached count or fetch - visitorCount = await redis.scard("erika:unique_visitors"); - } - } else { - visitorCount = await redis.scard("erika:unique_visitors"); - } - } catch (e) { - console.error("Failed to track visitor:", e); - } + const visitorCountPromise = headers() + .then(trackUniqueVisitor) + .catch((error) => { + console.error("Failed to track visitor:", error); + return 0; + }); + const [{ counts }, visitorCount] = await Promise.all([ + getFollowerCounts(), + visitorCountPromise, + ]); // Use configured profile data. const profileName = config.profile.name; diff --git a/lib/config/server.ts b/lib/config/server.ts index 4494e00..3f3cb4f 100644 --- a/lib/config/server.ts +++ b/lib/config/server.ts @@ -13,6 +13,14 @@ export function getCanonicalUrl() { return requiredEnvironmentUrl("NEXTAUTH_URL"); } +export function getVisitorTrackingConfig() { + return { + secret: readFeatureEnvironment("Visitor tracking", [ + "NEXTAUTH_SECRET", + ] as const).NEXTAUTH_SECRET, + }; +} + export function getDiscordServerConfig() { const config = readFeatureEnvironment("Discord", [ "DISCORD_BOT_TOKEN", diff --git a/lib/visitor/unique-visitors.test.ts b/lib/visitor/unique-visitors.test.ts new file mode 100644 index 0000000..5f17e18 --- /dev/null +++ b/lib/visitor/unique-visitors.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test"; +import { + getClientAddress, + hashClientAddress, +} from "./visitor-identifiers"; + +describe("privacy-preserving visitor tracking", () => { + test("uses the first valid forwarded address", () => { + const headers = new Headers({ + "x-forwarded-for": "203.0.113.9, 10.0.0.1", + "x-real-ip": "192.0.2.2", + }); + + expect(getClientAddress(headers)).toBe("203.0.113.9"); + }); + + test("rejects untrusted non-IP identifiers", () => { + expect( + getClientAddress(new Headers({ "x-forwarded-for": "unknown" })), + ).toBeNull(); + }); + + test("creates a stable identifier without retaining the address", () => { + const address = "203.0.113.9"; + const identifier = hashClientAddress(address, "test-secret"); + + expect(identifier).toBe(hashClientAddress(address, "test-secret")); + expect(identifier).not.toContain(address); + expect(identifier).toHaveLength(64); + }); +}); diff --git a/lib/visitor/unique-visitors.ts b/lib/visitor/unique-visitors.ts new file mode 100644 index 0000000..658019f --- /dev/null +++ b/lib/visitor/unique-visitors.ts @@ -0,0 +1,22 @@ +import "server-only"; + +import { getVisitorTrackingConfig } from "@/lib/config/server"; +import { getRedisClient } from "@/lib/redis"; +import { + getClientAddress, + hashClientAddress, +} from "@/lib/visitor/visitor-identifiers"; + +const VISITOR_SET_KEY = "erika:unique_visitors:v2"; + +export async function trackUniqueVisitor(headers: Headers) { + const client = await getRedisClient(); + const address = getClientAddress(headers); + + if (address) { + const { secret } = getVisitorTrackingConfig(); + await client.sadd(VISITOR_SET_KEY, hashClientAddress(address, secret)); + } + + return client.scard(VISITOR_SET_KEY); +} diff --git a/lib/visitor/visitor-identifiers.ts b/lib/visitor/visitor-identifiers.ts new file mode 100644 index 0000000..cf3f67b --- /dev/null +++ b/lib/visitor/visitor-identifiers.ts @@ -0,0 +1,15 @@ +import { createHmac } from "node:crypto"; +import { isIP } from "node:net"; + +export function getClientAddress(headers: Headers) { + const forwardedAddress = headers + .get("x-forwarded-for") + ?.split(",", 1)[0] + ?.trim(); + const address = forwardedAddress || headers.get("x-real-ip")?.trim(); + return address && isIP(address) ? address : null; +} + +export function hashClientAddress(address: string, secret: string) { + return createHmac("sha256", secret).update(address).digest("hex"); +}