feat(frontend/feature-flags): Add LaunchDarkly feature flagging UI (#8847)

This PR allows us to feature flag on the frontend, this means we can
rollout features in stages, hide features, do AB testing etc.

### Changes 🏗️

Added a LaunchDarkly Provider
Added a withFeatureFlag component
Added two env vars for: 
- enabling LD 
- specifying the _public_ client side key

Usage: 

```
'use client'

import { useFlags } from 'launchdarkly-react-client-sdk'
import { withFeatureFlag } from '@/components/feature-flag/with-feature-flag'

function TestFlagPage() {
  const flags = useFlags()

  return (
    <div className="p-4">
      <h1>If you can see this, the feature flag is ON</h1>
      <pre>Current flag value: {JSON.stringify(flags, null, 2)}</pre>
    </div>
  )
}

export default withFeatureFlag(TestFlagPage, 'test-flag')
```

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Test plan</summary>
- Set LD to false
- Navigate to a test page, should not be visible
- Set LD to true
- Navigate to same test page, should be visible
</details>

#### For configuration changes:
- [x] `.env.example` is updated or already compatible with my changes
- [x] I have included a list of my configuration changes in the PR
description (under **Changes**)
- [x] I have updated infra repo

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Bently <tomnoon9@gmail.com>
Co-authored-by: SerchioSD <69461657+serchiosd@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com>
Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
Co-authored-by: Toran Bruce Richards <toran.richards@gmail.com>
This commit is contained in:
Aarushi 2024-12-06 19:11:06 +00:00 committed by GitHub
parent ea6c9a1152
commit d7c9742d7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1700 additions and 1669 deletions

View File

@ -2,6 +2,8 @@ NEXT_PUBLIC_AUTH_CALLBACK_URL=http://localhost:8006/auth/callback
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
NEXT_PUBLIC_APP_ENV=dev
## Supabase credentials

View File

@ -46,23 +46,24 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@sentry/nextjs": "^8",
"@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.46.2",
"@supabase/supabase-js": "^2.46.1",
"@tanstack/react-table": "^8.20.5",
"@xyflow/react": "^12.3.5",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"cookie": "1.0.2",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"elliptic": "6.6.1",
"launchdarkly-react-client-sdk": "^3.6.0",
"lucide-react": "^0.462.0",
"moment": "^2.30.1",
"next": "^14.2.13",
"next-themes": "^0.4.3",
"react": "^18",
"react-day-picker": "^9.4.1",
"react-day-picker": "^9.4.0",
"react-dom": "^18",
"react-hook-form": "^7.53.2",
"react-icons": "^5.3.0",

View File

@ -7,6 +7,7 @@ import { BackendAPIProvider } from "@/lib/autogpt-server-api";
import { TooltipProvider } from "@/components/ui/tooltip";
import SupabaseProvider from "@/components/SupabaseProvider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
@ -14,7 +15,9 @@ export function Providers({ children, ...props }: ThemeProviderProps) {
<SupabaseProvider>
<BackendAPIProvider>
<CredentialsProvider>
<TooltipProvider>{children}</TooltipProvider>
<LaunchDarklyProvider>
<TooltipProvider>{children}</TooltipProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</SupabaseProvider>

View File

@ -0,0 +1,17 @@
import { LDProvider } from "launchdarkly-react-client-sdk";
import { ReactNode } from "react";
export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
if (
process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === true &&
!process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID
) {
throw new Error("NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID is not defined");
}
return (
<LDProvider clientSideID={process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID}>
{children}
</LDProvider>
);
}

View File

@ -0,0 +1,46 @@
"use client";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
export function withFeatureFlag<P extends object>(
WrappedComponent: React.ComponentType<P>,
flagKey: string,
) {
return function FeatureFlaggedComponent(props: P) {
const flags = useFlags();
const router = useRouter();
const [hasFlagLoaded, setHasFlagLoaded] = useState(false);
useEffect(() => {
// Only proceed if flags received
if (flags && flagKey in flags) {
setHasFlagLoaded(true);
}
}, [flags, flagKey]);
useEffect(() => {
if (hasFlagLoaded && !flags[flagKey]) {
router.push("/404");
}
}, [hasFlagLoaded, flags, flagKey, router]);
// Show loading state until flags loaded
if (!hasFlagLoaded) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
);
}
// If flag is loaded but false, return null (will redirect)
if (!flags[flagKey]) {
return null;
}
// Flag is loaded and true, show component
return <WrappedComponent {...props} />;
};
}

File diff suppressed because it is too large Load Diff