diff --git a/apps/nextjs/src/app/layout.tsx b/apps/nextjs/src/app/layout.tsx index a81cfc132..d476bfad8 100644 --- a/apps/nextjs/src/app/layout.tsx +++ b/apps/nextjs/src/app/layout.tsx @@ -20,6 +20,7 @@ import "@/app/theme-config.css"; import { Providers } from "@/components/AppComponents/Chat//providers"; import { AnalyticsProvider } from "@/components/ContextProviders/AnalyticsProvider"; import { CookieConsentProvider } from "@/components/ContextProviders/CookieConsentProvider"; +import { FeatureFlagProvider } from "@/components/ContextProviders/FeatureFlagProvider"; import FontProvider from "@/components/ContextProviders/FontProvider"; import { GleapProvider } from "@/components/ContextProviders/GleapProvider"; import { WebDebuggerPosition } from "@/lib/avo/Avo"; @@ -118,7 +119,13 @@ export default async function RootLayout({ }} bootstrappedFeatures={bootstrappedFeatures} > - {children} + + + {children} + + diff --git a/apps/nextjs/src/components/ContextProviders/FeatureFlagProvider.tsx b/apps/nextjs/src/components/ContextProviders/FeatureFlagProvider.tsx new file mode 100644 index 000000000..9ad4cc76d --- /dev/null +++ b/apps/nextjs/src/components/ContextProviders/FeatureFlagProvider.tsx @@ -0,0 +1,82 @@ +"use client"; + +import type { ReactNode } from "react"; +import { + useMemo, + createContext, + useContext, + useEffect, + useState, + useRef, +} from "react"; + +import { aiLogger } from "@oakai/logger"; + +import useAnalytics from "@/lib/analytics/useAnalytics"; + +const log = aiLogger("feature-flags"); + +export interface FeatureFlagContextProps { + bootstrappedFeatures: Record; +} + +const FeatureFlagContext = createContext({ + bootstrappedFeatures: {}, +}); + +export interface FeatureFlagProviderProps { + children: ReactNode; + bootstrappedFeatures: Record; +} + +export const FeatureFlagProvider = ({ + children, + bootstrappedFeatures, +}: FeatureFlagProviderProps) => { + const value = useMemo( + () => ({ bootstrappedFeatures }), + [bootstrappedFeatures], + ); + + return ( + + {children} + + ); +}; + +export const useClientSideFeatureFlag = (flag: string) => { + const context = useContext(FeatureFlagContext); + + const { posthogAiBetaClient: posthog } = useAnalytics(); + const hasLogged = useRef(false); + + const [posthogFeatureFlag, setPosthogFeatureFlag] = useState< + boolean | string | undefined + >(); + + const bootstrappedFlag = context.bootstrappedFeatures[flag]; + + useEffect(() => { + return posthog.onFeatureFlags(() => { + const updatedValue = posthog.isFeatureEnabled(flag); + if (updatedValue !== bootstrappedFlag) { + log.info(`Updating ${flag} to ${updatedValue}`); + setPosthogFeatureFlag(updatedValue); + } + }); + }, [posthog, flag, bootstrappedFlag]); + + const isDebug = process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "true"; + if (isDebug) { + if (!hasLogged.current) { + hasLogged.current = true; + log.info(`Feature flag ${flag} is enabled in debug mode`); + } + return true; + } + + // NOTE: This will flash from the bootstrapped value to the posthog value + // only on page load within 1 minute of toggling a flag + return posthogFeatureFlag ?? bootstrappedFlag ?? false; +}; diff --git a/apps/nextjs/src/utils/useClientSideFeatureFlag.ts b/apps/nextjs/src/utils/useClientSideFeatureFlag.ts deleted file mode 100644 index fb07ab398..000000000 --- a/apps/nextjs/src/utils/useClientSideFeatureFlag.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from "react"; - -import { aiLogger } from "@oakai/logger"; - -import useAnalytics from "@/lib/analytics/useAnalytics"; - -const log = aiLogger("feature-flags"); - -export function useClientSideFeatureFlag(flag: string): boolean { - const { posthogAiBetaClient: client } = useAnalytics(); - - const [featureEnabled, setFeatureEnabled] = useState(); - - useEffect(() => { - const isDebug = process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "true"; - - if (isDebug) { - log.info(`Feature flag ${flag} is enabled in debug mode`); - setFeatureEnabled(true); - } else { - return client.onFeatureFlags(() => { - setFeatureEnabled(client.isFeatureEnabled(flag)); - }); - } - }, [client, flag]); - - return featureEnabled ?? false; -}