UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

524 lines 22.7 kB
/** * Configurable Defaults Manager * * This class manages user-configurable defaults for Optimizely entities, * solving the jQuery 1.11.3 problem by allowing users to set modern defaults. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { FIELDS } from '../generated/fields.generated.js'; import { getLogger } from '../logging/Logger.js'; export class ConfigurableDefaultsManager { sessionDefaults = null; fileDefaults = null; environmentDefaults = null; builtInDefaults; configPath; configSource = 'built-in'; constructor() { this.builtInDefaults = this.loadBuiltInDefaults(); this.environmentDefaults = this.loadEnvironmentDefaults(); this.loadUserDefaults(); } /** * Set defaults from chat interface */ setDefaults(configuration) { this.sessionDefaults = configuration; this.configSource = 'session'; getLogger().info('Defaults configured from chat session'); } /** * Save session defaults to file */ async saveToFile(configuration) { const toSave = configuration || this.sessionDefaults || this.fileDefaults; if (!toSave) { throw new Error('No configuration to save'); } const filePath = '.optimizely/defaults.json'; await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, JSON.stringify(toSave, null, 2)); this.configPath = filePath; return filePath; } /** * Load built-in modern defaults that override legacy Optimizely defaults */ loadBuiltInDefaults() { return { // Project defaults (both platforms) project: { platform: "web", // Default for Web projects (will be removed if is_flags_enabled=true) description: "Created via MCP Server", // CRITICAL: Platform determines project type! // platform: "web" = Web Experimentation project (is_flags_enabled: false/undefined) // platform: "custom" = Feature Experimentation project (is_flags_enabled: true) // platform: "ios"/"android" = Legacy → converted to Feature Experimentation (platform: "custom", is_flags_enabled: true) // DO NOT SET is_flags_enabled here - it's auto-set based on platform value web_snippet: { include_jquery: false, // SOLUTION: Override default true library: "none", // SOLUTION: Override jquery-1.11.3-trim ip_anonymization: true, // Modern: Privacy-first enable_force_variation: false, // Modern: Natural experiments exclude_disabled_experiments: false, exclude_names: true } }, // Feature Experimentation project defaults feature_project: { platform: "custom", // CRITICAL: Feature Experimentation requires platform: "custom" is_flags_enabled: true, // CRITICAL: This makes it a Feature project description: "Feature Experimentation project created via MCP Server" }, // Web Experimentation project defaults web_project: { platform: "web", is_flags_enabled: false, // Explicitly false for Web projects description: "Web Experimentation project created via MCP Server", web_snippet: { include_jquery: false, library: "none", ip_anonymization: true, enable_force_variation: false, exclude_disabled_experiments: false, exclude_names: true } }, // Web Experimentation defaults - "Ready to Test" philosophy page: { activation_type: "immediate", // 🔥 CRITICAL: Start experiments immediately category: "other" // Default page classification }, experiment: { type: "a/b", // Default experiment methodology audience_conditions: "everyone", // Default targeting percentage_included: 1.0, // Modern: Full traffic by default variations: [ { name: "Control", description: "Baseline experience", weight: 5000, actions: [ { changes: [] // Empty changes for control (no modifications) // page_id will be added by EntityOrchestrator based on template context } ] }, { name: "Treatment", description: "Test experience", weight: 5000, actions: [ { changes: [] // Empty changes - user will add specific actions for their test // page_id will be added by EntityOrchestrator based on template context } ] } ] }, variation: { // Web-specific defaults weight: 5000, // Default traffic split for A/B test status: "active" // Enable variations by default }, campaign: { type: "personalization", // Default campaign type status: "not_started" // Safe initial state }, // Feature Experimentation defaults - "Safe to Deploy" philosophy flag: { archived: false // Active flag }, ruleset: { enabled: false, // 🔥 CRITICAL: Start disabled for safety status: "draft", // 🔥 CRITICAL: Begin in draft mode archived: false // Active ruleset }, rule: { type: "targeted_delivery", // 🔥 CRITICAL: Default to feature rollout status: "draft", // 🔥 CRITICAL: Safe deployment state enabled: true, // Rule is active (but ruleset is disabled) percentage_included: 0, // 🔥 CRITICAL: No traffic exposure initially archived: false // Active rule }, variable_definition: { type: "boolean", // 🔥 CRITICAL: Safe default data type default_value: "false" // 🔥 CRITICAL: Conservative fallback }, variable: { type: "boolean", // 🔥 CRITICAL: Safe default data type default_value: "false", // 🔥 CRITICAL: Conservative fallback archived: false // Active variable }, fx_environment: { archived: false, // Active environment priority: 1 // Default priority }, feature: { archived: false // Active feature (legacy) }, // Shared entity defaults (both platforms) event: { event_type: "custom", // 🔥 CRITICAL: Modern custom events category: "other", // Default event classification is_classic: false, // Modern Optimizely archived: false // Active event }, audience: { archived: false, // Active audience segmentation: false, // Standard audience (not for segmentation) is_classic: false // Modern Optimizely }, attribute: { archived: false, // Active attribute condition_type: "custom_attribute" // Default attribute type }, extension: { enabled: true, // Auto-enable extensions archived: false // Active extension }, group: { archived: false // Active group (mutual exclusion) }, webhook: { active: true // Auto-enable webhooks } }; } /** * Load environment variable defaults * Supports patterns: * - OPTIMIZELY_{ENTITY}_{FIELD}={VALUE} (global) * - OPTIMIZELY_PROJECT_{ID}_{ENTITY}_{FIELD}={VALUE} (project-specific) * - OPTIMIZELY_{PLATFORM}_{ENTITY}_{FIELD}={VALUE} (platform-specific) */ loadEnvironmentDefaults() { const envDefaults = {}; let hasEnvironmentDefaults = false; Object.keys(process.env).forEach(key => { // Pattern 1: OPTIMIZELY_{ENTITY}_{FIELD}={VALUE} (global) let match = key.match(/^OPTIMIZELY_([A-Z_]+)_([A-Z_]+)$/); if (match) { const [, entityType, fieldName] = match; const value = this.parseEnvironmentValue(process.env[key]); const entity = entityType.toLowerCase(); const field = fieldName.toLowerCase(); // Skip platform names that could be confused with entity types if (!['web', 'feature', 'project'].includes(entity) || entity === 'project') { if (!envDefaults[entity]) { envDefaults[entity] = {}; } envDefaults[entity][field] = value; hasEnvironmentDefaults = true; } } // Pattern 2: OPTIMIZELY_PROJECT_{ID}_{ENTITY}_{FIELD}={VALUE} (project-specific) match = key.match(/^OPTIMIZELY_PROJECT_(\d+)_([A-Z_]+)_([A-Z_]+)$/); if (match) { const [, projectId, entityType, fieldName] = match; const value = this.parseEnvironmentValue(process.env[key]); const entity = entityType.toLowerCase(); const field = fieldName.toLowerCase(); if (!envDefaults._project_specific) { envDefaults._project_specific = {}; } if (!envDefaults._project_specific[projectId]) { envDefaults._project_specific[projectId] = {}; } if (!envDefaults._project_specific[projectId][entity]) { envDefaults._project_specific[projectId][entity] = {}; } envDefaults._project_specific[projectId][entity][field] = value; hasEnvironmentDefaults = true; } // Pattern 3: OPTIMIZELY_{PLATFORM}_{ENTITY}_{FIELD}={VALUE} (platform-specific) match = key.match(/^OPTIMIZELY_(WEB|FEATURE)_([A-Z_]+)_([A-Z_]+)$/); if (match) { const [, platform, entityType, fieldName] = match; const value = this.parseEnvironmentValue(process.env[key]); const platformKey = platform.toLowerCase(); const entity = entityType.toLowerCase(); const field = fieldName.toLowerCase(); if (!envDefaults._platform_specific) { envDefaults._platform_specific = {}; } if (!envDefaults._platform_specific[platformKey]) { envDefaults._platform_specific[platformKey] = {}; } if (!envDefaults._platform_specific[platformKey][entity]) { envDefaults._platform_specific[platformKey][entity] = {}; } envDefaults._platform_specific[platformKey][entity][field] = value; hasEnvironmentDefaults = true; } }); if (hasEnvironmentDefaults) { getLogger().info('Loaded configurable defaults from environment variables'); this.configSource = 'environment'; return { version: "environment", defaults: envDefaults }; } return null; } /** * Parse environment variable value with type inference */ parseEnvironmentValue(value) { // Handle boolean values if (value === 'true') return true; if (value === 'false') return false; // Handle numeric values if (/^\d+$/.test(value)) return parseInt(value, 10); if (/^\d*\.\d+$/.test(value)) return parseFloat(value); // Handle JSON values if (value.startsWith('{') || value.startsWith('[')) { try { return JSON.parse(value); } catch { return value; // Return as string if JSON parsing fails } } // Return as string return value; } /** * Load user defaults from configuration file */ async loadUserDefaults() { // Try environment variable path first const envPath = process.env.OPTIMIZELY_DEFAULTS_FILE; if (envPath && await this.fileExists(envPath)) { this.configPath = envPath; this.fileDefaults = await this.loadFromPath(envPath); this.configSource = 'user-file'; return; } // Try convention-based locations const conventionPaths = [ '.optimizely/defaults.json', 'optimizely-defaults.json' ]; for (const filePath of conventionPaths) { if (await this.fileExists(filePath)) { this.configPath = filePath; this.fileDefaults = await this.loadFromPath(filePath); this.configSource = 'user-file'; return; } } getLogger().info('No user defaults found, using built-in modern defaults'); } /** * Load configuration from a specific file path */ async loadFromPath(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const config = JSON.parse(content); getLogger().info(`Loaded user defaults from ${filePath}`); return config; } catch (error) { getLogger().warn(`Failed to load ${filePath}: ${error.message}`); throw error; } } /** * Check if file exists */ async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Get merged defaults for an entity type with context support * Priority: Session > File > Environment (project-specific > platform-specific > global) > Built-in > Swagger */ getDefaults(entityType, context) { const platform = context?.platform; const projectId = context?.projectId; const swaggerDefaults = FIELDS[entityType]?.defaults || {}; let builtInDefaults = this.builtInDefaults[entityType] || {}; // Apply platform-specific filtering to built-in defaults if (platform) { builtInDefaults = this.filterDefaultsByPlatform(builtInDefaults, platform, entityType); } // Environment defaults with hierarchy: global < platform-specific < project-specific const envGlobalDefaults = this.environmentDefaults?.defaults?.[entityType] || {}; const envPlatformDefaults = platform ? this.environmentDefaults?.defaults?._platform_specific?.[platform]?.[entityType] || {} : {}; const envProjectDefaults = projectId ? this.environmentDefaults?.defaults?._project_specific?.[projectId]?.[entityType] || {} : {}; const fileDefaults = this.fileDefaults?.defaults?.[entityType] || {}; const sessionDefaults = this.sessionDefaults?.defaults?.[entityType] || {}; // Deep merge with correct priority return this.deepMerge(swaggerDefaults, // Lowest priority (legacy defaults) builtInDefaults, // Override legacy with modern (platform-filtered) envGlobalDefaults, // Global environment defaults envPlatformDefaults, // Platform-specific environment defaults envProjectDefaults, // Project-specific environment defaults (highest env priority) fileDefaults, // User file preferences sessionDefaults // Session overrides everything (highest priority) ); } /** * Backward compatibility method */ getDefaultsLegacy(entityType, platform) { return this.getDefaults(entityType, { platform }); } /** * Filter defaults based on platform to prevent cross-platform field pollution */ filterDefaultsByPlatform(defaults, platform, entityType) { if (!defaults || typeof defaults !== 'object') { return defaults; } const filtered = { ...defaults }; // Platform-specific entity filtering const webOnlyEntities = ['page', 'campaign']; const featureOnlyEntities = ['flag', 'ruleset', 'rule', 'variable_definition', 'variable', 'fx_environment', 'feature']; // If this is a platform-specific entity and we're on the wrong platform, return empty if (platform === 'feature' && webOnlyEntities.includes(entityType)) { return {}; // No defaults for Web entities in Feature platform } if (platform === 'web' && featureOnlyEntities.includes(entityType)) { return {}; // No defaults for Feature entities in Web platform } // For shared entities (experiment, variation, event, audience, etc.), filter fields if (entityType === 'variation') { // For Feature Experimentation, remove Web-specific fields if (platform === 'feature') { delete filtered.actions; // Web only - variations use variables instead delete filtered.changes; // Web only delete filtered.page_id; // Web only delete filtered.weight; // Web only - FX uses percentage_included delete filtered.status; // Web only - FX variations don't have status field } // For Web Experimentation, remove Feature-specific fields if (platform === 'web') { delete filtered.variables; // Feature only - Web uses actions delete filtered.percentage_included; // Feature only - Web uses weight } } return filtered; } /** * Get current configuration source */ getConfigSource() { // Return highest priority source that has data if (this.sessionDefaults) return 'session'; if (this.fileDefaults) return 'user-file'; if (this.environmentDefaults) return 'environment'; return 'built-in'; } /** * Get configuration file path */ getConfigPath() { return this.configPath; } /** * Get template by name */ getTemplate(templateName) { return this.fileDefaults?.templates?.[templateName] || this.sessionDefaults?.templates?.[templateName]; } /** * Get available template names */ getAvailableTemplates() { const fileTemplates = Object.keys(this.fileDefaults?.templates || {}); const sessionTemplates = Object.keys(this.sessionDefaults?.templates || {}); return [...new Set([...fileTemplates, ...sessionTemplates])]; } /** * Get environment variable defaults for debugging */ getEnvironmentDefaults() { const envDefaults = {}; Object.keys(process.env).forEach(key => { const match = key.match(/^OPTIMIZELY_([A-Z_]+)_([A-Z_]+)$/); if (match) { const [, entityType, fieldName] = match; const value = this.parseEnvironmentValue(process.env[key]); const entity = entityType.toLowerCase(); const field = fieldName.toLowerCase(); if (!envDefaults[entity]) { envDefaults[entity] = {}; } envDefaults[entity][field] = { value, originalKey: key }; } }); return envDefaults; } /** * Demonstrate the jQuery problem solution */ demonstrateJQueryFix() { getLogger().info('JQUERY PROBLEM DEMONSTRATION:'); getLogger().info('================================'); // Show Optimizely's legacy defaults const swaggerDefaults = FIELDS.project?.defaults || {}; getLogger().info('Optimizely API defaults (legacy):'); getLogger().info(` platform: "${swaggerDefaults.platform}" (leads to jQuery 1.11.3)`); // Show our modern defaults const modernDefaults = this.getDefaults('project'); getLogger().info('Our modern defaults:'); getLogger().info(` platform: "${modernDefaults.platform}" (Feature Experimentation)`); getLogger().info(` include_jquery: ${modernDefaults.settings?.web?.snippet?.include_jquery}`); getLogger().info(` library: "${modernDefaults.settings?.web?.snippet?.library}"`); getLogger().info(`Configuration source: ${this.getConfigSource()}`); if (this.configPath) { getLogger().info(`Configuration file: ${this.configPath}`); } // Show environment variables if any const envDefaults = this.getEnvironmentDefaults(); if (Object.keys(envDefaults).length > 0) { getLogger().info('Environment variable defaults:'); Object.entries(envDefaults).forEach(([entity, fields]) => { Object.entries(fields).forEach(([field, data]) => { getLogger().info(` ${data.originalKey}: ${data.value}`); }); }); } getLogger().info('================================'); } /** * Deep merge multiple objects */ deepMerge(...objects) { const result = {}; for (const obj of objects) { if (!obj) continue; for (const key in obj) { if (obj[key] !== null && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { result[key] = this.deepMerge(result[key] || {}, obj[key]); } else { result[key] = obj[key]; } } } return result; } } //# sourceMappingURL=ConfigurableDefaultsManager.js.map