285 lines
7.0 KiB
TypeScript
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)'
|
|
};
|
|
} |