Skip to content

Commit

Permalink
if a workspace exceeds the number of clicks, we should stop sending c…
Browse files Browse the repository at this point in the history
…lick webhooks
  • Loading branch information
devkiran committed Oct 14, 2024
1 parent fdba44b commit e681d53
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 39 deletions.
1 change: 1 addition & 0 deletions apps/web/app/api/track/click/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const POST = async (req: Request) => {
linkId: link.id,
url: link.url,
skipRatelimit: true,
workspaceId: workspace.id,
}),
);
}
Expand Down
30 changes: 17 additions & 13 deletions apps/web/app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DubApiError } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { addWebhook } from "@/lib/webhook/api";
import { addWebhook, updateWebhookStatusForWorkspace } from "@/lib/webhook/api";
import { transformWebhook } from "@/lib/webhook/transform";
import { createWebhookSchema } from "@/lib/zod/schemas/webhooks";
import { waitUntil } from "@vercel/functions";
Expand Down Expand Up @@ -97,20 +97,24 @@ export const POST = withWorkspace(
});

waitUntil(
sendEmail({
email: session.user.email,
subject: "New webhook added",
react: WebhookAdded({
Promise.allSettled([
updateWebhookStatusForWorkspace({ workspace }),

sendEmail({
email: session.user.email,
workspace: {
name: workspace.name,
slug: workspace.slug,
},
webhook: {
name,
},
subject: "New webhook added",
react: WebhookAdded({
email: session.user.email,
workspace: {
name: workspace.name,
slug: workspace.slug,
},
webhook: {
name,
},
}),
}),
}),
]),
);

return NextResponse.json(transformWebhook(webhook), { status: 201 });
Expand Down
8 changes: 8 additions & 0 deletions apps/web/lib/middleware/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default async function LinkMiddleware(
expiredUrl,
doIndex,
webhookIds,
projectId: workspaceId,
} = link;

// by default, we only index default dub domain links (e.g. dub.sh)
Expand Down Expand Up @@ -181,6 +182,7 @@ export default async function LinkMiddleware(
clickId,
url,
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -225,6 +227,7 @@ export default async function LinkMiddleware(
clickId,
url,
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -258,6 +261,7 @@ export default async function LinkMiddleware(
clickId,
url,
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -306,6 +310,7 @@ export default async function LinkMiddleware(
clickId,
url: ios,
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -335,6 +340,7 @@ export default async function LinkMiddleware(
clickId,
url: android,
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -364,6 +370,7 @@ export default async function LinkMiddleware(
clickId,
url: geo[country],
webhookIds,
workspaceId,
}),
);

Expand Down Expand Up @@ -393,6 +400,7 @@ export default async function LinkMiddleware(
clickId,
url,
webhookIds,
workspaceId,
}),
);

Expand Down
94 changes: 68 additions & 26 deletions apps/web/lib/tinybird/record-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getIdentityHash,
} from "../middleware/utils";
import { conn } from "../planetscale";
import { WorkspaceProps } from "../types";
import { redis } from "../upstash";
import { webhookCache } from "../webhook/cache";
import { sendWebhooks } from "../webhook/qstash";
Expand All @@ -30,13 +31,15 @@ export async function recordClick({
url,
webhookIds,
skipRatelimit,
workspaceId,
}: {
req: Request;
linkId: string;
clickId: string;
url?: string;
webhookIds?: string[];
skipRatelimit?: boolean;
workspaceId: string | undefined;
}) {
const searchParams = new URL(req.url).searchParams;

Expand Down Expand Up @@ -119,7 +122,9 @@ export async function recordClick({
referer_url: referer || "(direct)",
};

await Promise.allSettled([
const hasWebhooks = webhookIds && webhookIds.length > 0;

const [, , , , workspaceRows] = await Promise.all([
fetch(
`${process.env.TINYBIRD_API_URL}/v0/events?name=dub_click_events&wait=true`,
{
Expand All @@ -146,37 +151,74 @@ export async function recordClick({
// and then we have a cron that will reset it at the start of new billing cycle
url &&
conn.execute(
"UPDATE Project p JOIN Link l ON p.id = l.projectId SET p.usage = p.usage + 1 WHERE l.id = ?",
"UPDATE Project p JOIN Link l ON p.id = l.projectId SET p.usage = p.usage + 1 WHERE l.id = ?;",
[linkId],
),

// fetch the workspace usage for the workspace
workspaceId && hasWebhooks
? conn.execute(
"SELECT usage, usageLimit FROM Project WHERE id = ? LIMIT 1",
[workspaceId],
)
: null,
]);

// Send webhook events if link has webhooks enabled
if (webhookIds && webhookIds.length > 0) {
const webhooks = await webhookCache.mget(webhookIds);
const workspace =
workspaceRows && workspaceRows.rows.length > 0
? (workspaceRows.rows[0] as Pick<WorkspaceProps, "usage" | "usageLimit">)
: null;

const hasExceededUsageLimit =
workspace && workspace.usage >= workspace.usageLimit;

const linkWebhooks = webhooks.filter(
(webhook) =>
webhook.disabledAt === null &&
webhook.triggers &&
Array.isArray(webhook.triggers) &&
webhook.triggers.includes("link.clicked"),
// Send webhook events if link has webhooks enabled and the workspace usage has not exceeded the limit
if (hasWebhooks && !hasExceededUsageLimit) {
await sendLinkClickWebhooks({ webhookIds, linkId, clickData });
}
}

async function sendLinkClickWebhooks({
webhookIds,
linkId,
clickData,
}: {
webhookIds: string[];
linkId: string;
clickData: any;
}) {
const webhooks = await webhookCache.mget(webhookIds);

// Couldn't find webhooks in the cache
// TODO: Should we look them up in the database?
if (!webhooks || webhooks.length === 0) {
return;
}

const activeLinkWebhooks = webhooks.filter((webhook) => {
return (
!webhook.disabledAt &&
webhook.triggers &&
Array.isArray(webhook.triggers) &&
webhook.triggers.includes("link.clicked")
);
});

if (linkWebhooks.length > 0) {
const link = await conn
.execute("SELECT * FROM Link WHERE id = ?", [linkId])
.then((res) => res.rows[0]);

await sendWebhooks({
trigger: "link.clicked",
webhooks: linkWebhooks,
// @ts-ignore – bot & qr should be boolean
data: transformClickEventData({
...clickData,
link: transformLink(link as LinkWithTags),
}),
});
}
if (activeLinkWebhooks.length === 0) {
return;
}

const link = await conn
.execute("SELECT * FROM Link WHERE id = ?", [linkId])
.then((res) => res.rows[0]);

await sendWebhooks({
trigger: "link.clicked",
webhooks: activeLinkWebhooks,
// @ts-ignore – bot & qr should be boolean
data: transformClickEventData({
...clickData,
link: transformLink(link as LinkWithTags),
}),
});
}

0 comments on commit e681d53

Please sign in to comment.