@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
214 lines (211 loc) • 7.98 kB
JavaScript
/**
* 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