Skip to content

Commit 0c129d8

Browse files
authored
Merge pull request #2147 from dubinc/click-cache-class
Refactor click caching with dedicated ClickCache class
2 parents 5ade7d5 + 4a5f9f8 commit 0c129d8

File tree

5 files changed

+39
-15
lines changed

5 files changed

+39
-15
lines changed

apps/web/app/api/track/click/route.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { verifyAnalyticsAllowedHostnames } from "@/lib/analytics/verify-analytics-allowed-hostnames";
22
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
3+
import { clickCache } from "@/lib/api/links/click-cache";
34
import { parseRequestBody } from "@/lib/api/utils";
45
import { getLinkWithAllowedHostnames } from "@/lib/planetscale/get-link-with-allowed-hostnames";
56
import { recordClick } from "@/lib/tinybird";
6-
import { redis } from "@/lib/upstash";
77
import { isValidUrl, LOCALHOST_IP, nanoid } from "@dub/utils";
88
import { ipAddress, waitUntil } from "@vercel/functions";
99
import { AxiomRequest, withAxiom } from "next-axiom";
@@ -31,8 +31,7 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
3131

3232
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
3333

34-
const cacheKey = `recordClick:${domain}:${key}:${ip}`;
35-
let clickId = await redis.get<string>(cacheKey);
34+
let clickId = await clickCache.get({ domain, key, ip });
3635

3736
// only generate + record a new click ID if it's not already cached in Redis
3837
if (!clickId) {

apps/web/app/api/track/visit/route.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { verifyAnalyticsAllowedHostnames } from "@/lib/analytics/verify-analytics-allowed-hostnames";
22
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
3+
import { clickCache } from "@/lib/api/links/click-cache";
34
import { parseRequestBody } from "@/lib/api/utils";
45
import { getLinkWithAllowedHostnames } from "@/lib/planetscale/get-link-with-allowed-hostnames";
56
import { recordClick } from "@/lib/tinybird";
6-
import { redis } from "@/lib/upstash";
77
import { isValidUrl, LOCALHOST_IP, nanoid } from "@dub/utils";
88
import { ipAddress, waitUntil } from "@vercel/functions";
99
import { AxiomRequest, withAxiom } from "next-axiom";
@@ -37,9 +37,8 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
3737
}
3838

3939
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
40-
const cacheKey = `recordClick:${domain}:${key}:${ip}`;
4140

42-
let clickId = await redis.get<string>(cacheKey);
41+
let clickId = await clickCache.get({ domain, key, ip });
4342

4443
// only generate + record a new click ID if it's not already cached in Redis
4544
if (!clickId) {

apps/web/lib/api/links/click-cache.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { redis } from "@/lib/upstash";
2+
3+
// Cache the click ID in Redis for 1 hour
4+
const CACHE_EXPIRATION = 60 * 60;
5+
6+
interface KeyProps {
7+
domain: string;
8+
key: string;
9+
ip: string | undefined;
10+
}
11+
12+
class ClickCache {
13+
async set({ domain, key, ip, clickId }: KeyProps & { clickId: string }) {
14+
return await redis.set(this._createKey({ domain, key, ip }), clickId, {
15+
ex: CACHE_EXPIRATION,
16+
});
17+
}
18+
19+
async get({ domain, key, ip }: KeyProps) {
20+
return await redis.get<string>(this._createKey({ domain, key, ip }));
21+
}
22+
23+
_createKey({ domain, key, ip }: KeyProps) {
24+
return `recordClick:${domain}:${key}:${ip}`;
25+
}
26+
}
27+
28+
export const clickCache = new ClickCache();

apps/web/lib/middleware/link.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
parse,
77
} from "@/lib/middleware/utils";
88
import { recordClick } from "@/lib/tinybird";
9-
import { formatRedisLink, redis } from "@/lib/upstash";
9+
import { formatRedisLink } from "@/lib/upstash";
1010
import {
1111
DUB_HEADERS,
1212
LEGAL_WORKSPACE_ID,
@@ -27,6 +27,7 @@ import {
2727
} from "next/server";
2828
import { linkCache } from "../api/links/cache";
2929
import { isCaseSensitiveDomain } from "../api/links/case-sensitivity";
30+
import { clickCache } from "../api/links/click-cache";
3031
import { getLinkViaEdge } from "../planetscale";
3132
import { getDomainViaEdge } from "../planetscale/get-domain-via-edge";
3233
import { hasEmptySearchParams } from "./utils/has-empty-search-params";
@@ -227,8 +228,8 @@ export default async function LinkMiddleware(
227228
// if trackConversion is enabled, check if clickId is cached in Redis
228229
if (trackConversion) {
229230
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
230-
const cacheKey = `recordClick:${domain}:${key}:${ip}`;
231-
clickId = (await redis.get<string>(cacheKey)) || undefined;
231+
232+
clickId = (await clickCache.get({ domain, key, ip })) || undefined;
232233
}
233234
// if there's still no clickId, generate a new one
234235
if (!clickId) {

apps/web/lib/tinybird/record-click.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { EU_COUNTRY_CODES } from "@dub/utils/src/constants/countries";
88
import { geolocation, ipAddress } from "@vercel/functions";
99
import { userAgent } from "next/server";
10+
import { clickCache } from "../api/links/click-cache";
1011
import { ExpandedLink, transformLink } from "../api/links/utils/transform-link";
1112
import {
1213
detectBot,
@@ -67,13 +68,11 @@ export async function recordClick({
6768

6869
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
6970

70-
const cacheKey = `recordClick:${domain}:${key}:${ip}`;
71-
7271
// by default, we deduplicate clicks for a domain + key pair from the same IP address – only record 1 click per hour
7372
// we only need to do these if skipRatelimit is not true (we skip it in /api/track/:path endpoints)
7473
if (!skipRatelimit) {
7574
// here, we check if the clickId is cached in Redis within the last hour
76-
const cachedClickId = await redis.get<string>(cacheKey);
75+
const cachedClickId = await clickCache.get({ domain, key, ip });
7776
if (cachedClickId) {
7877
return null;
7978
}
@@ -153,9 +152,7 @@ export async function recordClick({
153152
).then((res) => res.json()),
154153

155154
// cache the click ID in Redis for 1 hour
156-
redis.set(cacheKey, clickId, {
157-
ex: 60 * 60,
158-
}),
155+
clickCache.set({ domain, key, ip, clickId }),
159156

160157
// cache the click data for 5 mins
161158
// we're doing this because ingested click events are not available immediately in Tinybird

0 commit comments

Comments
 (0)