Skip to content

Commit a4f8fe1

Browse files
committed
Add webhook testing functionality to dashboard
1 parent a63703d commit a4f8fe1

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/WebhooksTable.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import { useDashboardRouter } from "@/lib/DashboardRouter";
1313
import type { ColumnDef } from "@tanstack/react-table";
1414
import { TWTable } from "components/shared/TWTable";
1515
import { format } from "date-fns";
16-
import { TrashIcon } from "lucide-react";
16+
import { PlayIcon, TrashIcon } from "lucide-react";
1717
import { useMemo, useState } from "react";
1818
import { toast } from "sonner";
19+
import { useTestWebhook } from "../hooks/useTestWebhook";
1920
import { RelativeTime } from "./RelativeTime";
2021

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

3233
export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) {
3334
const [isDeleting, setIsDeleting] = useState<Record<string, boolean>>({});
34-
// const { testWebhookEndpoint, isTestingMap } = useTestWebhook(clientId);
35+
const { testWebhookEndpoint, isTestingMap } = useTestWebhook(clientId);
3536
const router = useDashboardRouter();
3637

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

59+
const handleTestWebhook = async (webhook: WebhookResponse) => {
60+
const filterType = getEventType(webhook.filters);
61+
if (filterType === "Unknown") {
62+
toast.error("Cannot test webhook", {
63+
description:
64+
"This webhook does not have a valid event type (event or transaction).",
65+
});
66+
return;
67+
}
68+
await testWebhookEndpoint(
69+
webhook.webhook_url,
70+
filterType.toLowerCase() as "event" | "transaction",
71+
webhook.id,
72+
);
73+
};
74+
5875
const columns: ColumnDef<WebhookResponse>[] = [
5976
{
6077
accessorKey: "name",
@@ -129,12 +146,26 @@ export function WebhooksTable({ webhooks, clientId }: WebhooksTableProps) {
129146
},
130147
{
131148
id: "actions",
132-
header: "Actions",
149+
header: () => <div className="flex w-full justify-end pr-2">Actions</div>,
133150
cell: ({ row }) => {
134151
const webhook = row.original;
135152

136153
return (
137-
<div className="flex justify-end gap-2">
154+
<div className="flex items-center justify-end gap-2">
155+
<Button
156+
variant="outline"
157+
size="icon"
158+
className="h-8 w-8"
159+
disabled={isTestingMap[webhook.id] || isDeleting[webhook.id]}
160+
onClick={() => handleTestWebhook(webhook)}
161+
aria-label={`Test webhook ${webhook.name}`}
162+
>
163+
{isTestingMap[webhook.id] ? (
164+
<Spinner className="h-4 w-4" />
165+
) : (
166+
<PlayIcon className="h-4 w-4" />
167+
)}
168+
</Button>
138169
<Button
139170
size="icon"
140171
variant="outline"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { testWebhook } from "@/api/insight/webhooks";
2+
import { useState } from "react";
3+
import { toast } from "sonner";
4+
5+
type TestResult = {
6+
status: "success" | "error";
7+
timestamp: number;
8+
};
9+
10+
export function useTestWebhook(clientId: string) {
11+
const [isTestingMap, setIsTestingMap] = useState<Record<string, boolean>>({});
12+
const [testResults, setTestResults] = useState<Record<string, TestResult>>(
13+
{},
14+
);
15+
16+
const testWebhookEndpoint = async (
17+
webhookUrl: string,
18+
type: "event" | "transaction",
19+
id?: string,
20+
) => {
21+
if (!webhookUrl) {
22+
toast.error("Cannot test webhook", {
23+
description: "Webhook URL is required",
24+
});
25+
return false;
26+
}
27+
28+
const uniqueId = id || webhookUrl;
29+
30+
if (isTestingMap[uniqueId]) return false;
31+
32+
try {
33+
setIsTestingMap((prev) => ({ ...prev, [uniqueId]: true }));
34+
setTestResults((prev) => {
35+
const newResults = { ...prev };
36+
delete newResults[uniqueId];
37+
return newResults;
38+
});
39+
40+
const result = await testWebhook(
41+
{ webhook_url: webhookUrl, type },
42+
clientId,
43+
);
44+
45+
if (result.success) {
46+
setTestResults((prev) => ({
47+
...prev,
48+
[uniqueId]: {
49+
status: "success",
50+
timestamp: Date.now(),
51+
},
52+
}));
53+
54+
toast.success("Test event sent successfully", {
55+
description:
56+
"Check your webhook endpoint to verify the delivery. The test event is signed with a secret key.",
57+
});
58+
return true;
59+
} else {
60+
setTestResults((prev) => ({
61+
...prev,
62+
[uniqueId]: {
63+
status: "error",
64+
timestamp: Date.now(),
65+
},
66+
}));
67+
68+
toast.error("Failed to send test event", {
69+
description:
70+
"The server reported a failure in sending the test event.",
71+
});
72+
return false;
73+
}
74+
} catch (error) {
75+
console.error("Error testing webhook:", error);
76+
77+
setTestResults((prev) => ({
78+
...prev,
79+
[uniqueId]: {
80+
status: "error",
81+
timestamp: Date.now(),
82+
},
83+
}));
84+
85+
const errorMessage =
86+
error instanceof Error ? error.message : "An unexpected error occurred";
87+
const isFailedSendError = errorMessage.includes(
88+
"Failed to send test event",
89+
);
90+
91+
toast.error(
92+
isFailedSendError ? "Unable to test webhook" : "Failed to test webhook",
93+
{
94+
description: isFailedSendError
95+
? "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."
96+
: errorMessage,
97+
duration: 10000,
98+
},
99+
);
100+
return false;
101+
} finally {
102+
setIsTestingMap((prev) => ({ ...prev, [uniqueId]: false }));
103+
}
104+
};
105+
106+
const isRecentResult = (uniqueId: string) => {
107+
const result = testResults[uniqueId];
108+
if (!result) return false;
109+
110+
return Date.now() - result.timestamp < 5000;
111+
};
112+
113+
return {
114+
testWebhookEndpoint,
115+
isTestingMap,
116+
testResults,
117+
isRecentResult,
118+
};
119+
}

0 commit comments

Comments
 (0)