diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index 44ea3c8886d..d2d6c643c19 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -13,6 +13,8 @@ import type { UserOpStats, WalletStats, WalletUserStats, + WebhookLatencyStats, + WebhookRequestStats, WebhookSummaryStats, } from "@/types/analytics"; import { getAuthToken } from "./auth-token"; @@ -426,44 +428,56 @@ export async function getEngineCloudMethodUsage( return json.data as EngineCloudStats[]; } -export async function getWebhookMetrics(params: { - teamId: string; - projectId: string; - webhookId: string; - period?: "day" | "week" | "month" | "year" | "all"; - from?: Date; - to?: Date; -}): Promise<{ data: WebhookSummaryStats[] } | { error: string }> { - const searchParams = new URLSearchParams(); - - // Required params - searchParams.append("teamId", params.teamId); - searchParams.append("projectId", params.projectId); +export async function getWebhookSummary( + params: AnalyticsQueryParams & { webhookId: string }, +): Promise<{ data: WebhookSummaryStats[] } | { error: string }> { + const searchParams = buildSearchParams(params); searchParams.append("webhookId", params.webhookId); - // Optional params - if (params.period) { - searchParams.append("period", params.period); - } - if (params.from) { - searchParams.append("from", params.from.toISOString()); + const res = await fetchAnalytics( + `v2/webhook/summary?${searchParams.toString()}`, + ); + if (!res.ok) { + const reason = await res.text(); + return { error: reason }; } - if (params.to) { - searchParams.append("to", params.to.toISOString()); + + return (await res.json()) as { data: WebhookSummaryStats[] }; +} + +export async function getWebhookRequests( + params: AnalyticsQueryParams & { webhookId?: string }, +): Promise<{ data: WebhookRequestStats[] } | { error: string }> { + const searchParams = buildSearchParams(params); + if (params.webhookId) { + searchParams.append("webhookId", params.webhookId); } const res = await fetchAnalytics( - `v2/webhook/summary?${searchParams.toString()}`, - { - method: "GET", - }, + `v2/webhook/requests?${searchParams.toString()}`, ); + if (!res.ok) { + const reason = await res.text(); + return { error: reason }; + } + return (await res.json()) as { data: WebhookRequestStats[] }; +} + +export async function getWebhookLatency( + params: AnalyticsQueryParams & { webhookId?: string }, +): Promise<{ data: WebhookLatencyStats[] } | { error: string }> { + const searchParams = buildSearchParams(params); + if (params.webhookId) { + searchParams.append("webhookId", params.webhookId); + } + const res = await fetchAnalytics( + `v2/webhook/latency?${searchParams.toString()}`, + ); if (!res.ok) { - const reason = await res?.text(); + const reason = await res.text(); return { error: reason }; } - return (await res.json()) as { - data: WebhookSummaryStats[]; - }; + + return (await res.json()) as { data: WebhookLatencyStats[] }; } diff --git a/apps/dashboard/src/@/api/webhook-metrics.ts b/apps/dashboard/src/@/api/webhook-metrics.ts deleted file mode 100644 index 8aff3b45218..00000000000 --- a/apps/dashboard/src/@/api/webhook-metrics.ts +++ /dev/null @@ -1,16 +0,0 @@ -"use server"; - -import { getWebhookMetrics } from "@/api/analytics"; -import type { WebhookSummaryStats } from "@/types/analytics"; - -export async function getWebhookMetricsAction(params: { - teamId: string; - projectId: string; - webhookId: string; - period?: "day" | "week" | "month" | "year" | "all"; - from?: Date; - to?: Date; -}): Promise { - const metrics = await getWebhookMetrics(params); - return metrics[0] || null; -} diff --git a/apps/dashboard/src/@/types/analytics.ts b/apps/dashboard/src/@/types/analytics.ts index f1f2e743920..1600bb5cb7c 100644 --- a/apps/dashboard/src/@/types/analytics.ts +++ b/apps/dashboard/src/@/types/analytics.ts @@ -72,6 +72,21 @@ export interface UniversalBridgeWalletStats { developerFeeUsdCents: number; } +export interface WebhookRequestStats { + date: string; + webhookId: string; + httpStatusCode: number; + totalRequests: number; +} + +export interface WebhookLatencyStats { + date: string; + webhookId: string; + p50LatencyMs: number; + p90LatencyMs: number; + p99LatencyMs: number; +} + export interface WebhookSummaryStats { webhookId: string; totalRequests: number; @@ -79,7 +94,7 @@ export interface WebhookSummaryStats { errorRequests: number; successRate: number; avgLatencyMs: number; - errorBreakdown: Record; + errorBreakdown: Record; } export interface AnalyticsQueryParams { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/actions/get-webhook-summary.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/actions/get-webhook-summary.ts new file mode 100644 index 00000000000..b5fcd0db7fe --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/actions/get-webhook-summary.ts @@ -0,0 +1,27 @@ +"use server"; + +import { getWebhookSummary } from "@/api/analytics"; +import type { WebhookSummaryStats } from "@/types/analytics"; + +export async function getWebhookSummaryAction(params: { + teamId: string; + projectId: string; + webhookId: string; + period?: "day" | "week" | "month" | "year" | "all"; + from?: Date; + to?: Date; +}): Promise { + try { + const result = await getWebhookSummary(params); + + if ("error" in result) { + console.error("Failed to fetch webhook summary:", result.error); + return null; + } + + return result.data[0] ?? null; + } catch (error) { + console.error("Unexpected error fetching webhook summary:", error); + return null; + } +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsCharts.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsCharts.tsx new file mode 100644 index 00000000000..4fbdfd282d2 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsCharts.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { format } from "date-fns"; +import { useMemo, useState } from "react"; +import { ResponsiveSuspense } from "responsive-rsc"; +import type { WebhookConfig } from "@/api/webhook-configs"; +import type { Range } from "@/components/analytics/date-range-selector"; +import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart"; +import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import type { ChartConfig } from "@/components/ui/chart"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import type { + WebhookLatencyStats, + WebhookRequestStats, +} from "@/types/analytics"; +import { WebhookAnalyticsFilter } from "./WebhookAnalyticsFilter"; + +interface WebhookAnalyticsChartsProps { + webhookConfigs: WebhookConfig[]; + requestsData: WebhookRequestStats[]; + latencyData: WebhookLatencyStats[]; + range: Range; +} + +export function WebhookAnalyticsCharts({ + webhookConfigs, + requestsData, + latencyData, + range, +}: WebhookAnalyticsChartsProps) { + const [selectedWebhook, setSelectedWebhook] = useState("all"); + + // Filter data based on selected webhook and date range + const webhookId = selectedWebhook === "all" ? undefined : selectedWebhook; + const filteredRequestsData = useMemo(() => { + let data = requestsData; + + // Filter by webhook if specified + if (webhookId) { + data = data.filter((item) => item.webhookId === webhookId); + } + + // Filter by date range + data = data.filter((item) => { + const itemDate = new Date(item.date); + return itemDate >= range.from && itemDate <= range.to; + }); + + return data; + }, [requestsData, webhookId, range.from, range.to]); + + const filteredLatencyData = useMemo(() => { + let data = latencyData; + + // Filter by webhook if specified + if (webhookId) { + data = data.filter((item) => item.webhookId === webhookId); + } + + // Filter by date range + data = data.filter((item) => { + const itemDate = new Date(item.date); + return itemDate >= range.from && itemDate <= range.to; + }); + + return data; + }, [latencyData, webhookId, range.from, range.to]); + + // Process status code distribution data by individual status codes + const statusCodeData = useMemo(() => { + if (!filteredRequestsData.length) return []; + + const groupedData = filteredRequestsData.reduce( + (acc, item) => { + const date = new Date(item.date).getTime(); + if (!acc[date]) { + acc[date] = { time: date }; + } + + // Only include valid status codes (not 0) with actual request counts + if (item.httpStatusCode > 0 && item.totalRequests > 0) { + const statusKey = item.httpStatusCode.toString(); + acc[date][statusKey] = + (acc[date][statusKey] || 0) + item.totalRequests; + } + return acc; + }, + {} as Record & { time: number }>, + ); + + return Object.values(groupedData).sort( + (a, b) => (a.time || 0) - (b.time || 0), + ); + }, [filteredRequestsData]); + + // Process latency data for charts + const latencyChartData = useMemo(() => { + if (!filteredLatencyData.length) return []; + + return filteredLatencyData + .map((item) => ({ + p50: item.p50LatencyMs, + p90: item.p90LatencyMs, + p99: item.p99LatencyMs, + time: new Date(item.date).getTime(), + })) + .sort((a, b) => a.time - b.time); + }, [filteredLatencyData]); + + // Chart configurations + const latencyChartConfig: ChartConfig = { + p50: { + color: "hsl(var(--chart-1))", + label: "P50 Latency", + }, + p90: { + color: "hsl(var(--chart-2))", + label: "P90 Latency", + }, + p99: { + color: "hsl(var(--chart-3))", + label: "P99 Latency", + }, + }; + + // Generate status code chart config dynamically with class-based colors + const statusCodeConfig: ChartConfig = useMemo(() => { + const statusCodes = new Set(); + statusCodeData.forEach((item) => { + Object.keys(item).forEach((key) => { + if (key !== "time" && !Number.isNaN(Number.parseInt(key))) { + statusCodes.add(key); + } + }); + }); + + const getColorForStatusCode = (statusCode: number): string => { + if (statusCode >= 200 && statusCode < 300) { + return "hsl(142, 76%, 36%)"; // Green for 2xx + } else if (statusCode >= 300 && statusCode < 400) { + return "hsl(48, 96%, 53%)"; // Yellow for 3xx + } else if (statusCode >= 400 && statusCode < 500) { + return "hsl(25, 95%, 53%)"; // Orange for 4xx + } else { + return "hsl(0, 84%, 60%)"; // Red for 5xx + } + }; + + const config: ChartConfig = {}; + Array.from(statusCodes) + .sort((a, b) => { + const codeA = Number.parseInt(a); + const codeB = Number.parseInt(b); + return codeA - codeB; + }) + .forEach((statusKey) => { + const statusCode = Number.parseInt(statusKey); + config[statusKey] = { + color: getColorForStatusCode(statusCode), + label: statusCode.toString(), + }; + }); + + return config; + }, [statusCodeData]); + + const selectedWebhookConfig = webhookConfigs.find( + (w) => w.id === selectedWebhook, + ); + + // Show empty state if no data + if (statusCodeData.length === 0 && latencyChartData.length === 0) { + return ( +
+
+ +
+
+
+

+ No webhook data available +

+

+ Webhook analytics will appear here once you start receiving + webhook events. +

+
+
+
+ ); + } + + return ( +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + + +
+ } + searchParamsUsed={["from", "to", "interval", "webhook"]} + > +
+ {/* Header with webhook selector */} +
+
+

