@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
524 lines • 22.7 kB
JavaScript
/**
* 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