Skip to content

Commit ff3ea26

Browse files
committed
Dashboard: Fix webhooks posthog feature flag conditional rendering
1 parent f3234c3 commit ff3ea26

File tree

8 files changed

+185
-87
lines changed

8 files changed

+185
-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}

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

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,49 @@
1-
import { notFound } from "next/navigation";
1+
import { notFound, redirect } from "next/navigation";
2+
import { isFeatureFlagEnabled } from "@/analytics/posthog-server";
3+
import { getWebhookSummary } from "@/api/analytics";
24
import { getAuthToken } from "@/api/auth-token";
35
import { getProject } from "@/api/projects";
4-
import { getWebhookSummary } from "../../../../../../../@/api/analytics";
5-
import {
6-
getAvailableTopics,
7-
getWebhookConfigs,
8-
} from "../../../../../../../@/api/webhook-configs";
6+
import { getAvailableTopics, getWebhookConfigs } from "@/api/webhook-configs";
7+
import { getValidAccount } from "../../../../../account/settings/getAccount";
98
import { WebhooksOverview } from "./components/overview";
109

11-
export default async function WebhooksPage({
12-
params,
13-
}: {
10+
export default async function WebhooksPage(props: {
1411
params: Promise<{ team_slug: string; project_slug: string }>;
1512
}) {
16-
const [authToken, resolvedParams] = await Promise.all([
13+
const [authToken, params, account] = await Promise.all([
1714
getAuthToken(),
18-
params,
15+
props.params,
16+
getValidAccount(),
1917
]);
2018

21-
const project = await getProject(
22-
resolvedParams.team_slug,
23-
resolvedParams.project_slug,
24-
);
19+
if (!account || !authToken) {
20+
notFound();
21+
}
22+
23+
const [isFeatureEnabled, project] = await Promise.all([
24+
isFeatureFlagEnabled({
25+
flagKey: "centralized-webhooks",
26+
accountId: account.id,
27+
email: account.email,
28+
}),
29+
getProject(params.team_slug, params.project_slug),
30+
]);
2531

2632
if (!project || !authToken) {
2733
notFound();
2834
}
2935

36+
if (!isFeatureEnabled) {
37+
redirect(
38+
`/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
39+
);
40+
}
41+
3042
// Fetch webhook configs and topics in parallel
3143
const [webhookConfigsResult, topicsResult] = await Promise.all([
3244
getWebhookConfigs({
33-
projectIdOrSlug: resolvedParams.project_slug,
34-
teamIdOrSlug: resolvedParams.team_slug,
45+
projectIdOrSlug: params.project_slug,
46+
teamIdOrSlug: params.team_slug,
3547
}),
3648
getAvailableTopics(),
3749
]);
@@ -75,9 +87,9 @@ export default async function WebhooksPage({
7587
<WebhooksOverview
7688
metricsMap={metricsMap}
7789
projectId={project.id}
78-
projectSlug={resolvedParams.project_slug}
90+
projectSlug={params.project_slug}
7991
teamId={project.teamId}
80-
teamSlug={resolvedParams.team_slug}
92+
teamSlug={params.team_slug}
8193
topics={topics}
8294
webhookConfigs={webhookConfigs}
8395
/>

0 commit comments

Comments
 (0)