Skip to content

Commit

Permalink
Merge pull request #1896 from dubinc/webhook-disable
Browse files Browse the repository at this point in the history
Refactor webhook failure handling and notifications
  • Loading branch information
steven-tey authored Jan 15, 2025
2 parents d5b8c8c + 7979e9c commit 3c4a7c2
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 14 deletions.
1 change: 1 addition & 0 deletions apps/web/app/api/webhooks/[webhookId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const PATCH = withWorkspace(
})),
},
}),
disabledAt: null,
},
select: {
id: true,
Expand Down
6 changes: 3 additions & 3 deletions apps/web/emails/webhook-disabled.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WEBHOOK_FAILURE_NOTIFY_THRESHOLD } from "@/lib/webhook/constants";
import { WEBHOOK_FAILURE_DISABLE_THRESHOLD } from "@/lib/webhook/constants";
import { DUB_WORDMARK } from "@dub/utils";
import {
Body,
Expand Down Expand Up @@ -56,8 +56,8 @@ export default function WebhookDisabled({
</Heading>
<Text className="text-sm leading-6 text-black">
Your webhook <strong>{webhook.url}</strong> has failed to deliver
successfully {WEBHOOK_FAILURE_NOTIFY_THRESHOLD} times in a row and
has been deactivated to prevent further issues.
successfully {WEBHOOK_FAILURE_DISABLE_THRESHOLD} times in a row
and has been deactivated to prevent further issues.
</Text>
<Text className="text-sm leading-6 text-black">
Please review the webhook details and update the URL if necessary
Expand Down
78 changes: 78 additions & 0 deletions apps/web/emails/webhook-failed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { DUB_WORDMARK } from "@dub/utils";
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import Footer from "./components/footer";

export default function WebhookFailed({
email = "[email protected]",
workspace = {
name: "Acme, Inc",
slug: "acme",
},
webhook = {
id: "wh_tYedrqsWgNJxUwQOaAnupcUJ1",
url: "https://example.com/webhook",
},
}: {
email: string;
workspace: {
name: string;
slug: string;
};
webhook: {
id: string;
url: string;
};
}) {
return (
<Html>
<Head />
<Preview>Webhook is failing to deliver</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 max-w-[500px] rounded border border-solid border-gray-200 px-10 py-5">
<Section className="mt-8">
<Img
src={DUB_WORDMARK}
height="40"
alt="Dub.co"
className="mx-auto my-0"
/>
</Section>
<Heading className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
Webhook is failing to deliver
</Heading>
<Text className="text-sm leading-6 text-black">
Your webhook <strong>{webhook.url}</strong> is encountering
delivery failures and will be disabled if it continues to fail.
</Text>
<Text className="text-sm leading-6 text-black">
Please review the webhook details and update the URL if necessary
to restore functionality.
</Text>
<Section className="mb-8 mt-4 text-center">
<Link
className="rounded-full bg-black px-6 py-3 text-center text-[12px] font-semibold text-white no-underline"
href={`https://app.dub.co/${workspace.slug}/settings/webhooks/${webhook.id}/edit`}
>
Edit Webhook
</Link>
</Section>
<Footer email={email} />
</Container>
</Body>
</Tailwind>
</Html>
);
}
4 changes: 1 addition & 3 deletions apps/web/lib/actions/enable-disable-webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ export const enableOrDisableWebhook = authActionClient
const { workspace } = ctx;
const { webhookId } = parsedInput;

const canAccessWebhook = !["free", "pro"].includes(workspace.plan);

if (!canAccessWebhook) {
if (["free", "pro"].includes(workspace.plan)) {
throw new Error("You must upgrade your plan to enable webhooks.");
}

Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/webhook/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export const WEBHOOK_TRIGGER_DESCRIPTIONS = {
"sale.created": "Sale created",
} as const;

export const WEBHOOK_FAILURE_NOTIFY_THRESHOLD = 20;
export const WEBHOOK_FAILURE_NOTIFY_THRESHOLDS = [5, 10, 15] as const;
export const WEBHOOK_FAILURE_DISABLE_THRESHOLD = 20 as const;
72 changes: 65 additions & 7 deletions apps/web/lib/webhook/failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { prisma } from "@dub/prisma";
import { Webhook } from "@dub/prisma/client";
import { sendEmail } from "emails";
import WebhookDisabled from "emails/webhook-disabled";
import WebhookFailed from "emails/webhook-failed";
import { webhookCache } from "./cache";
import { WEBHOOK_FAILURE_NOTIFY_THRESHOLD } from "./constants";
import {
WEBHOOK_FAILURE_DISABLE_THRESHOLD,
WEBHOOK_FAILURE_NOTIFY_THRESHOLDS,
} from "./constants";
import { toggleWebhooksForWorkspace } from "./update-webhook";

export const handleWebhookFailure = async (webhookId: string) => {
const webhook = await prisma.webhook.update({
where: { id: webhookId },
where: {
id: webhookId,
},
data: {
consecutiveFailures: { increment: 1 },
lastFailedAt: new Date(),
Expand All @@ -29,10 +35,16 @@ export const handleWebhookFailure = async (webhookId: string) => {
return;
}

const failureThresholdReached =
webhook.consecutiveFailures >= WEBHOOK_FAILURE_NOTIFY_THRESHOLD;
if (
WEBHOOK_FAILURE_NOTIFY_THRESHOLDS.includes(
webhook.consecutiveFailures as any,
)
) {
await notifyWebhookFailure(webhook);
return;
}

if (failureThresholdReached) {
if (webhook.consecutiveFailures >= WEBHOOK_FAILURE_DISABLE_THRESHOLD) {
// Disable the webhook
const updatedWebhook = await prisma.webhook.update({
where: { id: webhookId },
Expand All @@ -43,7 +55,7 @@ export const handleWebhookFailure = async (webhookId: string) => {

await Promise.allSettled([
// Notify the user
sendFailureNotification(updatedWebhook),
notifyWebhookDisabled(updatedWebhook),

// Update the webhook cache
webhookCache.set(updatedWebhook),
Expand All @@ -66,7 +78,53 @@ export const resetWebhookFailureCount = async (webhookId: string) => {
});
};

const sendFailureNotification = async (
// Send email to workspace owners when the webhook is failing to deliver
const notifyWebhookFailure = async (
webhook: Pick<Webhook, "id" | "url" | "projectId">,
) => {
const workspaceOwners = await prisma.projectUsers.findFirst({
where: { projectId: webhook.projectId, role: "owner" },
select: {
project: {
select: {
name: true,
slug: true,
},
},
user: {
select: {
email: true,
},
},
},
});

if (!workspaceOwners) {
return;
}

const email = workspaceOwners.user.email!;
const workspace = workspaceOwners.project;

sendEmail({
subject: "Webhook is failing to deliver",
email,
react: WebhookFailed({
email,
workspace: {
name: workspace.name,
slug: workspace.slug,
},
webhook: {
id: webhook.id,
url: webhook.url,
},
}),
});
};

// Send email to the workspace owners when the webhook has been disabled
const notifyWebhookDisabled = async (
webhook: Pick<Webhook, "id" | "url" | "projectId">,
) => {
const workspaceOwners = await prisma.projectUsers.findFirst({
Expand Down

0 comments on commit 3c4a7c2

Please sign in to comment.