perf(home): parallelize private visitor tracking
This commit is contained in:
+11
-27
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user