From a717d9e69431207c74d230cb916f50ecf0d6fd86 Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Wed, 2 Sep 2026 19:07:21 -0300 Subject: [PATCH 1/3] record fake click for demo workspace --- apps/web/.env.example | 2 + apps/web/app/(ee)/api/demo/click/route.ts | 107 +++++++++++++++++++++ apps/web/lib/tinybird/record-fake-click.ts | 43 ++++++++- 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/(ee)/api/demo/click/route.ts diff --git a/apps/web/.env.example b/apps/web/.env.example index 4ea84d0dc49..e7883eb7b3b 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -8,6 +8,8 @@ NEXTAUTH_URL=http://localhost:8888 # (only needed for localhost) # Secret for Vercel cron jobs + sync-embeddings CRON_SECRET= +# Shared with the LoopWork demo cron. Mints geo-accurate clicks via POST /api/demo/click +DEMO_CLICK_SECRET= # Encryption key (AES-256-GCM) for encrypting sensitive data in the database ENCRYPTION_KEY= # Email unsubscribe token secret (optional, falls back to NEXTAUTH_SECRET) diff --git a/apps/web/app/(ee)/api/demo/click/route.ts b/apps/web/app/(ee)/api/demo/click/route.ts new file mode 100644 index 00000000000..d35f90f1678 --- /dev/null +++ b/apps/web/app/(ee)/api/demo/click/route.ts @@ -0,0 +1,107 @@ +import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors"; +import { parseRequestBody } from "@/lib/api/utils"; +import { prefixWorkspaceId } from "@/lib/api/workspaces/workspace-id"; +import { withAxiom } from "@/lib/axiom/server"; +import { getLinkWithPartner } from "@/lib/planetscale/get-link-with-partner"; +import { recordFakeClick } from "@/lib/tinybird/record-fake-click"; +import { + COUNTRY_CODES, + DEMO_PROGRAM_ID, + DEMO_WORKSPACE_ID, + getDomainWithoutWWW, +} from "@dub/utils"; +import { NextResponse } from "next/server"; +import * as z from "zod/v4"; + +const demoClickSchema = z.object({ + domain: z.preprocess( + (val) => getDomainWithoutWWW(val as string), + z.string({ error: "domain is required." }), + ), + key: z.string({ error: "key is required." }), + country: z.enum(COUNTRY_CODES), + region: z.string().nullish(), + city: z.string().nullish(), + continent: z.string().nullish(), + referrer: z.string().nullish(), + userAgent: z.string().nullish(), +}); + +function verifyDemoClickSecret(req: Request) { + const secret = process.env.DEMO_CLICK_SECRET; + const authorization = req.headers.get("authorization"); + + if (!secret || authorization !== `Bearer ${secret}`) { + throw new DubApiError({ + code: "unauthorized", + message: "Invalid or missing DEMO_CLICK_SECRET.", + }); + } +} + +// POST /api/demo/click – mint a geo-accurate click for the LoopWork demo workspace only +export const POST = withAxiom(async (req) => { + try { + verifyDemoClickSecret(req); + + const { + domain, + key, + country, + region, + city, + continent, + referrer, + userAgent, + } = demoClickSchema.parse(await parseRequestBody(req)); + + const link = await getLinkWithPartner({ domain, key }); + + if (!link) { + throw new DubApiError({ + code: "not_found", + message: `Link not found for domain: ${domain} and key: ${key}.`, + }); + } + + if ( + prefixWorkspaceId(link.projectId) !== prefixWorkspaceId(DEMO_WORKSPACE_ID) + ) { + throw new DubApiError({ + code: "forbidden", + message: "This endpoint can only record clicks for the demo workspace.", + }); + } + + if (link.programId && link.programId !== DEMO_PROGRAM_ID) { + throw new DubApiError({ + code: "forbidden", + message: "This endpoint can only record clicks for the demo program.", + }); + } + + const clickEvent = await recordFakeClick({ + link: { + id: link.id, + url: link.url, + domain: link.domain, + key: link.key, + projectId: link.projectId, + programId: link.programId, + partnerId: link.partnerId, + }, + customer: { + country, + region, + city, + continent, + }, + referrer, + userAgent, + }); + + return NextResponse.json({ clickId: clickEvent.click_id }); + } catch (error) { + return handleAndReturnErrorResponse(error); + } +}); diff --git a/apps/web/lib/tinybird/record-fake-click.ts b/apps/web/lib/tinybird/record-fake-click.ts index 76464382dd6..9c4a75bebee 100644 --- a/apps/web/lib/tinybird/record-fake-click.ts +++ b/apps/web/lib/tinybird/record-fake-click.ts @@ -1,4 +1,4 @@ -import { nanoid } from "@dub/utils"; +import { COUNTRIES_TO_CONTINENTS, nanoid } from "@dub/utils"; import { Link } from "@prisma/client"; import { clickEventSchemaTB } from "../zod/schemas/clicks"; import { recordClick } from "./record-click"; @@ -19,28 +19,58 @@ function toSafeHeaderValue(value: string | null | undefined) { return value; } +const DEFAULT_USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"; + // TODO: // Use this in other places where we need to record a fake click event (Eg: import-customers) export async function recordFakeClick({ link, customer, timestamp, + referrer, + userAgent, }: { - link: Pick; + link: Pick & { + programId?: string | null; + partnerId?: string | null; + }; customer?: { country?: string | null; region?: string | null; continent?: string | null; + city?: string | null; + latitude?: string | null; + longitude?: string | null; }; timestamp?: string | number; + referrer?: string | null; + userAgent?: string | null; }) { + const country = toSafeHeaderValue(customer?.country) || "US"; + const continent = + toSafeHeaderValue(customer?.continent) || + COUNTRIES_TO_CONTINENTS[country] || + "NA"; + const dummyRequest = new Request(link.url, { headers: new Headers({ - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", + "user-agent": userAgent || DEFAULT_USER_AGENT, "x-forwarded-for": "127.0.0.1", - "x-vercel-ip-country": toSafeHeaderValue(customer?.country) || "US", + "x-vercel-ip-country": country, "x-vercel-ip-country-region": toSafeHeaderValue(customer?.region) || "CA", - "x-vercel-ip-continent": toSafeHeaderValue(customer?.continent) || "NA", + "x-vercel-ip-continent": continent, + ...(customer?.city && { + "x-vercel-ip-city": toSafeHeaderValue(customer.city) || "Unknown", + }), + ...(customer?.latitude && { + "x-vercel-ip-latitude": + toSafeHeaderValue(customer.latitude) || "Unknown", + }), + ...(customer?.longitude && { + "x-vercel-ip-longitude": + toSafeHeaderValue(customer.longitude) || "Unknown", + }), }), }); @@ -52,8 +82,11 @@ export async function recordFakeClick({ domain: link.domain, key: link.key, url: link.url, + programId: link.programId ?? undefined, + partnerId: link.partnerId ?? undefined, skipRatelimit: true, shouldCacheClickId: true, + ...(referrer && { referrer }), ...(timestamp && { timestamp: new Date(timestamp).toISOString() }), }); From 6255a55a9c6aed7a1259041cd621e36f202cf178 Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Wed, 2 Sep 2026 19:09:45 -0300 Subject: [PATCH 2/3] update record-fake-click --- apps/web/lib/tinybird/record-fake-click.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/lib/tinybird/record-fake-click.ts b/apps/web/lib/tinybird/record-fake-click.ts index 9c4a75bebee..7461969f92b 100644 --- a/apps/web/lib/tinybird/record-fake-click.ts +++ b/apps/web/lib/tinybird/record-fake-click.ts @@ -19,9 +19,6 @@ function toSafeHeaderValue(value: string | null | undefined) { return value; } -const DEFAULT_USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"; - // TODO: // Use this in other places where we need to record a fake click event (Eg: import-customers) export async function recordFakeClick({ @@ -55,7 +52,8 @@ export async function recordFakeClick({ const dummyRequest = new Request(link.url, { headers: new Headers({ - "user-agent": userAgent || DEFAULT_USER_AGENT, + "user-agent": + userAgent || "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "x-forwarded-for": "127.0.0.1", "x-vercel-ip-country": country, "x-vercel-ip-country-region": toSafeHeaderValue(customer?.region) || "CA", From 4b23d564c76ddc2fe0ab0b5cd0b49895733ed2fa Mon Sep 17 00:00:00 2001 From: Pedro Ladeira Date: Wed, 2 Sep 2026 20:25:09 -0300 Subject: [PATCH 3/3] add workspace_id, domain, key and country --- apps/web/lib/api/commissions/create-manual-commissions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/lib/api/commissions/create-manual-commissions.ts b/apps/web/lib/api/commissions/create-manual-commissions.ts index 6998a52aa90..faccb29afab 100644 --- a/apps/web/lib/api/commissions/create-manual-commissions.ts +++ b/apps/web/lib/api/commissions/create-manual-commissions.ts @@ -447,9 +447,13 @@ async function recordEvents(args: RecordEventsArgs) { timestamp: clickedAt.toISOString(), identity_hash: targetCustomer.externalId || targetCustomer.id, click_id: clickId, + workspace_id: workspace.id, link_id: targetLink.id, + domain: targetLink.domain, + key: targetLink.key, url: targetLink.url, ip: "127.0.0.1", + country: targetCustomer.country || "Unknown", continent: targetCustomer.country ? COUNTRIES_TO_CONTINENTS[targetCustomer.country.toUpperCase()] || "" : "",