From 861cf544e730ea7b9346456574e75e38ad6587b8 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Tue, 8 Oct 2024 22:05:23 +0530 Subject: [PATCH 01/38] add webhookIds to create and update link --- apps/web/lib/api/links/create-link.ts | 20 ++++++++++++++++-- apps/web/lib/api/links/process-link.ts | 29 ++++++++++++++++++++++++++ apps/web/lib/api/links/update-link.ts | 13 ++++++++++++ apps/web/lib/zod/schemas/links.ts | 6 ++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/api/links/create-link.ts b/apps/web/lib/api/links/create-link.ts index 5c70998bd2..1d4dbaf448 100644 --- a/apps/web/lib/api/links/create-link.ts +++ b/apps/web/lib/api/links/create-link.ts @@ -24,7 +24,7 @@ export async function createLink(link: ProcessedLinkProps) { const { utm_source, utm_medium, utm_campaign, utm_term, utm_content } = getParamsFromURL(url); - const { tagId, tagIds, tagNames, ...rest } = link; + const { tagId, tagIds, tagNames, webhookIds, ...rest } = link; const response = await prisma.link.create({ data: { @@ -69,6 +69,18 @@ export async function createLink(link: ProcessedLinkProps) { }, }, }), + + // Webhooks + ...(webhookIds && + webhookIds.length > 0 && { + webhooks: { + createMany: { + data: webhookIds.map((webhookId) => ({ + webhookId, + })), + }, + }, + }), }, include: { tags: { @@ -91,8 +103,12 @@ export async function createLink(link: ProcessedLinkProps) { Promise.all([ // record link in Redis redis.hset(link.domain.toLowerCase(), { - [link.key.toLowerCase()]: await formatRedisLink(response), + [link.key.toLowerCase()]: await formatRedisLink({ + ...response, + ...(webhookIds && { webhookIds }), + }), }), + // record link in Tinybird recordLink({ link_id: response.id, diff --git a/apps/web/lib/api/links/process-link.ts b/apps/web/lib/api/links/process-link.ts index bd6dd1854a..d4eb6876c9 100644 --- a/apps/web/lib/api/links/process-link.ts +++ b/apps/web/lib/api/links/process-link.ts @@ -66,6 +66,7 @@ export async function processLink>({ tagNames, externalId, identifier, + webhookIds, } = payload; let expiresAt: string | Date | null | undefined = payload.expiresAt; @@ -400,6 +401,31 @@ export async function processLink>({ } } + // Validate the webhooks + if (webhookIds && webhookIds.length > 0) { + webhookIds = [...new Set(webhookIds)]; + + const webhooks = await prisma.webhook.findMany({ + select: { + id: true, + }, + where: { projectId: workspace?.id, id: { in: webhookIds } }, + }); + + if (webhooks.length !== webhookIds.length) { + const invalidWebhookIds = webhookIds.filter( + (webhookId) => + webhooks.find(({ id }) => webhookId === id) === undefined, + ); + + return { + link: payload, + error: "Invalid webhookIds detected: " + invalidWebhookIds.join(", "), + code: "unprocessable_entity", + }; + } + } + // remove polyfill attributes from payload delete payload["shortLink"]; delete payload["qrCode"]; @@ -423,6 +449,9 @@ export async function processLink>({ ...(userId && { userId, }), + ...(webhookIds && { + webhookIds, + }), }, error: null, }; diff --git a/apps/web/lib/api/links/update-link.ts b/apps/web/lib/api/links/update-link.ts index aa775c401a..a692e631d2 100644 --- a/apps/web/lib/api/links/update-link.ts +++ b/apps/web/lib/api/links/update-link.ts @@ -49,6 +49,7 @@ export async function updateLink({ tagId, tagIds, tagNames, + webhookIds, ...rest } = updatedLink; @@ -108,6 +109,18 @@ export async function updateLink({ })), }, }), + + // Webhooks + ...(webhookIds && { + webhooks: { + deleteMany: {}, + createMany: { + data: webhookIds.map((webhookId) => ({ + webhookId, + })), + }, + }, + }), }, include: { tags: { diff --git a/apps/web/lib/zod/schemas/links.ts b/apps/web/lib/zod/schemas/links.ts index 953f045ac6..ab33d17be0 100644 --- a/apps/web/lib/zod/schemas/links.ts +++ b/apps/web/lib/zod/schemas/links.ts @@ -317,6 +317,12 @@ export const createLinkBodySchema = z.object({ .describe( "The referral tag of the short link. If set, this will populate or override the `ref` query parameter in the destination URL.", ), + webhookIds: z + .array(z.string()) + .nullish() + .describe( + "An array of webhook IDs to trigger when the link is clicked. These webhooks will receive click event data.", + ), }); export const updateLinkBodySchema = createLinkBodySchema.partial().optional(); From fbcf855fd4c3a3a062408e9debc421b2c77e70fd Mon Sep 17 00:00:00 2001 From: Kiran K Date: Tue, 8 Oct 2024 22:05:29 +0530 Subject: [PATCH 02/38] add Copy Webhook ID button --- apps/web/ui/webhooks/webhook-header.tsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/web/ui/webhooks/webhook-header.tsx b/apps/web/ui/webhooks/webhook-header.tsx index 98c2ef61fb..6e89521489 100644 --- a/apps/web/ui/webhooks/webhook-header.tsx +++ b/apps/web/ui/webhooks/webhook-header.tsx @@ -6,6 +6,8 @@ import { WebhookProps } from "@/lib/types"; import { ThreeDots } from "@/ui/shared/icons"; import { Button, + CircleCheck, + Copy, MaxWidthWrapper, Popover, TabSelect, @@ -16,6 +18,7 @@ import { ChevronLeft, Send, Trash } from "lucide-react"; import Link from "next/link"; import { notFound, useRouter, useSelectedLayoutSegment } from "next/navigation"; import { useState } from "react"; +import { toast } from "sonner"; import useSWR from "swr"; import { useDeleteWebhookModal } from "../modals/delete-webhook-modal"; import { useSendTestWebhookModal } from "../modals/send-test-webhook-modal"; @@ -23,6 +26,7 @@ import { useSendTestWebhookModal } from "../modals/send-test-webhook-modal"; export default function WebhookHeader({ webhookId }: { webhookId: string }) { const router = useRouter(); const { slug, id: workspaceId, role } = useWorkspace(); + const [copiedWebhookId, setCopiedWebhookId] = useState(false); const selectedLayoutSegment = useSelectedLayoutSegment(); const page = selectedLayoutSegment === null ? "" : selectedLayoutSegment; @@ -52,6 +56,13 @@ export default function WebhookHeader({ webhookId }: { webhookId: string }) { return notFound(); } + const copyWebhookId = () => { + navigator.clipboard.writeText(webhookId); + setCopiedWebhookId(true); + toast.success("Webhook ID copied!"); + setTimeout(() => setCopiedWebhookId(false), 3000); + }; + return ( <> @@ -108,6 +119,20 @@ export default function WebhookHeader({ webhookId }: { webhookId: string }) { }} /> +