UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

214 lines (211 loc) 7.98 kB
/** * Ruleset Update Transformer * * Transforms Web Experimentation style ruleset updates to Feature Experimentation format. * This prevents agents from using incorrect structures when updating Feature Flag rulesets. * * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../logging/Logger.js'; export class RulesetUpdateTransformer { logger = getLogger(); /** * Transform a ruleset update payload to correct format */ transformRulesetUpdate(payload, projectPlatform, context) { this.logger.info({ payloadLength: Array.isArray(payload) ? payload.length : 1, projectPlatform, context }, 'RulesetUpdateTransformer: Transforming ruleset update'); if (projectPlatform === 'web') { // Web platform - correct format return { success: true, transformed: payload }; } // Feature Experimentation - check for common mistakes if (!Array.isArray(payload)) { return { success: false, errors: ['Ruleset updates must be an array of JSON Patch operations'], guidance: 'Use an array of operations like: [{op: "add", path: "/rules/0", value: {...}}]' }; } const errors = []; const transformed = []; for (const operation of payload) { const result = this.transformOperation(operation); if (result.error) { errors.push(result.error); } else if (result.transformed) { transformed.push(...result.transformed); } } if (errors.length > 0) { return { success: false, errors, guidance: this.generateGuidance(payload, errors) }; } return { success: true, transformed }; } /** * Transform a single operation */ transformOperation(operation) { // Check for A/B test rule with wrong structure if (operation.value && this.isWebStyleAbTestRule(operation.value)) { return this.transformWebStyleAbTest(operation); } // Check for traffic_allocation (Web style) that should be variations with weights if (operation.value?.traffic_allocation) { return this.transformTrafficAllocation(operation); } // Check for single_experiment type (invalid for Feature) if (operation.value?.type === 'single_experiment') { return { error: "Rule type 'single_experiment' is not valid for Feature Experimentation. Use 'a/b' instead." }; } // Default - return as is return { transformed: [operation] }; } /** * Check if this is a Web-style A/B test rule */ isWebStyleAbTestRule(value) { return value.variations && Array.isArray(value.variations) && value.variations.some((v) => !v.weight) && (value.traffic_allocation || value.type === 'single_experiment'); } /** * Transform Web-style A/B test to Feature Experimentation format */ transformWebStyleAbTest(operation) { const rule = operation.value; // Extract variations const variations = rule.variations || []; const trafficAllocation = rule.traffic_allocation || []; // Create weight-based variations const transformedVariations = variations.map((variation, index) => { // Find weight from traffic_allocation const allocation = trafficAllocation.find((ta) => ta.entity_id === variation.key || ta.entity_id === variation.variation_id); let weight = 0; if (allocation) { // Calculate weight from endOfRange const prevAllocation = trafficAllocation[index - 1]; const startRange = prevAllocation ? prevAllocation.endOfRange : 0; weight = allocation.endOfRange - startRange; } else { // Equal distribution if no allocation found weight = Math.floor(10000 / variations.length); } return { key: variation.key, weight: weight }; }); // Transform the rule const transformedRule = { key: rule.key || 'experiment_rule', enabled: rule.enabled !== false, audience_conditions: rule.audience_id ? JSON.stringify(["and", { "audience_id": rule.audience_id }]) : "everyone", percentage_included: 10000, // Default to 100% variations: transformedVariations }; // Remove Web-specific fields if ('traffic_allocation' in transformedRule) { delete transformedRule.traffic_allocation; } if ('type' in transformedRule) { delete transformedRule.type; } if ('metrics' in transformedRule) { delete transformedRule.metrics; // Metrics go on experiments, not rules } return { transformed: [{ op: operation.op, path: operation.path, value: transformedRule }] }; } /** * Transform traffic_allocation to variations with weights */ transformTrafficAllocation(operation) { const rule = operation.value; const trafficAllocation = rule.traffic_allocation; if (!Array.isArray(trafficAllocation)) { return { error: 'traffic_allocation must be an array' }; } // Calculate weights from traffic allocation const variations = trafficAllocation.map((allocation, index) => { const prevAllocation = trafficAllocation[index - 1]; const startRange = prevAllocation ? prevAllocation.endOfRange : 0; const weight = allocation.endOfRange - startRange; return { key: allocation.entity_id, weight: weight }; }); // Update the rule const transformedRule = { ...rule }; delete transformedRule.traffic_allocation; transformedRule.variations = variations; return { transformed: [{ op: operation.op, path: operation.path, value: transformedRule }] }; } /** * Generate helpful guidance based on errors */ generateGuidance(payload, errors) { const hasVariations = payload.some((op) => op.value?.variations && Array.isArray(op.value.variations)); if (hasVariations) { return `For Feature Experimentation A/B tests: 1. First create variations separately: manage_entity_lifecycle( operation="create", entity_type="variation", entity_data={key: "control", name: "Control", weight: 5000}, options={flag_key: "your_flag_key"} ) 2. Then update the ruleset: [{ op: "add", path: "/rules/0", value: { key: "experiment_rule", enabled: true, audience_conditions: "everyone", percentage_included: 10000, variations: [ {key: "control", weight: 5000}, {key: "treatment", weight: 5000} ] } }] Key differences from Web Experimentation: - Use 'percentage_included' not 'traffic_allocation' - Variations need 'weight' in basis points (10000 = 100%) - No 'type: single_experiment' - use proper rule structure - Metrics go on experiments, not rules`; } return 'See resource://feature-experimentation-ab-test-guide for proper ruleset update format'; } } // Export singleton export const rulesetUpdateTransformer = new RulesetUpdateTransformer(); //# sourceMappingURL=RulesetUpdateTransformer.js.map