@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
JavaScript
/**
* 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;
}