Skip to content

Add webhook testing functionality to dashboard #7173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/dashboard/src/@/api/insight/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ export async function deleteWebhook(
};
}
}
// biome-ignore lint/correctness/noUnusedVariables: will be used in the next PR
async function testWebhook(

export async function testWebhook(
payload: TestWebhookPayload,
clientId: string,
): Promise<TestWebhookResponse> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ColumnDef } from "@tanstack/react-table";
import { TWTable } from "components/shared/TWTable";
import { format } from "date-fns";
import { TrashIcon } from "lucide-react";
import { PlayIcon, TrashIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { useTestWebhook } from "../hooks/useTestWebhook";
import { RelativeTime } from "./RelativeTime";

function getEventType(filters: WebhookFilters): string {
Expand All @@ -31,7 +32,7 @@ interface WebhooksTableProps {

export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) {
const [isDeleting, setIsDeleting] = useState<Record<string, boolean>>({});
// const { testWebhookEndpoint, isTestingMap } = useTestWebhook(clientId);
const { testWebhookEndpoint, isTestingMap } = useTestWebhook(clientId);
const router = useDashboardRouter();

const handleDeleteWebhook = async (webhookId: string) => {
Expand All @@ -55,6 +56,22 @@ export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) {
}
};

const handleTestWebhook = async (webhook: WebhookResponse) => {
const filterType = getEventType(webhook.filters);
if (filterType === "Unknown") {
toast.error("Cannot test webhook", {
description:
"This webhook does not have a valid event type (event or transaction).",
});
return;
}
await testWebhookEndpoint(
webhook.webhook_url,
filterType.toLowerCase() as "event" | "transaction",
webhook.id,
);
};

const columns: ColumnDef<WebhookResponse>[] = [
{
accessorKey: "name",
Expand Down Expand Up @@ -129,12 +146,26 @@ export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) {
},
{
id: "actions",
header: "Actions",
header: () => <div className="flex w-full justify-end pr-2">Actions</div>,
cell: ({ row }) => {
const webhook = row.original;

return (
<div className="flex justify-end gap-2">
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
disabled={isTestingMap[webhook.id] || isDeleting[webhook.id]}
onClick={() => handleTestWebhook(webhook)}
aria-label={`Test webhook ${webhook.name}`}
>
{isTestingMap[webhook.id] ? (
<Spinner className="h-4 w-4" />
) : (
<PlayIcon className="h-4 w-4" />
)}
</Button>
<Button
size="icon"
variant="outline"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { testWebhook } from "@/api/insight/webhooks";
import { useState } from "react";
import { toast } from "sonner";

type TestResult = {
status: "success" | "error";
timestamp: number;
};

export function useTestWebhook(clientId: string) {
const [isTestingMap, setIsTestingMap] = useState<Record<string, boolean>>({});
const [testResults, setTestResults] = useState<Record<string, TestResult>>(
{},
);

const testWebhookEndpoint = async (
webhookUrl: string,
type: "event" | "transaction",
id?: string,
) => {
if (!webhookUrl) {
toast.error("Cannot test webhook", {
description: "Webhook URL is required",
});
return false;
}

const uniqueId = id || webhookUrl;

if (isTestingMap[uniqueId]) return false;

try {
setIsTestingMap((prev) => ({ ...prev, [uniqueId]: true }));
setTestResults((prev) => {
const newResults = { ...prev };
delete newResults[uniqueId];
return newResults;
});

const result = await testWebhook(
{ webhook_url: webhookUrl, type },
clientId,
);

if (result.success) {
setTestResults((prev) => ({
...prev,
[uniqueId]: {
status: "success",
timestamp: Date.now(),
},
}));

toast.success("Test event sent successfully", {
description:
"Check your webhook endpoint to verify the delivery. The test event is signed with a secret key.",
});
return true;
} else {
setTestResults((prev) => ({
...prev,
[uniqueId]: {
status: "error",
timestamp: Date.now(),
},
}));

toast.error("Failed to send test event", {
description:
"The server reported a failure in sending the test event.",
});
return false;
}
} catch (error) {
console.error("Error testing webhook:", error);

setTestResults((prev) => ({
...prev,
[uniqueId]: {
status: "error",
timestamp: Date.now(),
},
}));

const errorMessage =
error instanceof Error ? error.message : "An unexpected error occurred";
const isFailedSendError = errorMessage.includes(
"Failed to send test event",
);

toast.error(
isFailedSendError ? "Unable to test webhook" : "Failed to test webhook",
{
description: isFailedSendError
? "We couldn't send a test request to your webhook endpoint. This might be due to network issues or the endpoint being unavailable. Please verify your webhook URL and try again later."
: errorMessage,
duration: 10000,
},
);
return false;
} finally {
setIsTestingMap((prev) => ({ ...prev, [uniqueId]: false }));
}
};

const isRecentResult = (uniqueId: string) => {
const result = testResults[uniqueId];
if (!result) return false;

return Date.now() - result.timestamp < 5000;
};

return {
testWebhookEndpoint,
isTestingMap,
testResults,
isRecentResult,
};
}
Loading