/** * Feature flag system for gradual ArtPlayer rollout * Controls which users get access to the new player */ export interface FeatureFlags { enableArtPlayer: boolean; enableHLS: boolean; enableAdvancedFeatures: boolean; enableAnalytics: boolean; } /** * Hash function to consistently map user/video combinations to percentages */ 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 32bit integer } return Math.abs(hash); } /** * Get feature flags based on user and video context */ export function getFeatureFlags(userId?: string, videoId?: string): FeatureFlags { const flags: FeatureFlags = { enableArtPlayer: false, enableHLS: false, enableAdvancedFeatures: false, enableAnalytics: true // Always enable analytics }; // If no user ID, enable basic ArtPlayer for testing if (!userId) { flags.enableArtPlayer = true; flags.enableHLS = false; flags.enableAdvancedFeatures = false; return flags; } // Phase 1: Enable ArtPlayer for 10% of users for native formats (MP4/WebM) const artPlayerHash = hashString(`${userId}:artplayer`); flags.enableArtPlayer = (artPlayerHash % 100) < 10; // Phase 2: Enable HLS for 5% of users (for broader format support) const hlsHash = hashString(`${userId}:hls`); flags.enableHLS = (hlsHash % 100) < 5; // Phase 3: Enable advanced features for 2% of users const advancedHash = hashString(`${userId}:advanced`); flags.enableAdvancedFeatures = (advancedHash % 100) < 2; // Video-specific overrides if (videoId) { // Always enable ArtPlayer for MP4/WebM videos in development if (process.env.NODE_ENV === 'development') { const videoHash = hashString(`${userId}:${videoId}`); if ((videoHash % 100) < 50) { flags.enableArtPlayer = true; } } // Enable HLS for specific video formats const videoFormatHash = hashString(`${userId}:${videoId}:format`); if ((videoFormatHash % 100) < 20) { flags.enableHLS = true; } } return flags; } /** * Check if user should use ArtPlayer for a specific video */ export function shouldUseArtPlayer(userId?: string, videoId?: string, videoFormat?: string): boolean { const flags = getFeatureFlags(userId, videoId); // Always use ArtPlayer for native formats if enabled if (flags.enableArtPlayer && videoFormat) { const nativeFormats = ['mp4', 'webm', 'ogg', 'ogv']; const extension = videoFormat.toLowerCase(); if (nativeFormats.includes(extension)) { return true; } } // Use ArtPlayer with HLS for other formats if HLS is enabled if (flags.enableHLS) { return true; } return false; } /** * Get rollout percentage for different phases */ export function getRolloutPercentage(phase: 'artplayer' | 'hls' | 'advanced'): number { switch (phase) { case 'artplayer': return 10; // 10% of users case 'hls': return 5; // 5% of users case 'advanced': return 2; // 2% of users default: return 0; } } /** * Override feature flags (for testing and admin purposes) */ export function overrideFeatureFlags(flags: Partial): FeatureFlags { const defaultFlags = getFeatureFlags(); return { ...defaultFlags, ...flags }; } /** * Feature flag configuration for different environments */ export function getEnvironmentConfig(): { enableGradualRollout: boolean; defaultFlags: FeatureFlags; } { switch (process.env.NODE_ENV) { case 'development': return { enableGradualRollout: false, // Enable all features in development defaultFlags: { enableArtPlayer: true, enableHLS: true, enableAdvancedFeatures: true, enableAnalytics: true } }; case 'production': return { enableGradualRollout: true, defaultFlags: getFeatureFlags() }; default: return { enableGradualRollout: true, defaultFlags: getFeatureFlags() }; } } /** * Analytics tracking for feature flag usage */ export interface FlagUsageMetrics { artPlayerImpressions: number; hlsImpressions: number; advancedFeatureImpressions: number; fallbackToCurrentPlayer: number; artPlayerErrors: number; } let usageMetrics: FlagUsageMetrics = { artPlayerImpressions: 0, hlsImpressions: 0, advancedFeatureImpressions: 0, fallbackToCurrentPlayer: 0, artPlayerErrors: 0 }; /** * Track feature flag usage */ export function trackFlagUsage(flag: keyof FeatureFlags, success: boolean = true) { switch (flag) { case 'enableArtPlayer': if (success) { usageMetrics.artPlayerImpressions++; } else { usageMetrics.artPlayerErrors++; usageMetrics.fallbackToCurrentPlayer++; } break; case 'enableHLS': if (success) { usageMetrics.hlsImpressions++; } break; case 'enableAdvancedFeatures': if (success) { usageMetrics.advancedFeatureImpressions++; } break; } } /** * Get usage metrics */ export function getUsageMetrics(): FlagUsageMetrics { return { ...usageMetrics }; } /** * Reset usage metrics */ export function resetUsageMetrics() { usageMetrics = { artPlayerImpressions: 0, hlsImpressions: 0, advancedFeatureImpressions: 0, fallbackToCurrentPlayer: 0, artPlayerErrors: 0 }; } /** * Feature flag middleware for API routes */ export function withFeatureFlags(handler: (flags: FeatureFlags, request: Request) => Promise) { return async (request: Request) => { // Extract user ID from request (could be from session, token, etc.) const userId = extractUserId(request); const videoId = extractVideoId(request); const flags = getFeatureFlags(userId, videoId); return handler(flags, request); }; } /** * Extract user ID from request (placeholder implementation) */ function extractUserId(request: Request): string | undefined { // This would typically come from session, JWT token, or user context // For now, return a placeholder or undefined const url = new URL(request.url); return url.searchParams.get('userId') || undefined; } /** * Extract video ID from request (placeholder implementation) */ function extractVideoId(request: Request): string | undefined { const url = new URL(request.url); const pathParts = url.pathname.split('/'); const videoId = pathParts.find(part => !isNaN(parseInt(part))); return videoId; } /** * A/B testing utilities */ export interface ABTestResult { variant: 'artplayer' | 'current'; reason: string; } /** * Determine A/B test variant for user */ export function getABTestVariant(userId?: string, videoId?: string): ABTestResult { const flags = getFeatureFlags(userId, videoId); if (flags.enableArtPlayer) { return { variant: 'artplayer', reason: 'ArtPlayer enabled via feature flag' }; } return { variant: 'current', reason: 'Using current player (ArtPlayer not enabled)' }; }