From c54db522b34b2d3f83f4eae647cd70b76afe6837 Mon Sep 17 00:00:00 2001 From: MananTank Date: Thu, 10 Jul 2025 20:15:27 +0000 Subject: [PATCH] Dashboard: Fix webhooks posthog feature flag conditional rendering (#7585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR introduces a centralized webhooks feature, enhancing the sidebar layout and analytics for webhooks. It adds loading states, integrates feature flags, and modifies the handling of webhooks in various components. ### Detailed summary - Added `Loading` components using `GenericLoadingPage` in multiple webhook pages. - Updated `ProjectSidebarLayout` to include `isCentralizedWebhooksFeatureFlagEnabled`. - Enhanced `ProjectLayout` to fetch and utilize the centralized webhooks feature flag. - Modified `WebhooksAnalyticsPage` to check for the centralized webhooks feature flag before redirecting. - Adjusted `WebhooksLayout` to conditionally render links based on the centralized webhooks feature flag. - Refactored `isFeatureFlagEnabled` function for improved feature flag handling. - Updated `WebhooksPage` to include centralized webhooks feature flag checks and redirection logic. - Cleaned up imports and ensured consistent use of `params` across various components. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Added multiple loading indicators across webhooks analytics, contracts, universal bridge, and sidebar routes for smoother user experience. * Sidebar and tab navigation dynamically update based on the centralized webhooks feature flag. * **Bug Fixes** * Enhanced error handling with 404 responses for missing accounts, projects, or authentication on webhooks pages. * Redirects implemented when the centralized webhooks feature flag is disabled. * **Refactor** * Updated feature flag checks to a new cached API accepting structured parameters. * Streamlined parameter handling and navigation logic for webhooks pages. --- .../src/@/analytics/posthog-server.ts | 66 +++++++++------- .../components/ProjectSidebarLayout.tsx | 15 +++- .../[project_slug]/(sidebar)/layout.tsx | 20 ++++- .../(sidebar)/webhooks/analytics/loading.tsx | 7 ++ .../(sidebar)/webhooks/analytics/page.tsx | 31 +++++++- .../(sidebar)/webhooks/contracts/loading.tsx | 7 ++ .../(sidebar)/webhooks/layout.tsx | 76 +++++++++++-------- .../(sidebar)/webhooks/loading.tsx | 7 ++ .../(sidebar)/webhooks/page.tsx | 50 +++++++----- .../webhooks/universal-bridge/loading.tsx | 7 ++ 10 files changed, 199 insertions(+), 87 deletions(-) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/loading.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/loading.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/loading.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/universal-bridge/loading.tsx diff --git a/apps/dashboard/src/@/analytics/posthog-server.ts b/apps/dashboard/src/@/analytics/posthog-server.ts index 87167f267be..cf6d93fc091 100644 --- a/apps/dashboard/src/@/analytics/posthog-server.ts +++ b/apps/dashboard/src/@/analytics/posthog-server.ts @@ -1,15 +1,16 @@ import "server-only"; +import { unstable_cache } from "next/cache"; import { PostHog } from "posthog-node"; -let posthogServer: PostHog | null = null; +let _posthogClient: PostHog | null = null; function getPostHogServer(): PostHog | null { - if (!posthogServer && process.env.NEXT_PUBLIC_POSTHOG_KEY) { - posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) { + _posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { host: "https://us.i.posthog.com", }); } - return posthogServer; + return _posthogClient; } /** @@ -17,29 +18,40 @@ function getPostHogServer(): PostHog | null { * @param flagKey - The feature flag key * @param userEmail - The user's email address for filtering */ -export async function isFeatureFlagEnabled( - flagKey: string, - userEmail?: string, -): Promise { - // For localdev environments where Posthog is not running, enable all feature flags. - if (!posthogServer) { - return true; - } +export const isFeatureFlagEnabled = unstable_cache( + async (params: { + flagKey: string; + accountId: string; + email: string | undefined; + }): Promise => { + const posthogClient = getPostHogServer(); + if (!posthogClient) { + console.warn("Posthog client not set"); + return true; + } + + const { flagKey, accountId, email } = params; - try { - const client = getPostHogServer(); - if (client && userEmail) { - const isEnabled = await client.isFeatureEnabled(flagKey, userEmail, { - personProperties: { - email: userEmail, - }, - }); - if (isEnabled !== undefined) { - return isEnabled; + try { + if (posthogClient && accountId) { + const isEnabled = await posthogClient.isFeatureEnabled( + flagKey, + accountId, + { + personProperties: email ? { email } : undefined, + }, + ); + if (isEnabled !== undefined) { + return isEnabled; + } } + } catch (error) { + console.error(`Error checking feature flag ${flagKey}:`, error); } - } catch (error) { - console.error(`Error checking feature flag ${flagKey}:`, error); - } - return false; -} + return false; + }, + ["is-feature-flag-enabled"], + { + revalidate: 3600, // 1 hour + }, +); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx index cd92dbf8c23..3549bab49a8 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx @@ -22,8 +22,14 @@ export function ProjectSidebarLayout(props: { layoutPath: string; engineLinkType: "cloud" | "dedicated"; children: React.ReactNode; + isCentralizedWebhooksFeatureFlagEnabled: boolean; }) { - const { layoutPath, engineLinkType, children } = props; + const { + layoutPath, + engineLinkType, + children, + isCentralizedWebhooksFeatureFlagEnabled, + } = props; return ( { + return pathname.startsWith(`${layoutPath}/webhooks`); + }, label: ( Webhooks New diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx index c1f517d3ff2..0e13ff01443 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx @@ -1,4 +1,5 @@ import { redirect } from "next/navigation"; +import { isFeatureFlagEnabled } from "@/analytics/posthog-server"; import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token"; import { getProject, getProjects, type Project } from "@/api/projects"; import { getTeamBySlug, getTeams } from "@/api/team"; @@ -56,10 +57,18 @@ export default async function ProjectLayout(props: { teamId: team.id, }); - const engineLinkType = await getEngineLinkType({ - authToken, - project, - }); + const [engineLinkType, isCentralizedWebhooksFeatureFlagEnabled] = + await Promise.all([ + getEngineLinkType({ + authToken, + project, + }), + isFeatureFlagEnabled({ + flagKey: "centralized-webhooks", + accountId: account.id, + email: account.email, + }), + ]); const isStaffMode = !teams.some((t) => t.slug === team.slug); @@ -81,6 +90,9 @@ export default async function ProjectLayout(props: { {props.children} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/loading.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/loading.tsx new file mode 100644 index 00000000000..0528bd15ae9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/analytics/loading.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; + +export default function Loading() { + 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 index 91f0e4aa5b4..9c23895b514 100644 --- 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 @@ -1,10 +1,12 @@ -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import { ResponsiveSearchParamsProvider } from "responsive-rsc"; +import { isFeatureFlagEnabled } from "@/analytics/posthog-server"; import { getWebhookLatency, getWebhookRequests } from "@/api/analytics"; import { getAuthToken } from "@/api/auth-token"; import { getProject } from "@/api/projects"; import { getWebhookConfigs } from "@/api/webhook-configs"; import { getFiltersFromSearchParams } from "@/lib/time"; +import { getValidAccount } from "../../../../../../account/settings/getAccount"; import { WebhooksAnalytics } from "./components/WebhooksAnalytics"; export default async function WebhooksAnalyticsPage(props: { @@ -16,14 +18,35 @@ export default async function WebhooksAnalyticsPage(props: { webhook?: string | undefined | string[]; }>; }) { - const [authToken, params] = await Promise.all([getAuthToken(), props.params]); + const [authToken, params, account] = await Promise.all([ + getAuthToken(), + props.params, + getValidAccount(), + ]); + + if (!account || !authToken) { + notFound(); + } - const project = await getProject(params.team_slug, params.project_slug); + const [isFeatureEnabled, project] = await Promise.all([ + isFeatureFlagEnabled({ + flagKey: "centralized-webhooks", + accountId: account.id, + email: account.email, + }), + getProject(params.team_slug, params.project_slug), + ]); - if (!project || !authToken) { + if (!project) { notFound(); } + if (!isFeatureEnabled) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`, + ); + } + const searchParams = await props.searchParams; const { range, interval } = getFiltersFromSearchParams({ defaultRange: "last-7", diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/loading.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/loading.tsx new file mode 100644 index 00000000000..0528bd15ae9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contracts/loading.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; + +export default function Loading() { + return ; +} 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 43cfed57ddb..852acc1ffa1 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 @@ -10,10 +10,11 @@ export default async function WebhooksLayout(props: { }>; }) { const account = await getValidAccount(); - const isFeatureEnabled = await isFeatureFlagEnabled( - "webhook-analytics-tab", - account.email, - ); + const isFeatureEnabled = await isFeatureFlagEnabled({ + flagKey: "centralized-webhooks", + accountId: account.id, + email: account.email, + }); const params = await props.params; return ( @@ -29,33 +30,46 @@ export default async function WebhooksLayout(props: { - + {isFeatureEnabled ? ( + + ) : ( + + )} +
{props.children} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/loading.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/loading.tsx new file mode 100644 index 00000000000..0528bd15ae9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/loading.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; + +export default function Loading() { + return ; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx index 653389e2c14..68e75ecbc7f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx @@ -1,37 +1,49 @@ -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; +import { isFeatureFlagEnabled } from "@/analytics/posthog-server"; +import { getWebhookSummary } from "@/api/analytics"; import { getAuthToken } from "@/api/auth-token"; import { getProject } from "@/api/projects"; -import { getWebhookSummary } from "../../../../../../../@/api/analytics"; -import { - getAvailableTopics, - getWebhookConfigs, -} from "../../../../../../../@/api/webhook-configs"; +import { getAvailableTopics, getWebhookConfigs } from "@/api/webhook-configs"; +import { getValidAccount } from "../../../../../account/settings/getAccount"; import { WebhooksOverview } from "./components/overview"; -export default async function WebhooksPage({ - params, -}: { +export default async function WebhooksPage(props: { params: Promise<{ team_slug: string; project_slug: string }>; }) { - const [authToken, resolvedParams] = await Promise.all([ + const [authToken, params, account] = await Promise.all([ getAuthToken(), - params, + props.params, + getValidAccount(), ]); - const project = await getProject( - resolvedParams.team_slug, - resolvedParams.project_slug, - ); + if (!account || !authToken) { + notFound(); + } + + const [isFeatureEnabled, project] = await Promise.all([ + isFeatureFlagEnabled({ + flagKey: "centralized-webhooks", + accountId: account.id, + email: account.email, + }), + getProject(params.team_slug, params.project_slug), + ]); if (!project || !authToken) { notFound(); } + if (!isFeatureEnabled) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`, + ); + } + // Fetch webhook configs and topics in parallel const [webhookConfigsResult, topicsResult] = await Promise.all([ getWebhookConfigs({ - projectIdOrSlug: resolvedParams.project_slug, - teamIdOrSlug: resolvedParams.team_slug, + projectIdOrSlug: params.project_slug, + teamIdOrSlug: params.team_slug, }), getAvailableTopics(), ]); @@ -75,9 +87,9 @@ export default async function WebhooksPage({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/universal-bridge/loading.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/universal-bridge/loading.tsx new file mode 100644 index 00000000000..0528bd15ae9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/universal-bridge/loading.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage"; + +export default function Loading() { + return ; +}