nextav/src/lib/feature-flags.ts

285 lines
7.0 KiB
TypeScript

/**
* 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>): 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<Response>) {
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)'
};
}