Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 39 additions & 27 deletions apps/dashboard/src/@/analytics/posthog-server.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
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;
}

/**
* Check if a feature flag is enabled for a specific user
* @param flagKey - The feature flag key
* @param userEmail - The user's email address for filtering
*/
export async function isFeatureFlagEnabled(
flagKey: string,
userEmail?: string,
): Promise<boolean> {
// 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<boolean> => {
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
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FullWidthSidebarLayout
Expand Down Expand Up @@ -119,8 +125,13 @@ export function ProjectSidebarLayout(props: {
]}
footerSidebarLinks={[
{
href: `${layoutPath}/webhooks`,
href: isCentralizedWebhooksFeatureFlagEnabled
? `${layoutPath}/webhooks`
: `${layoutPath}/webhooks/contracts`,
icon: BellIcon,
isActive: (pathname) => {
return pathname.startsWith(`${layoutPath}/webhooks`);
},
label: (
<span className="flex items-center gap-2">
Webhooks <Badge>New</Badge>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -81,6 +90,9 @@ export default async function ProjectLayout(props: {
<ProjectSidebarLayout
engineLinkType={engineLinkType}
layoutPath={layoutPath}
isCentralizedWebhooksFeatureFlagEnabled={
isCentralizedWebhooksFeatureFlagEnabled
}
>
{props.children}
</ProjectSidebarLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";

export default function Loading() {
return <GenericLoadingPage className="border-none" />;
}
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";

export default function Loading() {
return <GenericLoadingPage className="border-none" />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -29,33 +30,46 @@ export default async function WebhooksLayout(props: {
</div>
</div>

<TabPathLinks
links={[
...(isFeatureEnabled
? [
{
exactMatch: true,
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`,
},
]
: []),
{
name: "Contracts",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
{
name: "Payments",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
},
]}
scrollableClassName="container max-w-7xl"
/>
{isFeatureEnabled ? (
<TabPathLinks
links={[
{
exactMatch: true,
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`,
},
{
name: "Contracts",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
{
name: "Payments",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
},
]}
scrollableClassName="container max-w-7xl"
/>
) : (
<TabPathLinks
links={[
{
name: "Contracts",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
{
name: "Payments",
path: `/team/${params.team_slug}/${params.project_slug}/webhooks/universal-bridge`,
},
]}
scrollableClassName="container max-w-7xl"
/>
)}

<div className="h-6" />
<div className="container flex max-w-7xl grow flex-col">
{props.children}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";

export default function Loading() {
return <GenericLoadingPage className="border-none" />;
}
Loading
Loading