refactor(frontend): improve LaunchDarkly provider initialization (#10422)

### Changes ️

The previous implementation of the `LaunchDarklyProvider` had a race
condition where it would only initialize after the user's authentication
state was fully resolved. This caused two primary issues:

1. A delay in evaluating any feature flags, leading to a "flash of
un-styled/un-flagged content" until the user session was loaded.
2. An unreliable transition from an un-flagged state to a flagged state,
which could cause UI flicker or incorrect flag evaluations upon login.


This pull request refactors the provider to follow a more robust,
industry-standard pattern. It now initializes immediately with an
`anonymous` context, ensuring flags are available from the very start of
the application lifecycle. When the user logs in and their session
becomes available, the provider seamlessly transitions to an
authenticated context, guaranteeing that the correct flags are evaluated
consistently.

### Checklist 

#### For code changes:

- I have clearly listed my changes in the PR description
- I have made a test plan
- I have tested my changes according to the test plan:

	**Test Plan:**
	
- [x] **Anonymous User:** Load the application in an incognito window
without logging in. Verify that feature flags are evaluated correctly
for an anonymous user. Check the browser console for the
`[LaunchDarklyProvider] Using anonymous context` message.
- [x] **Login Flow:** While on the site, log in. Verify that the UI
updates with the correct feature flags for the authenticated user. Check
the console for the `[LaunchDarklyProvider] Using authenticated context`
message and confirm the LaunchDarkly client re-initializes.
- [x] **Authenticated User (Page Refresh):** As a logged-in user,
refresh the page. Verify that the application loads directly with the
authenticated user's flags, leveraging the cached session and
bootstrapped flags from `localStorage`.
- [x] **Logout Flow:** While logged in, log out. Verify that the UI
reverts to the anonymous user's state and flags. The provider `key`
should change back to "anonymous", triggering another re-mount.





<details><summary>Summary of Code Changes</summary>

- Refactored `LaunchDarklyProvider` to handle user authentication state
changes gracefully.
- The provider now initializes immediately with an `anonymous` user
context while the Supabase user session is loading.
- Once the user is authenticated, the provider's context is updated to
reflect the logged-in user's details.
- Added a `key` prop to the `<LDProvider>` component, using the user's
ID (or "anonymous"). This forces React to re-mount the provider when the
user's identity changes, ensuring a clean re-initialization of the
LaunchDarkly SDK.
- Enabled `localStorage` bootstrapping (`options={{ bootstrap:
"localStorage" }}`) to cache flags and improve performance on subsequent
page loads.
- Added `console.debug` statements for improved observability into the
provider's state (anonymous vs. authenticated).


</details>

#### For configuration changes:

- `.env.example` is updated or already compatible with my changes
- `docker-compose.yml` is updated or already compatible with my changes
- I have included a list of my configuration changes in the PR
description (under **Changes**)


<details>
<summary>Configuration Changes</summary>

- No configuration changes were made. This PR relies on existing
environment variables (`NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID` and
`NEXT_PUBLIC_LAUNCHDARKLY_ENABLED`).


</details>

---------

Co-authored-by: Lluis Agusti <hi@llu.lu>
This commit is contained in:
Nicholas Tindle
2025-07-22 06:47:04 -05:00
committed by GitHub
parent f4a179e5d6
commit ae6ef8c0c2

View File

@@ -1,7 +1,8 @@
"use client";
import { LDProvider } from "launchdarkly-react-client-sdk";
import { ReactNode } from "react";
import type { ReactNode } from "react";
import { useMemo } from "react";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { BehaveAs, getBehaveAs } from "@/lib/utils";
@@ -9,33 +10,59 @@ const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
const { user } = useSupabase();
const { user, isUserLoading } = useSupabase();
const isCloud = getBehaveAs() === BehaveAs.CLOUD;
const enabled = isCloud && envEnabled && clientId && user;
const isLaunchDarklyConfigured = isCloud && envEnabled && clientId;
if (!enabled) return <>{children}</>;
const userContext = user
? {
kind: "user",
key: user.id,
email: user.email,
anonymous: false,
custom: {
role: user.role,
},
}
: {
kind: "user",
const context = useMemo(() => {
if (isUserLoading || !user) {
console.debug("[LaunchDarklyProvider] Using anonymous context", {
isUserLoading,
hasUser: !!user,
});
return {
kind: "user" as const,
key: "anonymous",
anonymous: true,
};
}
console.debug("[LaunchDarklyProvider] Using authenticated context", {
userId: user.id,
email: user.email,
role: user.role,
});
return {
kind: "user" as const,
key: user.id,
...(user.email && { email: user.email }),
anonymous: false,
custom: {
...(user.role && { role: user.role }),
},
};
}, [user, isUserLoading]);
if (!isLaunchDarklyConfigured) {
console.debug(
"[LaunchDarklyProvider] Not configured for this environment",
{
isCloud,
envEnabled,
hasClientId: !!clientId,
},
);
return <>{children}</>;
}
return (
<LDProvider
// Add this key prop. It will be 'anonymous' when logged out,
key={context.key}
clientSideID={clientId}
context={userContext}
context={context}
reactOptions={{ useCamelCaseFlagKeys: false }}
options={{ bootstrap: "localStorage" }}
>
{children}
</LDProvider>