✨ feat: Add client-side feature flag system#3
✨ feat: Add client-side feature flag system#3ForisKuang wants to merge 1 commit intocBioPortal:v0.8.2-custom-v3from
Conversation
Adds a lightweight feature flag store using Jotai atoms with URL query param and localStorage support. Flags can be activated by visiting ?featureFlags=FLAG_NAME and are persisted across page reloads. This enables developers to test features in production without exposing them to all users.
There was a problem hiding this comment.
Pull request overview
This PR introduces a client-side feature flag system that enables developers to test features in production without exposing them to all users. The implementation uses Jotai atoms for state management and supports activation via URL query parameters or localStorage, with automatic persistence across page reloads.
Changes:
- Added a Jotai-based feature flag store with URL query param and localStorage support
- Created a React hook
useFeatureFlagfor checking flag status in components - Exported feature flag utilities from store and hooks index files
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| client/src/store/featureFlags.ts | Core feature flag store implementation with atoms for reading/writing flags, URL parameter parsing, and localStorage persistence |
| client/src/hooks/useFeatureFlag.ts | React hook for checking feature flag status in components |
| client/src/store/index.ts | Added export of feature flag store exports |
| client/src/hooks/index.ts | Added export of useFeatureFlag hook |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const initialFlags = typeof window !== 'undefined' ? readFlags() : new Set<string>(); | ||
|
|
||
| export const featureFlagsAtom = atom<Set<string>>(initialFlags); |
There was a problem hiding this comment.
The implementation doesn't use the existing createStorageAtom utility from ~/store/jotai-utils, which provides proper SSR support and error handling. The current manual localStorage access could fail during SSR when window is undefined. Consider refactoring to use createStorageAtom for consistency with other Jotai-based stores like favoritesAtom (client/src/store/favorites.ts:19) and proper SSR support with getOnInit: true option.
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const removeFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.delete(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const clearFeatureFlagsAtom = atom(null, (_get, set) => { | ||
| set(featureFlagsAtom, new Set<string>()); | ||
| localStorage.removeItem(FEATURE_FLAG_KEY); |
There was a problem hiding this comment.
Direct localStorage access without try-catch blocks could throw exceptions if localStorage is disabled, quota is exceeded, or in private browsing mode. This is inconsistent with the error handling pattern seen in jotai-utils.ts:59-87 where localStorage operations are wrapped in try-catch blocks with fallback to default values.
| function readFlags(): Set<string> { | ||
| const stored = localStorage.getItem(FEATURE_FLAG_KEY); | ||
| const fromStorage = stored ? stored.split(DELIMITER).filter(Boolean) : []; | ||
|
|
||
| const url = new URL(window.location.href); | ||
| const param = url.searchParams.get(FEATURE_FLAG_KEY); | ||
| const fromUrl = param ? param.split(DELIMITER).filter(Boolean) : []; | ||
|
|
||
| const merged = new Set([...fromStorage, ...fromUrl]); | ||
|
|
||
| if (fromUrl.length > 0) { | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...merged].join(DELIMITER)); | ||
| } | ||
|
|
||
| return merged; | ||
| } |
There was a problem hiding this comment.
The readFlags() function directly accesses window and localStorage without checking if they exist. While line 38 has a typeof window !== 'undefined' check, this doesn't protect the initial call to readFlags() itself. During server-side rendering or in Node.js environments, this will throw a ReferenceError when window is not defined.
| export const addFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.add(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const removeFeatureFlagAtom = atom(null, (get, set, flag: string) => { | ||
| const current = get(featureFlagsAtom); | ||
| const next = new Set(current); | ||
| next.delete(flag); | ||
| set(featureFlagsAtom, next); | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...next].join(DELIMITER)); | ||
| }); | ||
|
|
||
| export const clearFeatureFlagsAtom = atom(null, (_get, set) => { | ||
| set(featureFlagsAtom, new Set<string>()); | ||
| localStorage.removeItem(FEATURE_FLAG_KEY); | ||
| }); |
There was a problem hiding this comment.
The write atoms (addFeatureFlagAtom, removeFeatureFlagAtom, clearFeatureFlagsAtom) access localStorage without environment checks. In SSR or when localStorage is unavailable, these operations will fail. Similar patterns in the codebase include environment checks, such as in createStorageAtomWithEffect (jotai-utils.ts:38) which checks typeof window !== 'undefined' before executing side effects.
| if (fromUrl.length > 0) { | ||
| localStorage.setItem(FEATURE_FLAG_KEY, [...merged].join(DELIMITER)); | ||
| } |
There was a problem hiding this comment.
Feature flags read from URL query parameters are user-controlled input that gets stored in localStorage without validation. While the current implementation with string Set storage is relatively safe, consider validating that URL parameter values match known FeatureFlag enum values before persisting them. This prevents arbitrary strings from being stored and improves type safety.
|
|
||
| export default { | ||
| featureFlagsAtom, | ||
| addFeatureFlagAtom, | ||
| removeFeatureFlagAtom, | ||
| clearFeatureFlagsAtom, | ||
| }; |
There was a problem hiding this comment.
The default export object at the end of the file is inconsistent with other Jotai-based store files in the codebase (favorites.ts, mcp.ts) which only use named exports. Since the store/index.ts uses export * from './featureFlags', this default export is not utilized and creates unnecessary inconsistency. Consider removing it.
| export default { | |
| featureFlagsAtom, | |
| addFeatureFlagAtom, | |
| removeFeatureFlagAtom, | |
| clearFeatureFlagsAtom, | |
| }; |
…anny-avila#12117) * chore: Remove unused setValueOnChange prop from MCPServerMenuItem component * fix: Resolve agent provider endpoint type for file upload support When using the agents endpoint with a custom provider (e.g., Moonshot), the endpointType was resolving to "agents" instead of the provider's actual type ("custom"), causing "Upload to Provider" to not appear in the file attach menu. Adds `resolveEndpointType` utility in data-provider that follows the chain: endpoint (if not agents) → agent.provider → agents. Applied consistently across AttachFileChat, DragDropContext, useDragHelpers, and AgentPanel file components (FileContext, FileSearch, Code/Files). * refactor: Extract useAgentFileConfig hook, restore deleted tests, fix review findings - Extract shared provider resolution logic into useAgentFileConfig hook (Finding #2: DRY violation across FileContext, FileSearch, Code/Files) - Restore 18 deleted test cases in AttachFileMenu.spec.tsx covering agent capabilities, SharePoint, edge cases, and button state (Finding #1: accidental test deletion) - Wrap fileConfigEndpoint in useMemo in AttachFileChat (Finding #3) - Fix misleading test name in AgentFileConfig.spec.tsx (Finding #4) - Fix import order in FileSearch.tsx, FileContext.tsx, Code/Files.tsx (Finding #5) - Add comment about cache gap in useDragHelpers (Finding #6) - Clarify resolveEndpointType JSDoc (Finding #7) * refactor: Memoize Footer component for performance optimization - Converted Footer component to a memoized version to prevent unnecessary re-renders. - Improved import structure by adding memo to the React import statement for clarity. * chore: Fix remaining review nits - Widen useAgentFileConfig return type to EModelEndpoint | string - Fix import order in FileContext.tsx and FileSearch.tsx - Remove dead endpointType param from setupMocks in AttachFileMenu test * fix: Pass resolved provider endpoint to file upload validation AgentPanel file components (FileContext, FileSearch, Code/Files) were hardcoding endpointOverride to "agents", causing both client-side validation (file limits, MIME types) and server-side validation to use the agents config instead of the provider-specific config. Adds endpointTypeOverride to UseFileHandling params so endpoint and endpointType can be set independently. Components now pass the resolved provider name and type from useAgentFileConfig, so the full fallback chain (provider → custom → agents → default) applies to file upload validation on both client and server. * test: Verify any custom endpoint is document-supported regardless of name Adds parameterized tests with arbitrary endpoint names (spaces, hyphens, colons, etc.) confirming that all custom endpoints resolve to document-supported through resolveEndpointType, both as direct endpoints and as agent providers. * fix: Use || for provider fallback, test endpointOverride wiring - Change providerValue ?? to providerValue || so empty string is treated as "no provider" consistently with resolveEndpointType - Add wiring tests to CodeFiles, FileContext, FileSearch verifying endpointOverride and endpointTypeOverride are passed correctly - Update endpointOverride JSDoc to document endpointType fallback
Adds a lightweight feature flag store using Jotai atoms with URL query param and localStorage support. Flags can be activated by visiting ?featureFlags=FLAG_NAME and are persisted across page reloads. This enables developers to test features in production without exposing them to all users.
Summary
?featureFlags=FLAG_NAME) or localStorage, enabling developers to test features in production without exposing them to all usersChanges
New Files
client/src/store/featureFlags.ts— Jotai-based feature flag store withFeatureFlagenum, read/write atoms, and URL + localStorage initializationclient/src/hooks/useFeatureFlag.ts—useFeatureFlag(flag)React hook that returns a booleanModified Files
client/src/store/index.ts— Export feature flag atoms and enumclient/src/hooks/index.ts— ExportuseFeatureFlaghookUsage
Define a flag
Test Plan
EOF
)"