Skip to content

Commit d4a704e

Browse files
committed
Dashboard: Fix webhooks posthog feature flag conditional rendering (#7585)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a centralized webhooks feature by implementing loading states for webhooks, updating the sidebar layout to conditionally render links based on feature flags, and modifying the analytics and page components to handle the new feature flag for centralized webhooks. ### Detailed summary - Added `Loading` components for webhooks in multiple files. - Updated `ProjectSidebarLayout` to include `isCentralizedWebhooksFeatureFlagEnabled`. - Modified `ProjectLayout` to fetch the centralized webhooks feature flag. - Enhanced `WebhooksAnalyticsPage` to redirect based on feature flag. - Updated `WebhooksLayout` to conditionally render tabs based on feature flag. - Refactored `isFeatureFlagEnabled` to accept parameters for account ID and email. - Adjusted `WebhooksPage` to check for the centralized webhooks feature flag before rendering. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 7d796f5 commit d4a704e

File tree

10 files changed

+199
-87
lines changed

10 files changed

+199
-87
lines changed
Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,57 @@
11
import "server-only";
2+
import { unstable_cache } from "next/cache";
23
import { PostHog } from "posthog-node";
34

4-
let posthogServer: PostHog | null = null;
5+
let _posthogClient: PostHog | null = null;
56

67
function getPostHogServer(): PostHog | null {
7-
if (!posthogServer && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
8-
posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
8+
if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
9+
_posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
910
host: "https://us.i.posthog.com",
1011
});
1112
}
12-
return posthogServer;
13+
return _posthogClient;
1314
}
1415

