Files
gh-hopeoverture-worldbuildi…/skills/feature-flag-manager/assets/feature-flag-provider.tsx
2025-11-29 18:46:20 +08:00

164 lines
3.7 KiB
TypeScript

/**
* Feature Flag Provider - JSON-based implementation
*/
import { createContext, useContext, ReactNode } from 'react';
// Feature flag configuration type
export interface FeatureFlagConfig {
enabled: boolean;
description?: string;
rolloutPercentage?: number;
allowedUsers?: string[];
environments?: Record<string, boolean>;
}
export interface FeatureFlags {
[key: string]: FeatureFlagConfig;
}
// Context for feature flags
const FeatureFlagContext = createContext<FeatureFlags>({});
interface FeatureFlagProviderProps {
children: ReactNode;
flags: FeatureFlags;
userId?: string;
environment?: string;
}
/**
* Provider component that makes feature flags available to the app
*/
export function FeatureFlagProvider({
children,
flags,
userId,
environment = process.env.NODE_ENV,
}: FeatureFlagProviderProps) {
// Evaluate flags based on environment and user
const evaluatedFlags = evaluateFlags(flags, { userId, environment });
return (
<FeatureFlagContext.Provider value={evaluatedFlags}>
{children}
</FeatureFlagContext.Provider>
);
}
/**
* Hook to access feature flags
*/
export function useFeatureFlags() {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureFlags must be used within FeatureFlagProvider');
}
return context;
}
/**
* Hook to check if a specific feature flag is enabled
*/
export function useFeatureFlag(flagName: string): boolean {
const flags = useFeatureFlags();
const flag = flags[flagName];
if (!flag) {
console.warn(`Feature flag "${flagName}" not found, defaulting to false`);
return false;
}
return flag.enabled;
}
/**
* Hook for multivariate flags (A/B testing)
*/
export function useFeatureFlagVariant<T extends string>(
flagName: string,
variants: Record<string, T>,
defaultVariant: T
): T {
const flags = useFeatureFlags();
const flag = flags[flagName];
if (!flag || !flag.enabled) {
return defaultVariant;
}
// Simple hash-based variant assignment
const variantKeys = Object.keys(variants);
const hash = hashString(flagName + (flag.allowedUsers?.[0] || ''));
const index = hash % variantKeys.length;
return variants[variantKeys[index]] || defaultVariant;
}
/**
* Evaluate flags based on context
*/
function evaluateFlags(
flags: FeatureFlags,
context: { userId?: string; environment?: string }
): FeatureFlags {
const evaluated: FeatureFlags = {};
for (const [key, config] of Object.entries(flags)) {
evaluated[key] = {
...config,
enabled: evaluateFlag(config, context),
};
}
return evaluated;
}
/**
* Evaluate a single flag
*/
function evaluateFlag(
config: FeatureFlagConfig,
context: { userId?: string; environment?: string }
): boolean {
// Check environment-specific override
if (config.environments && context.environment) {
const envEnabled = config.environments[context.environment];
if (envEnabled !== undefined) {
return envEnabled;
}
}
// Check user allowlist
if (config.allowedUsers && config.allowedUsers.length > 0 && context.userId) {
if (config.allowedUsers.includes(context.userId)) {
return config.enabled;
}
return false;
}
// Check rollout percentage
if (config.rolloutPercentage !== undefined && context.userId) {
const hash = hashString(context.userId);
const bucket = hash % 100;
if (bucket >= config.rolloutPercentage) {
return false;
}
}
return config.enabled;
}
/**
* Simple string hash function
*/
function hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}