Skip to content

Commit 09714d6

Browse files
committed
feat: dashboard analytics for webhooks
1 parent 7353f8b commit 09714d6

File tree

7 files changed

+69
-126
lines changed

7 files changed

+69
-126
lines changed

apps/dashboard/src/@/api/analytics.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ async function fetchAnalytics(
2424
input: string | URL,
2525
init?: RequestInit,
2626
): Promise<Response> {
27-
// const token = await getAuthToken();
28-
// DEBUG
29-
const token = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIweDAxNjc1N2REZjJBYjZhOTk4YTQ3MjlBODBhMDkxMzA4ZDkwNTlFMTciLCJzdWIiOiIweGE1Qjg0OTJEODIyM0QyNTVkQjI3OUM3YzNlYmRBMzRCZTVlQzlEODUiLCJhdWQiOiJ0aGlyZHdlYi5jb20iLCJleHAiOjE3NTA4NjU2NzQsIm5iZiI6MTc1MDYwNTg3MiwiaWF0IjoxNzUwNjA2NDc0LCJqdGkiOiIzNmNjOGExYjBhYWY5ODA4ZWEyMjIyOTc4MzI5NDEwMzlkNDA2OTE0ZTJhN2U3YTQxYmM4ZDc1MWJiOTAzODk4IiwiY3R4Ijp7fX0.MHgzZGE4ZGUyM2M1Y2U2ZGExNWY0MTM4OGM3YjFjM2ZhNWNjODlkMDM3M2Q0ZjJkZDg0NGNmZWM1OTJlNTY4MDY3NGY2MmJkNWY1ZWIxMWIyYWRkNmRkN2Q3NmFlNWJjNTgxNWYzNTYwNDk3NzVkYjNiZGJlNTVjNDQyNDFjMTYzZTFi';
27+
const token = await getAuthToken();
3028
if (!token) {
3129
throw new Error("You are not authorized to perform this action");
3230
}

apps/dashboard/src/@/api/webhooks.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@ import "server-only";
44
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
55
import { getAuthToken } from "./auth-token";
66

7-
export type WebhookConfig = {
8-
id: string;
9-
description: string | null;
10-
createdAt: Date;
11-
updatedAt: Date;
12-
deletedAt: Date | null;
13-
teamId: string;
14-
projectId: string;
15-
destinationUrl: string;
16-
pausedAt: Date | null;
17-
webhookSecret: string;
18-
}
7+
export type WebhookConfig = {
8+
id: string;
9+
description: string | null;
10+
createdAt: Date;
11+
updatedAt: Date;
12+
deletedAt: Date | null;
13+
teamId: string;
14+
projectId: string;
15+
destinationUrl: string;
16+
pausedAt: Date | null;
17+
webhookSecret: string;
18+
};
1919

2020
export async function getWebhookConfigs(
2121
teamIdOrSlug: string,
2222
projectIdOrSlug: string,
23-
): Promise< {data: WebhookConfig[] } | {error: string}> {
23+
): Promise<{ data: WebhookConfig[] } | { error: string }> {
2424
const token = await getAuthToken();
2525
if (!token) {
26-
return { error: "Unauthorized." }
26+
return { error: "Unauthorized." };
2727
}
2828

2929
const res = await fetch(
@@ -35,14 +35,15 @@ export async function getWebhookConfigs(
3535
},
3636
);
3737
try {
38-
const json = (await res.json()) as { data: WebhookConfig[],
39-
error: { message: string};
40-
};
41-
if (json.error) {
42-
return { error: json.error.message }
43-
}
44-
return { data: json.data }
45-
} catch (e) {
46-
return { error: "Failed to fetch webhooks." }
38+
const json = (await res.json()) as {
39+
data: WebhookConfig[];
40+
error: { message: string };
41+
};
42+
if (json.error) {
43+
return { error: json.error.message };
44+
}
45+
return { data: json.data };
46+
} catch {
47+
return { error: "Failed to fetch webhooks." };
4748
}
4849
}
Lines changed: 38 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { subHours } from "date-fns";
22
import { AlertTriangleIcon, ClockIcon } from "lucide-react";
3+
import { toast } from "sonner";
34
import {
45
getWebhookLatency,
56
getWebhookRequests,
@@ -12,9 +13,7 @@ import {
1213
type Range,
1314
} from "@/components/analytics/date-range-selector";
1415
import { RangeSelector } from "@/components/analytics/range-selector";
15-
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
16-
import { Card, CardContent } from "@/components/ui/card";
17-
import type { ChartConfig } from "@/components/ui/chart";
16+
import { StatCard } from "@/components/analytics/stat";
1817
import type {
1918
WebhookLatencyStats,
2019
WebhookRequestStats,
@@ -23,16 +22,12 @@ import type {
2322
import { LatencyChart } from "./latency-chart";
2423
import { StatusCodesChart } from "./status-codes-chart";
2524
import { WebhookSelector } from "./webhook-selector";
26-
import { toast } from "sonner";
27-
import { StatCard } from "@/components/analytics/stat";
28-
29-
3025

3126
type WebhookAnalyticsProps = {
3227
interval: "day" | "week";
3328
range: Range;
3429
selectedWebhookId: string | null;
35-
webhooks: WebhookConfig[];
30+
webhooksConfigs: WebhookConfig[];
3631
requestStats: WebhookRequestStats[];
3732
latencyStats: WebhookLatencyStats[];
3833
summaryStats: WebhookSummaryStats[];
@@ -42,91 +37,67 @@ function WebhookAnalytics({
4237
interval,
4338
range,
4439
selectedWebhookId,
45-
webhooks,
40+
webhooksConfigs,
4641
requestStats,
4742
latencyStats,
4843
summaryStats,
4944
}: WebhookAnalyticsProps) {
50-
5145
// Calculate overview metrics for the last 24 hours
52-
const last24HoursSummary = summaryStats.find(
53-
(s) => s.webhookId === selectedWebhookId,
54-
);
55-
const errorRate = 100 - (last24HoursSummary?.successRate || 0);
56-
const avgLatency = last24HoursSummary?.avgLatencyMs || 0;
46+
const errorRate = 100 - (summaryStats[0]?.successRate || 0);
47+
const avgLatency = summaryStats[0]?.avgLatencyMs || 0;
5748

58-
// Transform request data for combined chart
49+
// Transform request data for combined chart.
5950
const allRequestsData = requestStats
60-
.filter(
61-
(stat) => !selectedWebhookId || stat.webhookId === selectedWebhookId,
62-
)
6351
.reduce((acc, stat) => {
52+
const statusCode = stat.httpStatusCode.toString();
6453
const existingEntry = acc.find((entry) => entry.time === stat.date);
6554
if (existingEntry) {
66-
existingEntry.totalRequests += stat.totalRequests;
67-
existingEntry[stat.httpStatusCode.toString()] =
68-
(existingEntry[stat.httpStatusCode.toString()] || 0) +
69-
stat.totalRequests;
55+
existingEntry[statusCode] =
56+
(existingEntry[statusCode] || 0) + stat.totalRequests;
7057
} else {
7158
acc.push({
72-
time: stat.date, // Changed from 'date' to 'time'
73-
totalRequests: stat.totalRequests,
74-
[stat.httpStatusCode.toString()]: stat.totalRequests,
59+
time: stat.date,
60+
[statusCode]: stat.totalRequests,
7561
});
7662
}
7763
return acc;
7864
}, [] as any[])
7965
.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
8066

81-
// Transform latency data for line chart
67+
// Transform latency data for line chart.
8268
const latencyData = latencyStats
83-
.filter(
84-
(stat) => !selectedWebhookId || stat.webhookId === selectedWebhookId,
85-
)
86-
.map((stat) => ({
87-
p50LatencyMs: stat.p50LatencyMs, // Changed from 'date' to 'time'
88-
p90LatencyMs: stat.p90LatencyMs,
89-
p99LatencyMs: stat.p99LatencyMs,
90-
time: stat.date,
91-
}))
69+
.map((stat) => ({ ...stat, time: stat.date }))
9270
.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
9371

9472
return (
9573
<div className="flex flex-col gap-6">
96-
{/* Webhook Selector */}
9774
<WebhookSelector
9875
selectedWebhookId={selectedWebhookId}
99-
webhooks={webhooks}
76+
webhooks={webhooksConfigs}
10077
/>
10178

102-
{/* Overview Cards */}
10379
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
10480
<StatCard
81+
formatter={(value) => `${value.toFixed(2)}%`}
10582
icon={AlertTriangleIcon}
10683
isPending={false}
10784
label="Error Rate (24h)"
10885
value={errorRate}
109-
formatter={(value) => `${value.toFixed(2)}%`}
11086
/>
111-
<StatCard
87+
<StatCard
88+
formatter={(value) => `${value.toFixed(0)}ms`}
11289
icon={ClockIcon}
11390
isPending={false}
11491
label="P50 Latency (24h)"
11592
value={avgLatency}
116-
formatter={(value) => `${value.toFixed(0)}ms`}
11793
/>
11894
</div>
11995

12096
<RangeSelector interval={interval} range={range} />
12197

12298
<div className="flex flex-col gap-4 lg:gap-6">
123-
{selectedWebhookId && (
124-
<StatusCodesChart data={allRequestsData} isPending={false} />
125-
)}
126-
127-
{selectedWebhookId && (
128-
<LatencyChart data={latencyData} isPending={false} />
129-
)}
99+
<StatusCodesChart data={allRequestsData} isPending={false} />
100+
<LatencyChart data={latencyData} isPending={false} />
130101
</div>
131102
</div>
132103
);
@@ -146,80 +117,53 @@ export async function AnalyticsPageContent({
146117
}) {
147118
// Parse search params for filters
148119
const selectedWebhookId = searchParams?.webhookId as string | undefined;
149-
const interval = (searchParams?.interval as "day" | "week") || DEFAULT_INTERVAL;
120+
const interval =
121+
(searchParams?.interval as "day" | "week") || DEFAULT_INTERVAL;
150122
const range = DEFAULT_RANGE; // Could be enhanced to parse from search params
151123

152-
// Fetch webhooks
153-
const webhooksResponse = await getWebhookConfigs(teamSlug, project.id);
154-
if ("error" in webhooksResponse) {
155-
toast.error(webhooksResponse.error);
124+
// Get webhook configs.
125+
const webhooksConfigsResponse = await getWebhookConfigs(teamSlug, project.id);
126+
if ("error" in webhooksConfigsResponse) {
127+
toast.error(webhooksConfigsResponse.error);
156128
return null;
157129
}
158130

159-
const webhooks: WebhookConfig[] =
160-
webhooksResponse.data.length > 0
161-
? webhooksResponse.data
162-
: [
163-
{
164-
id: "8582b449-551e-429f-99c4-5359f253dce1",
165-
description: "Webhook 2",
166-
createdAt: new Date(),
167-
updatedAt: new Date(),
168-
deletedAt: null,
169-
teamId: 'team_clmb33q9w00gn1x0u2ri8z0k0',
170-
projectId: 'prj_cm6ibyah500bgxag5q7d79fps',
171-
destinationUrl: "https://example.com/webhook2",
172-
pausedAt: null,
173-
webhookSecret: "secret",
174-
},
175-
];
176-
177-
// Fetch analytics data
131+
// Get webhook analytics.
178132
const [requestStats, latencyStats, summaryStats] = await Promise.all([
179133
getWebhookRequests({
180-
teamId: 'team_clmb33q9w00gn1x0u2ri8z0k0',
181-
projectId: 'prj_cm6ibyah500bgxag5q7d79fps',
182-
// teamId: project.teamId,
183-
// projectId: project.id,
134+
teamId: project.teamId,
135+
projectId: project.id,
184136
from: range.from,
185-
to: range.to,
186137
period: interval,
138+
to: range.to,
187139
webhookId: selectedWebhookId || undefined,
188140
}).catch(() => []),
189141
getWebhookLatency({
190-
teamId: 'team_clmb33q9w00gn1x0u2ri8z0k0',
191-
projectId: 'prj_cm6ibyah500bgxag5q7d79fps',
192-
// teamId: project.teamId,
193-
// projectId: project.id,
142+
teamId: project.teamId,
143+
projectId: project.id,
194144
from: range.from,
195-
to: range.to,
196145
period: interval,
146+
to: range.to,
197147
webhookId: selectedWebhookId || undefined,
198148
}).catch(() => []),
199149
getWebhookSummary({
200-
teamId: 'team_clmb33q9w00gn1x0u2ri8z0k0',
201-
projectId: 'prj_cm6ibyah500bgxag5q7d79fps',
202-
// teamId: project.teamId,
203-
// projectId: project.id,
150+
teamId: project.teamId,
151+
projectId: project.id,
204152
from: subHours(new Date(), 24),
205153
to: new Date(),
206154
webhookId: selectedWebhookId || undefined,
207155
}).catch(() => []),
208156
]);
209157

210-
console.log("requestStats", requestStats);
211-
console.log("latencyStats", latencyStats);
212-
console.log("summaryStats", summaryStats);
213-
214158
return (
215159
<WebhookAnalytics
216160
interval={interval}
161+
latencyStats={latencyStats}
217162
range={range}
218-
selectedWebhookId={selectedWebhookId || null}
219-
webhooks={webhooks}
220163
requestStats={requestStats}
221-
latencyStats={latencyStats}
164+
selectedWebhookId={selectedWebhookId || null}
222165
summaryStats={summaryStats}
166+
webhooksConfigs={webhooksConfigsResponse.data}
223167
/>
224168
);
225169
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/status-codes-chart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ export function StatusCodesChart({ data, isPending }: StatusCodesChartProps) {
3232
title: "Status Codes",
3333
}}
3434
hideLabel={false}
35+
isPending={isPending}
3536
showLegend={true}
3637
variant="stacked"
37-
isPending={isPending}
3838
/>
3939
);
40-
}
40+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/webhook-selector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function WebhookSelector({
2424

2525
return (
2626
<div className="flex flex-col gap-2">
27-
<label className="text-sm font-medium">Webhook Filter</label>
27+
<div className="text-sm font-medium">Webhook Filter</div>
2828
<Select
2929
onValueChange={(value) => {
3030
const params = new URLSearchParams(searchParams);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default async function WebhooksLayout(props: {
2525
links={[
2626
{
2727
exactMatch: true,
28-
name: "Overview",
28+
name: "Analytics",
2929
path: `/team/${params.team_slug}/${params.project_slug}/webhooks`,
3030
},
3131
{

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ export default async function WebhooksPage({
2727

2828
return (
2929
<div>
30-
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">Overview</h2>
30+
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">Analytics</h2>
3131
<p className="text-muted-foreground text-sm">
3232
Review your webhooks usage and errors.
3333
</p>
3434
<div className="h-6" />
3535
<AnalyticsPageContent
3636
project={project}
37-
teamSlug={resolvedParams.team_slug}
3837
searchParams={resolvedSearchParams}
38+
teamSlug={resolvedParams.team_slug}
3939
/>
4040
</div>
4141
);

0 commit comments

Comments
 (0)