1516
/**
1617
* Check if a feature flag is enabled for a specific user
1718
* @param flagKey - The feature flag key
1819
* @param userEmail - The user's email address for filtering
1920
*/
20-
export async function isFeatureFlagEnabled(
21-
flagKey: string,
22-
userEmail?: string,
23-
): Promise<boolean> {
24-
// For localdev environments where Posthog is not running, enable all feature flags.
25-
if (!posthogServer) {
26-
return true;
27-
}
21+
export const isFeatureFlagEnabled = unstable_cache(
22+
async (params: {
23+
flagKey: string;
24+
accountId: string;
25+
email: string | undefined;
26+
}): Promise<boolean> => {
27+
const posthogClient = getPostHogServer();
28+
if (!posthogClient) {
29+
console.warn("Posthog client not set");
30+
return true;
31+
}
32+
33+
const { flagKey, accountId, email } = params;
2834

29-
try {
30-
const client = getPostHogServer();
31-
if (client && userEmail) {
32-
const isEnabled = await client.isFeatureEnabled(flagKey, userEmail, {
33-
personProperties: {
34-
email: userEmail,
35-
},
36-
});
37-
if (isEnabled !== undefined) {
38-
return isEnabled;
35+
try {
36+
if (posthogClient && accountId) {
37+
const isEnabled = await posthogClient.isFeatureEnabled(
38+
flagKey,
39+
accountId,
40+
{
41+
personProperties: email ? { email } : undefined,
42+
},
43+
);
44+
if (isEnabled !== undefined) {
45+
return isEnabled;
46+
}
3947
}
48+
} catch (error) {
49+
console.error(`Error checking feature flag ${flagKey}:`, error);
4050
}
41-
} catch (error) {
42-
console.error(`Error checking feature flag ${flagKey}:`, error);
43-
}
44-
return false;
45-
}
51+
return false;
52+
},
53+
["is-feature-flag-enabled"],
54+
{
55+
revalidate: 3600, // 1 hour
56+
},
57+
);

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ export function ProjectSidebarLayout(props: {
2222
layoutPath: string;
2323
engineLinkType: "cloud" | "dedicated";
2424
children: React.ReactNode;
25+
isCentralizedWebhooksFeatureFlagEnabled: boolean;
2526
}) {
26-
const { layoutPath, engineLinkType, children } = props;
27+
const {
28+
layoutPath,
29+
engineLinkType,
30+
children,
31+
isCentralizedWebhooksFeatureFlagEnabled,
32+
} = props;
2733

2834
return (
2935
<FullWidthSidebarLayout
@@ -119,8 +125,13 @@ export function ProjectSidebarLayout(props: {
119125
]}
120126
footerSidebarLinks={[
121127
{
122-
href: `${layoutPath}/webhooks`,
128+
href: isCentralizedWebhooksFeatureFlagEnabled
129+
? `${layoutPath}/webhooks`
130+
: `${layoutPath}/webhooks/contracts`,
123131
icon: BellIcon,
132+
isActive: (pathname) => {
133+
return pathname.startsWith(`${layoutPath}/webhooks`);
134+
},
124135
label: (
125136
<span className="flex items-center gap-2">
126137
Webhooks <Badge>New</Badge>

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { redirect } from "next/navigation";
2+
import { isFeatureFlagEnabled } from "@/analytics/posthog-server";
23
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
34
import { getProject, getProjects, type Project } from "@/api/projects";
45
import { getTeamBySlug, getTeams } from "@/api/team";
@@ -56,10 +57,18 @@ export default async function ProjectLayout(props: {
5657
teamId: team.id,
5758
});
5859

59-
const engineLinkType = await getEngineLinkType({
60-
authToken,
61-
project,
62-
});
60+
const [engineLinkType, isCentralizedWebhooksFeatureFlagEnabled] =
61+
await Promise.all([
62+
getEngineLinkType({
63+
authToken,
64+
project,
65+
}),
66+
isFeatureFlagEnabled({
67+
flagKey: "centralized-webhooks",
68+
accountId: account.id,
69+
email: account.email,
70+
}),
71+
]);
6372

6473
const isStaffMode = !teams.some((t) => t.slug === team.slug);
6574

@@ -81,6 +90,9 @@ export default async function ProjectLayout(props: {
8190
<ProjectSidebarLayout
8291
engineLinkType={engineLinkType}
8392
layoutPath={layoutPath}
93+
isCentralizedWebhooksFeatureFlagEnabled={
94+
isCentralizedWebhooksFeatureFlagEnabled
95+
}
8496
>
8597
{props.children}
8698
</ProjectSidebarLayout>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
4+
5+
export default function Loading() {
6+
return <GenericLoadingPage className="border-none" />;
7+
}

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { notFound } from "next/navigation";
1+
import { notFound, redirect } from "next/navigation";
22
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
3+
import { isFeatureFlagEnabled } from "@/analytics/posthog-server";
34
import { getWebhookLatency, getWebhookRequests } from "@/api/analytics";
45
import { getAuthToken } from "@/api/auth-token";
56
import { getProject } from "@/api/projects";
67
import { getWebhookConfigs } from "@/api/webhook-configs";
78
import { getFiltersFromSearchParams } from "@/lib/time";
9+
import { getValidAccount } from "../../../../../../account/settings/getAccount";
810
import { WebhooksAnalytics } from "./components/WebhooksAnalytics";
911

1012
export default async function WebhooksAnalyticsPage(props: {
@@ -16,14 +18,35 @@ export default async function WebhooksAnalyticsPage(props: {
1618
webhook?: string | undefined | string[];
1719
}>;
1820
}) {
19-
const [authToken, params] = await Promise.all([getAuthToken(), props.params]);
21+
const [authToken, params, account] = await Promise.all([
22+
getAuthToken(),
23+
props.params,
24+
getValidAccount(),
25+
]);
26+
27+
if (!account || !authToken) {
28+
notFound();
29+
}
2030

21-
const project = await getProject(params.team_slug, params.project_slug);
31+
const [isFeatureEnabled, project] = await Promise.all([
32+
isFeatureFlagEnabled({
33+
flagKey: "centralized-webhooks",
34+
accountId: account.id,
35+
email: account.email,
36+
}),
37+
getProject(params.team_slug, params.project_slug),
38+
]);
2239

23-
if (!project || !authToken) {
40+
if (!project) {
2441
notFound();
2542
}
2643

44+
if (!isFeatureEnabled) {
45+
redirect(
46+
`/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
47+
);
48+
}
49+
2750
const searchParams = await props.searchParams;
2851
const { range, interval } = getFiltersFromSearchParams({
2952
defaultRange: "last-7",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
4+
5+
export default function Loading() {
6+
return <GenericLoadingPage className="border-none" />;
7+
}

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

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ export default async function WebhooksLayout(props: {
1010
}>;
1111
}) {
1212
const account = await getValidAccount();
13-
const isFeatureEnabled = await isFeatureFlagEnabled(
14-
"webhook-analytics-tab",
15-
account.email,
16-
);
13+
const isFeatureEnabled = await isFeatureFlagEnabled({
14+
flagKey: "centralized-webhooks",
15+
accountId: account.id,
16+
email: account.email,
17+
});
1718

1819
const params = await props.params;
1920
return (
@@ -29,33 +30,46 @@ export default async function WebhooksLayout(props: {
2930
</div>
3031
</div>
3132

32-
<TabPathLinks
33-
links={[
34-
...(isFeatureEnabled
35-
? [
36-
{
37-
exactMatch: true,
38-
name: "Overview",
39-
path: `/team/${params.team_slug}/${params.project_slug}/webhooks`,
40-
},
41-
{
42-
exactMatch: true,
43-
name: "Analytics",
44-
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/analytics`,
45-
},
46-
]
47-
: []),
48-
{
49-
name: "Contracts",
50-
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
51-
},
52-
{
53-
name: "Payments",
54-
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
55-
},
56-
]}
57-
scrollableClassName="container max-w-7xl"
58-
/>
33+
{isFeatureEnabled ? (
34+
<TabPathLinks
35+
links={[
36+
{
37+
exactMatch: true,
38+
name: "Overview",
39+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks`,
40+
},
41+
{
42+
exactMatch: true,
43+
name: "Analytics",
44+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/analytics`,
45+
},
46+
{
47+
name: "Contracts",
48+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
49+
},
50+
{
51+
name: "Payments",
52+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
53+
},
54+
]}
55+
scrollableClassName="container max-w-7xl"
56+
/>
57+
) : (
58+
<TabPathLinks
59+
links={[
60+
{
61+
name: "Contracts",
62+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
63+
},
64+
{
65+
name: "Payments",
66+
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
67+
},
68+
]}
69+
scrollableClassName="container max-w-7xl"
70+
/>
71+
)}
72+
5973
<div className="h-6" />
6074
<div className="container flex max-w-7xl grow flex-col">
6175
{props.children}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use client";
2+
3+
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
4+
5+
export default function Loading() {
6+
return <GenericLoadingPage className="border-none" />;
7+
}

0 commit comments

Comments
 (0)