chore: remove unused code and visitor tracking

This commit is contained in:
2026-08-23 13:46:47 +00:00 Unverified
parent c78bac8704
commit 03c5c69af9
12 changed files with 9 additions and 220 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ Erika is a Next.js application for a creator landing page, Discord-authenticated
- Bun 1.3+
- PostgreSQL
- Redis for visitor counts and server-sent events
- Redis for follower-count caching and server-sent events
- Discord OAuth and bot credentials for authentication and role-based access
## Development
+2 -22
View File
@@ -6,11 +6,8 @@ import Image from "next/image";
import Link from "next/link";
import { Finger_Paint } from "next/font/google";
import { getFollowerCounts } from "@/lib/followers";
import { headers } from "next/headers";
import { Share2, Trophy, Users } from "lucide-react";
import { Kbd } from "@/components/ui/kbd";
import { Share2, Trophy } from "lucide-react";
import Background from "@/public/background/space.webp";
import { trackUniqueVisitor } from "@/lib/visitor/unique-visitors";
import {
formatCount,
getCountForLink,
@@ -106,16 +103,7 @@ function LinkList({
}
export default async function Home() {
const visitorCountPromise = headers()
.then(trackUniqueVisitor)
.catch((error) => {
console.error("Failed to track visitor:", error);
return 0;
});
const [{ counts }, visitorCount] = await Promise.all([
getFollowerCounts(),
visitorCountPromise,
]);
const { counts } = await getFollowerCounts();
// Use configured profile data.
const profileName = config.profile.name;
@@ -166,14 +154,6 @@ export default async function Home() {
<h1 className={`${fingerPaint.className} text-2xl`}>
{profileName}
</h1>
{visitorCount > 0 && (
<div className="mt-3 flex justify-center">
<Kbd>
<Users className="size-3.5" />
<span>{visitorCount} Unique Visitors</span>
</Kbd>
</div>
)}
</div>
</CardHeader>
-8
View File
@@ -14,14 +14,6 @@ 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",
-50
View File
@@ -85,53 +85,3 @@ export async function getTiktokProfile(): Promise<TiktokProfile> {
return empty;
}
}
/** Convenience wrapper that just returns the follower count */
export async function getTiktokFollowerCount(): Promise<number | null> {
const profile = await getTiktokProfile();
return profile.followerCount;
}
export interface TiktokVideo {
id: string;
title: string;
coverImageUrl: string;
shareUrl: string;
createTime: number;
}
export async function getLatestTiktokVideo(): Promise<TiktokVideo | null> {
const profile = await getTiktokProfile();
if (!profile.username) {
console.error("[TikTok API] Could not get username from profile to fetch TikWM.");
return null;
}
try {
const res = await fetch(`https://www.tikwm.com/api/user/posts?unique_id=@${profile.username}&count=1`, {
cache: "no-store",
signal: AbortSignal.timeout(5000),
});
if (!res.ok) return null;
const data = await res.json();
const videos = data.data?.videos;
if (!videos || videos.length === 0) return null;
const v = videos[0];
const shareUrl = `https://www.tiktok.com/@${profile.username}/video/${v.video_id}`;
return {
id: v.video_id,
title: v.title ?? "",
coverImageUrl: v.cover ?? "",
shareUrl: shareUrl,
createTime: v.create_time ?? 0,
};
} catch (e) {
console.error("[TikTok API] Error fetching latest video via TikWM:", e);
return null;
}
}
+1 -49
View File
@@ -1,5 +1,5 @@
import "server-only";
import { and, eq, gte, inArray, sql } from "drizzle-orm";
import { and, eq, gte, inArray } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { integer, numeric, pgTable, text } from "drizzle-orm/pg-core";
import postgres from "postgres";
@@ -69,29 +69,6 @@ export async function getXpLeaderboardPage(
return buildXpLeaderboard(rows, requestedPage, searchQuery);
}
export async function getMemberXpStats(userId: string) {
const database = getLeaderboardDatabase();
const [sourceRows, leaderboard] = await Promise.all([
database
.select({
level: userLevels.level,
totalXp: userLevels.xp,
})
.from(userLevels)
.where(eq(userLevels.userId, userId))
.limit(1),
getXpLeaderboardPage(1, userId),
]);
const rankedRow = leaderboard.rows.find((row) => row.userId === userId);
const sourceRow = sourceRows[0];
return {
level: sourceRow?.level ?? 0,
totalXp: sourceRow?.totalXp ?? "0",
rank: rankedRow?.rank ?? null,
};
}
export async function getVoiceLeaderboardPage(
currentChannelIds: string[],
requestedPage: number,
@@ -131,28 +108,3 @@ export async function getVoiceLeaderboardPage(
searchQuery,
);
}
export async function getMemberVoiceStats(
userId: string,
currentChannelIds: string[],
) {
const leaderboard = await getVoiceLeaderboardPage(
currentChannelIds,
1,
undefined,
userId,
);
const rankedRow = leaderboard.rows.find((row) => row.userId === userId);
return {
totalSeconds: rankedRow?.totalSeconds ?? "0",
rank: rankedRow?.rank ?? null,
};
}
export async function getLeaderboardDatabaseReadOnlyStatus() {
const result = await getLeaderboardDatabase().execute<{
transaction_read_only: string;
}>(sql`show transaction_read_only`);
return result[0]?.transaction_read_only ?? null;
}
-31
View File
@@ -1,31 +0,0 @@
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);
});
});
-38
View File
@@ -1,38 +0,0 @@
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";
const LEGACY_VISITOR_SET_KEY = "erika:unique_visitors";
export async function trackUniqueVisitor(headers: Headers) {
const client = await getRedisClient();
const address = getClientAddress(headers);
if (address) {
const { secret } = getVisitorTrackingConfig();
const identifier = hashClientAddress(address, secret);
const [isLegacyVisitor, isPrivateVisitor] = await Promise.all([
client.sismember(LEGACY_VISITOR_SET_KEY, address),
client.sismember(VISITOR_SET_KEY, identifier),
]);
if (isLegacyVisitor) {
// Keep the legacy visitor in the old total without counting a revisit twice.
if (isPrivateVisitor) await client.srem(VISITOR_SET_KEY, identifier);
} else {
await client.sadd(VISITOR_SET_KEY, identifier);
}
}
const [legacyCount, privateCount] = await Promise.all([
client.scard(LEGACY_VISITOR_SET_KEY),
client.scard(VISITOR_SET_KEY),
]);
return legacyCount + privateCount;
}
-15
View File
@@ -1,15 +0,0 @@
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");
}
+2 -3
View File
@@ -16,6 +16,5 @@ Do not remove or bypass these without good reason:
1. `db/index.ts` uses a `globalThis` singleton and small pool size to prevent dev hot-reload connection leaks and reduce idle RAM.
2. The NextAuth session callback caches `discordId` lookups in memory with a TTL.
3. `lib/discord.ts` caches Discord API roles/profiles in memory for 60 seconds to avoid slow repeated external calls.
4. `app/page.tsx` batches Redis unique visitor tracking and only counts when `sadd` adds a new IP.
5. Docker Compose runs a single app replica; this is intentional for low RAM usage.
6. Nginx must disable proxy buffering for the Next.js upstream so App Router streaming and Server Actions do not hang.
4. Docker Compose runs a single app replica; this is intentional for low RAM usage.
5. Nginx must disable proxy buffering for the Next.js upstream so App Router streaming and Server Actions do not hang.
+1 -1
View File
@@ -6,7 +6,7 @@
|---|---|
| `lib/config.ts` | Linktree config for `/`: profile, social links, and `config_p2` with Form link. This does not configure the form system. |
| `lib/icons.tsx` | SVG icon components for social platforms and Form |
| `lib/redis.ts` | Redis singleton using Bun `RedisClient`; used for follower counts and unique visitors |
| `lib/redis.ts` | Redis singleton using Bun `RedisClient`; used for follower counts and server-sent events |
| `lib/auth.ts` | Shared session, admin, and form role-access authorization helpers |
| `lib/discord.ts` | Discord API utilities for guild roles/member profiles with in-memory TTL caching |
| `lib/sanitize-html.ts` | Sanitizes admin-authored rich text before storage/rendering |
+1 -1
View File
@@ -23,7 +23,7 @@ Erika is a Next.js website for the content creator "Erika!". It has two main fea
| `DATABASE_URL` | PostgreSQL connection string |
| `NEXTAUTH_SECRET` | Secret for signing NextAuth sessions |
| `NEXTAUTH_URL` | Canonical URL of the site, such as `https://erika.sudloh.com` |
| `REDIS_URL` | Redis connection string, used for follower-count caching and unique visitors |
| `REDIS_URL` | Redis connection string, used for follower-count caching and server-sent events |
| `BASE_URL` | Public base URL, used in metadata/SEO and generated links |
| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | Discord OAuth app credentials for user login |
| `ADMIN_DISCORD_IDS` | Comma-separated Discord user IDs with admin access |
+1 -1
View File
@@ -5,7 +5,7 @@
| File | Purpose |
|---|---|
| `app/layout.tsx` | Root layout with fonts, theme provider, tooltip provider, toaster, page progress, and SEO metadata |
| `app/page.tsx` | Linktree landing page with avatar, social links, follower counts, unique visitor count, and video background |
| `app/page.tsx` | Linktree landing page with avatar, social links, follower counts, and video background |
| `app/globals.css` | Global CSS styles |
## `/form` User-Facing Forms