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 Image from "next/image";
|
||||||
import { getFollowerCounts } from "@/lib/followers";
|
import { getFollowerCounts } from "@/lib/followers";
|
||||||
import { LinkCard } from "@/components/LinkCard";
|
import { LinkCard } from "@/components/LinkCard";
|
||||||
import { getRedisClient } from "@/lib/redis";
|
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { connection } from "next/server";
|
|
||||||
import { Users } from "lucide-react";
|
import { Users } from "lucide-react";
|
||||||
import { Kbd } from "@/components/ui/kbd";
|
import { Kbd } from "@/components/ui/kbd";
|
||||||
import Background from "@/public/background/space.webp";
|
import Background from "@/public/background/space.webp";
|
||||||
|
import { trackUniqueVisitor } from "@/lib/visitor/unique-visitors";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
await connection();
|
const visitorCountPromise = headers()
|
||||||
|
.then(trackUniqueVisitor)
|
||||||
const { counts } = await getFollowerCounts();
|
.catch((error) => {
|
||||||
|
console.error("Failed to track visitor:", error);
|
||||||
const headersList = await headers();
|
return 0;
|
||||||
const ip = headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || "unknown";
|
});
|
||||||
|
const [{ counts }, visitorCount] = await Promise.all([
|
||||||
let visitorCount = 0;
|
getFollowerCounts(),
|
||||||
try {
|
visitorCountPromise,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use configured profile data.
|
// Use configured profile data.
|
||||||
const profileName = config.profile.name;
|
const profileName = config.profile.name;
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ export function getCanonicalUrl() {
|
|||||||
return requiredEnvironmentUrl("NEXTAUTH_URL");
|
return requiredEnvironmentUrl("NEXTAUTH_URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getVisitorTrackingConfig() {
|
||||||
|
return {
|
||||||
|
secret: readFeatureEnvironment("Visitor tracking", [
|
||||||
|
"NEXTAUTH_SECRET",
|
||||||
|
] as const).NEXTAUTH_SECRET,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getDiscordServerConfig() {
|
export function getDiscordServerConfig() {
|
||||||
const config = readFeatureEnvironment("Discord", [
|
const config = readFeatureEnvironment("Discord", [
|
||||||
"DISCORD_BOT_TOKEN",
|
"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