UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

438 lines (379 loc) 13.6 kB
/** * Feature Flags System * Cart System Refactor - Phase 5 Deployment * * Enables gradual rollout and A/B testing of new features */ class FeatureFlags { constructor() { this.flags = new Map(); this.userSegments = new Map(); this.experiments = new Map(); this.listeners = new Map(); // Initialize default flags this.initializeDefaultFlags(); } /** * Initialize default feature flags */ initializeDefaultFlags() { const defaultFlags = { // Cart System Features newCartSystem: { enabled: true, rolloutPercentage: 100, description: 'New cart system with improved performance', environments: ['development', 'staging', 'production'] }, orderManagement: { enabled: true, rolloutPercentage: 100, description: 'Enhanced order management system', environments: ['development', 'staging', 'production'] }, paymentIntegration: { enabled: true, rolloutPercentage: 100, description: 'Integrated payment processing with Paystack', environments: ['development', 'staging', 'production'] }, rentalSystem: { enabled: true, rolloutPercentage: 100, description: 'Equipment rental system with referee validation', environments: ['development', 'staging', 'production'] }, bookingSystem: { enabled: true, rolloutPercentage: 100, description: 'Service booking system', environments: ['development', 'staging', 'production'] }, // Performance Features performanceMonitoring: { enabled: true, rolloutPercentage: 100, description: 'Real-time performance monitoring', environments: ['development', 'staging', 'production'] }, errorTracking: { enabled: true, rolloutPercentage: 100, description: 'Comprehensive error tracking and reporting', environments: ['development', 'staging', 'production'] }, // Testing Features testingDashboard: { enabled: true, rolloutPercentage: 100, description: 'Comprehensive testing dashboard', environments: ['development', 'staging'] }, systemStatusDashboard: { enabled: true, rolloutPercentage: 100, description: 'System health monitoring dashboard', environments: ['development', 'staging', 'production'] }, // Experimental Features advancedAnalytics: { enabled: false, rolloutPercentage: 0, description: 'Advanced user behavior analytics', environments: ['development'] }, aiRecommendations: { enabled: false, rolloutPercentage: 0, description: 'AI-powered product recommendations', environments: ['development'] }, realTimeNotifications: { enabled: false, rolloutPercentage: 10, description: 'Real-time push notifications', environments: ['development', 'staging'] } }; Object.entries(defaultFlags).forEach(([key, config]) => { this.flags.set(key, { ...config, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); }); } /** * Check if a feature is enabled for a user * @param {string} flagName - Name of the feature flag * @param {Object} context - User context (userId, userType, etc.) * @returns {boolean} - Whether the feature is enabled */ isEnabled(flagName, context = {}) { const flag = this.flags.get(flagName); if (!flag) { console.warn(`Feature flag '${flagName}' not found`); return false; } // Check if flag is globally disabled if (!flag.enabled) { return false; } // Check environment const currentEnv = process.env.NODE_ENV || 'development'; if (!flag.environments.includes(currentEnv)) { return false; } // Check user segment if (context.userId && this.userSegments.has(context.userId)) { const userSegment = this.userSegments.get(context.userId); if (userSegment.flags && userSegment.flags[flagName] !== undefined) { return userSegment.flags[flagName]; } } // Check rollout percentage if (flag.rolloutPercentage < 100) { const hash = this.hashUserId(context.userId || 'anonymous', flagName); return hash < flag.rolloutPercentage; } return true; } /** * Enable a feature flag * @param {string} flagName - Name of the feature flag * @param {Object} options - Configuration options */ enable(flagName, options = {}) { const flag = this.flags.get(flagName) || {}; this.flags.set(flagName, { ...flag, enabled: true, rolloutPercentage: options.rolloutPercentage || 100, environments: options.environments || flag.environments || ['development'], updatedAt: new Date().toISOString(), ...options }); this.notifyListeners(flagName, true); } /** * Disable a feature flag * @param {string} flagName - Name of the feature flag */ disable(flagName) { const flag = this.flags.get(flagName); if (flag) { this.flags.set(flagName, { ...flag, enabled: false, updatedAt: new Date().toISOString() }); this.notifyListeners(flagName, false); } } /** * Set rollout percentage for gradual rollout * @param {string} flagName - Name of the feature flag * @param {number} percentage - Rollout percentage (0-100) */ setRolloutPercentage(flagName, percentage) { const flag = this.flags.get(flagName); if (flag) { this.flags.set(flagName, { ...flag, rolloutPercentage: Math.max(0, Math.min(100, percentage)), updatedAt: new Date().toISOString() }); this.notifyListeners(flagName, flag.enabled); } } /** * Add user to a specific segment * @param {string} userId - User ID * @param {string} segment - Segment name * @param {Object} flags - User-specific flag overrides */ addUserToSegment(userId, segment, flags = {}) { this.userSegments.set(userId, { segment, flags, addedAt: new Date().toISOString() }); } /** * Remove user from segments * @param {string} userId - User ID */ removeUserFromSegment(userId) { this.userSegments.delete(userId); } /** * Create an A/B test experiment * @param {string} experimentName - Name of the experiment * @param {Object} config - Experiment configuration */ createExperiment(experimentName, config) { this.experiments.set(experimentName, { ...config, createdAt: new Date().toISOString(), status: 'active' }); } /** * Get experiment variant for a user * @param {string} experimentName - Name of the experiment * @param {string} userId - User ID * @returns {string} - Variant name */ getExperimentVariant(experimentName, userId) { const experiment = this.experiments.get(experimentName); if (!experiment || experiment.status !== 'active') { return experiment?.defaultVariant || 'control'; } const hash = this.hashUserId(userId, experimentName); let cumulativePercentage = 0; for (const [variant, percentage] of Object.entries(experiment.variants)) { cumulativePercentage += percentage; if (hash < cumulativePercentage) { return variant; } } return experiment.defaultVariant || 'control'; } /** * Get all feature flags * @returns {Object} - All feature flags */ getAllFlags() { const flags = {}; this.flags.forEach((config, name) => { flags[name] = config; }); return flags; } /** * Get flags for a specific user * @param {Object} context - User context * @returns {Object} - User-specific flags */ getFlagsForUser(context = {}) { const userFlags = {}; this.flags.forEach((config, name) => { userFlags[name] = this.isEnabled(name, context); }); return userFlags; } /** * Add listener for flag changes * @param {string} flagName - Name of the feature flag * @param {Function} callback - Callback function */ addListener(flagName, callback) { if (!this.listeners.has(flagName)) { this.listeners.set(flagName, []); } this.listeners.get(flagName).push(callback); } /** * Remove listener for flag changes * @param {string} flagName - Name of the feature flag * @param {Function} callback - Callback function */ removeListener(flagName, callback) { const listeners = this.listeners.get(flagName); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } /** * Notify listeners of flag changes * @param {string} flagName - Name of the feature flag * @param {boolean} enabled - Whether the flag is enabled */ notifyListeners(flagName, enabled) { const listeners = this.listeners.get(flagName); if (listeners) { listeners.forEach(callback => { try { callback(flagName, enabled); } catch (error) { console.error(`Error in feature flag listener for ${flagName}:`, error); } }); } } /** * Hash user ID for consistent rollout * @param {string} userId - User ID * @param {string} salt - Salt for hashing * @returns {number} - Hash value (0-100) */ hashUserId(userId, salt = '') { const str = userId + salt; 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) % 100; } /** * Export flags configuration * @returns {Object} - Serializable flags configuration */ export() { return { flags: Object.fromEntries(this.flags), userSegments: Object.fromEntries(this.userSegments), experiments: Object.fromEntries(this.experiments), exportedAt: new Date().toISOString() }; } /** * Import flags configuration * @param {Object} config - Flags configuration */ import(config) { if (config.flags) { this.flags = new Map(Object.entries(config.flags)); } if (config.userSegments) { this.userSegments = new Map(Object.entries(config.userSegments)); } if (config.experiments) { this.experiments = new Map(Object.entries(config.experiments)); } } /** * Get feature flag statistics * @returns {Object} - Statistics about feature flags */ getStatistics() { const totalFlags = this.flags.size; const enabledFlags = Array.from(this.flags.values()).filter(flag => flag.enabled).length; const experimentsCount = this.experiments.size; const userSegmentsCount = this.userSegments.size; return { totalFlags, enabledFlags, disabledFlags: totalFlags - enabledFlags, experimentsCount, userSegmentsCount, rolloutFlags: Array.from(this.flags.values()).filter(flag => flag.rolloutPercentage < 100).length }; } } // Singleton instance const featureFlags = new FeatureFlags(); // Export both the class and the singleton instance module.exports = { FeatureFlags, featureFlags }; // For ES6 modules if (typeof module !== 'undefined' && module.exports) { module.exports.default = featureFlags; }