Webhook Analytics

+

+ Performance metrics and trends for your webhooks +

+
+ +
+ +
+
+ + {/* Selected webhook info */} + {selectedWebhookConfig && ( + + +
+
+

+ {selectedWebhookConfig.description || "Unnamed Webhook"} +

+

+ {selectedWebhookConfig.destinationUrl} +

+
+
+ + {selectedWebhookConfig.pausedAt ? "Paused" : "Active"} + + {selectedWebhookConfig.topics.slice(0, 2).map((topic) => ( + + {topic.serviceName} + + ))} + {selectedWebhookConfig.topics.length > 2 && ( + + +{selectedWebhookConfig.topics.length - 2} more + + )} +
+
+
+
+ )} + + {/* Charts */} +
+ {/* Status Code Distribution Chart */} + + format( + new Date(Number.parseInt(label as string)), + "MMM dd, yyyy HH:mm", + ) + } + toolTipValueFormatter={(value) => `${value} requests`} + variant="stacked" + /> + + {/* Latency Chart */} + + format( + new Date(Number.parseInt(label as string)), + "MMM dd, yyyy HH:mm", + ) + } + toolTipValueFormatter={(value) => `${value}ms`} + /> +
+
+ + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsFilter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsFilter.tsx new file mode 100644 index 00000000000..7950c5b8f23 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsFilter.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { + useResponsiveSearchParams, + useSetResponsiveSearchParams, +} from "responsive-rsc"; +import { + DateRangeSelector, + type DurationId, +} from "@/components/analytics/date-range-selector"; +import { IntervalSelector } from "@/components/analytics/interval-selector"; +import { getFiltersFromSearchParams, normalizeTimeISOString } from "@/lib/time"; + +const STORAGE_KEY = "thirdweb:webhook-analytics-range"; + +type SavedRange = { + rangeType: "custom" | DurationId | undefined; + interval: "day" | "week"; +}; + +type SearchParams = { + from?: string; + to?: string; + interval?: "day" | "week"; +}; + +export function WebhookAnalyticsFilter() { + const responsiveSearchParams = useResponsiveSearchParams(); + const setResponsiveSearchParams = useSetResponsiveSearchParams(); + + // Load saved range from localStorage using useQuery + useQuery({ + enabled: !responsiveSearchParams.from && !responsiveSearchParams.to, + queryFn: () => { + const savedRangeString = localStorage.getItem(STORAGE_KEY); + if (savedRangeString) { + try { + const savedRange = JSON.parse(savedRangeString) as SavedRange; + // Get the current range based on the saved range type + const { range } = getFiltersFromSearchParams({ + defaultRange: + savedRange.rangeType === "custom" + ? "last-30" + : savedRange.rangeType || "last-30", + from: undefined, + interval: savedRange.interval, + to: undefined, + }); + + setResponsiveSearchParams((v) => ({ + ...v, + from: normalizeTimeISOString(range.from), + interval: savedRange.interval, + to: normalizeTimeISOString(range.to), + })); + } catch (e) { + console.error("Failed to parse saved range:", e); + } + } + return null; + }, + queryKey: [ + "savedRange", + responsiveSearchParams.from, + responsiveSearchParams.to, + ], + }); + + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-30", + from: responsiveSearchParams.from, + interval: responsiveSearchParams.interval, + to: responsiveSearchParams.to, + }); + + const saveToLocalStorage = (params: { + rangeType: string; + interval: "day" | "week"; + }) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(params)); + }; + + return ( +
+ { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + from: normalizeTimeISOString(newRange.from), + to: normalizeTimeISOString(newRange.to), + }; + // Save to localStorage + saveToLocalStorage({ + interval: newParams.interval || "day", + rangeType: newRange.type || "last-30", + }); + return newParams; + }); + }} + /> + + { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + interval: newInterval, + }; + // Save to localStorage + saveToLocalStorage({ + interval: newInterval, + rangeType: range.type || "last-30", + }); + return newParams; + }); + }} + /> +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsServer.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsServer.tsx new file mode 100644 index 00000000000..6cd0e858824 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhookAnalyticsServer.tsx @@ -0,0 +1,61 @@ +import { getWebhookLatency, getWebhookRequests } from "@/api/analytics"; +import type { WebhookConfig } from "@/api/webhook-configs"; +import type { Range } from "@/components/analytics/date-range-selector"; +import { WebhookAnalyticsCharts } from "./WebhookAnalyticsCharts"; + +interface WebhookAnalyticsServerProps { + teamId: string; + projectId: string; + webhookConfigs: WebhookConfig[]; + range: Range; + interval: "day" | "week"; +} + +export async function WebhookAnalyticsServer({ + teamId, + projectId, + webhookConfigs, + range, + interval, +}: WebhookAnalyticsServerProps) { + // Fetch webhook analytics data using the provided range and interval + const [requestsData, latencyData] = await Promise.all([ + (async () => { + const res = await getWebhookRequests({ + from: range.from, + period: interval, + projectId, + teamId, + to: range.to, + }); + if ("error" in res) { + console.error("Failed to fetch webhook requests:", res.error); + return []; + } + return res.data; + })(), + (async () => { + const res = await getWebhookLatency({ + from: range.from, + period: interval, + projectId, + teamId, + to: range.to, + }); + if ("error" in res) { + console.error("Failed to fetch webhook latency:", res.error); + return []; + } + return res.data; + })(), + ]); + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhooksAnalytics.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhooksAnalytics.tsx new file mode 100644 index 00000000000..06416215c77 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/components/WebhooksAnalytics.tsx @@ -0,0 +1,50 @@ +import { getWebhookConfigs } from "@/api/webhook-configs"; +import type { Range } from "@/components/analytics/date-range-selector"; +import { WebhookAnalyticsServer } from "./WebhookAnalyticsServer"; + +interface WebhooksAnalyticsProps { + teamId: string; + teamSlug: string; + projectId: string; + projectSlug: string; + range: Range; + interval: "day" | "week"; +} + +export async function WebhooksAnalytics({ + teamId, + teamSlug, + projectId, + projectSlug, + range, + interval, +}: WebhooksAnalyticsProps) { + const webhookConfigsResponse = await getWebhookConfigs({ + projectIdOrSlug: projectSlug, + teamIdOrSlug: teamSlug, + }).catch(() => ({ data: [], error: "Failed to fetch webhook configs" })); + + if ( + webhookConfigsResponse.error || + !webhookConfigsResponse.data || + webhookConfigsResponse.data.length === 0 + ) { + return ( +
+

+ No webhook configurations found. +

+
+ ); + } + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/page.tsx new file mode 100644 index 00000000000..956f3d23643 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/page.tsx @@ -0,0 +1,44 @@ +import { notFound } from "next/navigation"; +import { ResponsiveSearchParamsProvider } from "responsive-rsc"; +import { getAuthToken } from "@/api/auth-token"; +import { getProject } from "@/api/projects"; +import { getFiltersFromSearchParams } from "@/lib/time"; +import { WebhooksAnalytics } from "./components/WebhooksAnalytics"; + +export default async function WebhooksAnalyticsPage(props: { + params: Promise<{ team_slug: string; project_slug: string }>; + searchParams: Promise<{ + from?: string | undefined | string[]; + to?: string | undefined | string[]; + interval?: string | undefined | string[]; + }>; +}) { + const [authToken, params] = await Promise.all([getAuthToken(), props.params]); + + const project = await getProject(params.team_slug, params.project_slug); + + if (!project || !authToken) { + notFound(); + } + + const searchParams = await props.searchParams; + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-7", + from: searchParams.from, + interval: searchParams.interval, + to: searchParams.to, + }); + + return ( + + + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/overview.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/overview.tsx index a10f8dff04d..42e9e016cc9 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/overview.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/components/overview.tsx @@ -1,7 +1,6 @@ "use client"; import { redirect } from "next/navigation"; -import posthog from "posthog-js"; import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; import { useAvailableTopics } from "../hooks/use-available-topics"; import { useWebhookConfigs } from "../hooks/use-webhook-configs"; @@ -20,9 +19,8 @@ export function WebhooksOverview({ projectId, projectSlug, }: WebhooksOverviewProps) { - // Enabled on dev or if FF is enabled. - const isFeatureEnabled = - !posthog.__loaded || posthog.isFeatureEnabled("centralized-webhooks"); + // Feature is enabled (matches server component behavior) + const isFeatureEnabled = true; const webhookConfigsQuery = useWebhookConfigs({ enabled: isFeatureEnabled, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/hooks/use-webhook-metrics.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/hooks/use-webhook-metrics.ts index 813c1d5f146..678b7a43b53 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/hooks/use-webhook-metrics.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/hooks/use-webhook-metrics.ts @@ -1,8 +1,8 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { getWebhookMetricsAction } from "@/api/webhook-metrics"; import type { WebhookSummaryStats } from "@/types/analytics"; +import { getWebhookSummaryAction } from "../actions/get-webhook-summary"; interface UseWebhookMetricsParams { webhookId: string; @@ -19,16 +19,15 @@ export function useWebhookMetrics({ }: UseWebhookMetricsParams) { return useQuery({ enabled: enabled && !!webhookId, - queryFn: async (): Promise => { - return await getWebhookMetricsAction({ + queryFn: () => + getWebhookSummaryAction({ from: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago period: "day", projectId, teamId, to: new Date(), webhookId, - }); - }, + }), queryKey: ["webhook-metrics", teamId, projectId, webhookId], retry: 1, staleTime: 5 * 60 * 1000, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/layout.tsx index bc6c3280fb3..8b8a177bbe0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/layout.tsx @@ -1,4 +1,3 @@ -import posthog from "posthog-js"; import { TabPathLinks } from "@/components/ui/tabs"; export default async function WebhooksLayout(props: { @@ -9,8 +8,8 @@ export default async function WebhooksLayout(props: { }>; }) { // Enabled on dev or if FF is enabled. - const isFeatureEnabled = - !posthog.__loaded || posthog.isFeatureEnabled("centralized-webhooks"); + // In server components, we default to true since posthog is client-side only + const isFeatureEnabled = true; const params = await props.params; return ( @@ -35,6 +34,11 @@ export default async function WebhooksLayout(props: { name: "Overview", path: `/team/${params.team_slug}/${params.project_slug}/webhooks`, }, + { + exactMatch: true, + name: "Analytics", + path: `/team/${params.team_slug}/${params.project_slug}/webhooks/analytics`, + }, ] : []), {