Skip to content

Commit c54db52

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, 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}` <!-- 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 d62e997 commit c54db52

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)