UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

469 lines (393 loc) 18.8 kB
/** * Dependency Validator for Optimizely Entities * * This validator ensures that entity dependencies are satisfied before operations proceed. * It provides prescriptive guidance when dependencies are missing, following the operational * sequencing patterns defined in cursor agent rules. * * Key Dependencies: * - Audiences require Attributes to exist first * - Metrics require Events to exist first * - Experiments require Metrics (which require Events) * - Flags may require Variations and Variables */ // CRITICAL: SafeIdConverter prevents scientific notation in large IDs // DO NOT REMOVE OR REPLACE WITH toString() - This caused duplicate records bug // Evidence: 5.02479302872269e+15 vs 5024793028722690 for same entity // Fixed: January 26, 2025 - See CONTEXT-RECOVERY-V2.md for details import { safeIdToString } from '../utils/SafeIdConverter.js'; export class DependencyValidator { static dependencies = [ // AUDIENCE DEPENDENCIES { entityType: 'audience', field: 'conditions', dependsOn: 'attribute', extractKey: (conditions) => { const attributeKeys = []; try { const parsed = JSON.parse(conditions); // Recursively find all attribute references const findAttributes = (obj) => { if (Array.isArray(obj)) { obj.forEach(findAttributes); } else if (obj && typeof obj === 'object') { if (obj.type === 'attribute' && obj.name) { attributeKeys.push(obj.name); } Object.values(obj).forEach(findAttributes); } }; findAttributes(parsed); } catch (e) { // Invalid JSON, will be caught by other validation } return attributeKeys; }, errorMessage: (key) => `Custom attribute '${key}' does not exist`, prescriptiveGuidance: (key, projectId) => ` DEPENDENCY ERROR: Audience condition references attribute '${key}' incorrectly. PRESCRIPTIVE STEPS TO RESOLVE: 1. **LIST existing attributes to find the correct reference:** \`list_entities entity_type=attribute project_id=${projectId}\` 2. **CHECK the attribute details:** - The 'name' field is what you use in conditions (e.g., "Region") - The 'key' field is for API references (not used in conditions) - The 'id' field is the numeric ID 3. **USE the correct format in conditions:** For Web Experimentation audiences: \`"conditions": "[\\\"and\\\", {\\\"type\\\": \\\"custom_attribute\\\", \\\"name\\\": \\\"ATTRIBUTE_NAME\\\", \\\"value\\\": \\\"YOUR_VALUE\\\"}]"\` Note: Use the NAME field from the attribute, not the key! 4. **COMPLETE WORKING EXAMPLE:** \`manage_entity_lifecycle operation=create entity_type=audience project_id=${projectId} entity_data='{"name": "Audience Name", "description": "Description", "conditions": "[\\\\"and\\\\", {\\\\"type\\\\": \\\\"custom_attribute\\\\", \\\\"name\\\\": \\\\"Region\\\\", \\\\"value\\\\": \\\\"Italy 7\\\\"}]", "project_id": ${projectId}}'\` TIP: KEY INSIGHT: Web Experimentation uses the attribute "name" field (e.g., "Region"), not the "key" field (e.g., "region") WARNING: COMMON MISTAKES: - Using attribute KEY instead of NAME field - Wrong escaping of JSON in conditions string - Missing project_id in the request body ` }, { entityType: 'metric', field: 'event_id', dependsOn: 'event', // CRITICAL: Use SafeIdConverter to prevent scientific notation extractKey: (eventId) => safeIdToString(eventId), errorMessage: (key) => `Event with ID '${key}' does not exist`, prescriptiveGuidance: (key, projectId) => ` DEPENDENCY ERROR: Event must exist before creating a metric. PRESCRIPTIVE STEPS TO RESOLVE: 1. **LIST available events:** \`list_entities entity_type=event project_id=${projectId}\` 2. **IF EVENT MISSING, create it first:** \`manage_entity_lifecycle operation=create entity_type=event project_id=${projectId} entity_data='{"key": "conversion_event", "name": "Conversion Event", "project_id": ${projectId}}'\` 3. **THEN create metric using the event ID from response** TIP: OPERATIONAL SEQUENCE: Create Event Get Event ID Create Metric NOTE: NOTE: One event can have multiple metrics (total conversions, unique conversions, revenue, etc.) ` }, { entityType: 'experiment', field: 'metrics', dependsOn: 'event', extractKey: (metrics) => { if (!Array.isArray(metrics)) return []; return metrics .filter(m => m.event_id) // CRITICAL: Use SafeIdConverter to prevent scientific notation .map(m => safeIdToString(m.event_id)); }, errorMessage: (key) => `Metric references event '${key}' which does not exist`, prescriptiveGuidance: (key, projectId) => ` DEPENDENCY ERROR: Events must exist before adding metrics to experiments. PRESCRIPTIVE STEPS TO RESOLVE: 1. **CREATE the missing event:** \`manage_entity_lifecycle operation=create entity_type=event project_id=${projectId} entity_data='{"key": "goal_event", "name": "Goal Event", "project_id": ${projectId}}'\` 2. **NOTE the event ID from response** 3. **CREATE metric configuration:** \`{ "event_id": <event_id_from_step_2>, "aggregator": "count", "winning_direction": "increasing" }\` 4. **ADD metric to experiment** TIP: OPERATIONAL SEQUENCE: Create Events Configure Metrics Create Experiment NOTE: TIP: Common metric types: - Total conversions: aggregator="count" - Unique conversions: aggregator="unique" - Revenue: aggregator="sum" with event_properties ` }, // VARIATION DEPENDENCIES { entityType: 'variation', field: 'variables', dependsOn: 'variable_definition', extractKey: (variables) => { return Object.keys(variables || {}); }, errorMessage: (key) => `Variable '${key}' is not defined in flag schema`, prescriptiveGuidance: (key, projectId) => ` SCHEMA COMPLIANCE ERROR: Variation must match flag's variable definitions. PRESCRIPTIVE STEPS TO RESOLVE: 1. **GET flag's variable definitions:** \`list_entities entity_type=variable_definition project_id=${projectId} filters={"flag_key": "YOUR_FLAG_KEY"}\` 2. **VERIFY all variables are included:** - Your variation MUST have values for ALL defined variables - Each value must match the defined type (string, boolean, integer, double, json) 3. **FIX missing or incorrect variables:** - Add missing variable: "${key}" - Check type matches definition 4. **RETRY with complete variables object** TIP: KEY INSIGHT: Variations are instances of the schema defined by variable definitions. WARNING: COMMON MISTAKES: - Missing required variables - Wrong data types (e.g., string "true" instead of boolean true) - Extra variables not in schema ` }, // PAGE DEPENDENCIES { entityType: 'experiment', field: 'page_ids', dependsOn: 'page', extractKey: (pageIds) => { // CRITICAL: Use SafeIdConverter for each page ID to prevent scientific notation return (pageIds || []).map(id => safeIdToString(id)); }, errorMessage: (key) => `Page with ID '${key}' does not exist`, prescriptiveGuidance: (key, projectId) => ` DEPENDENCY ERROR: Page must exist before being referenced in experiment. PRESCRIPTIVE STEPS TO RESOLVE: 1. **LIST available pages:** \`list_entities entity_type=page project_id=${projectId}\` 2. **IF PAGE MISSING, create it first:** \`manage_entity_lifecycle operation=create entity_type=page project_id=${projectId} entity_data='{"name": "Home Page", "edit_url": "https://example.com", "project_id": ${projectId}}'\` 3. **NOTE the page ID from response** 4. **USE page ID in experiment creation** TIP: OPERATIONAL SEQUENCE: Create Pages Create Experiment with page_ids NOTE: NOTE: Pages define where experiments run in Web Experimentation ` }, // CAMPAIGN DEPENDENCIES { entityType: 'experiment', field: 'campaign_id', dependsOn: 'campaign', // CRITICAL: Use SafeIdConverter to prevent scientific notation extractKey: (campaignId) => campaignId ? safeIdToString(campaignId) : '', errorMessage: (key) => `Campaign with ID '${key}' does not exist`, prescriptiveGuidance: (key, projectId) => ` DEPENDENCY ERROR: Campaign must exist before creating experiments. PRESCRIPTIVE STEPS TO RESOLVE: 1. **LIST available campaigns:** \`list_entities entity_type=campaign project_id=${projectId}\` 2. **IF CAMPAIGN MISSING, create it first:** \`manage_entity_lifecycle operation=create entity_type=campaign project_id=${projectId} entity_data='{"name": "Landing Page Campaign", "project_id": ${projectId}}'\` 3. **NOTE the campaign ID from response** 4. **USE campaign ID in experiment creation** TIP: CONTEXT: Campaigns group related experiments in Web Experimentation WARNING: NOTE: Feature Experimentation experiments don't use campaigns ` } ]; /** * Check dependencies for an entity and return prescriptive guidance if missing */ static async checkDependencies(entityType, data, projectId, apiHelper // OptimizelyAPIHelper instance if available ) { const errors = []; // Find all dependency checks for this entity type const relevantChecks = this.dependencies.filter(check => check.entityType === entityType); for (const check of relevantChecks) { const fieldValue = data[check.field]; if (!fieldValue) continue; // Field not present, skip check const dependencyKeys = check.extractKey(fieldValue); const keysToCheck = Array.isArray(dependencyKeys) ? dependencyKeys : [dependencyKeys]; for (const key of keysToCheck) { if (!key) continue; // If we have an API helper, we could check if the dependency exists // For now, we'll return prescriptive guidance on any error const guidance = check.prescriptiveGuidance(key, projectId); // This is where we'd check if the dependency exists // For now, we'll add this to the entity builder's validation // to be triggered when the API returns a dependency error } } return { isValid: errors.length === 0, errors }; } /** * Generate prescriptive guidance for a specific error */ static generateGuidanceForError(entityType, errorMessage, projectId, operation) { // Check for condition format errors FIRST - but ONLY if it's actually an error about custom_attribute if (entityType === 'audience' && errorMessage.includes('HTTP 400') && !errorMessage.toLowerCase().includes('does not exist')) { // Only show this if it's a generic 400 error, not a specific "does not exist" error return ` CONDITION FORMAT ERROR: Invalid audience conditions format. UNDERSTANDING PLATFORM DIFFERENCES: **Web Experimentation** (is_flags_enabled: false): CORRECT: Uses: \`{"type": "custom_attribute", "name": "KEY", "value": "VALUE"}\` **Feature Experimentation** (is_flags_enabled: true): CORRECT: Uses: \`{"type": "attribute", "name": "KEY", "value": "VALUE"}\` NOTE: CHECK YOUR PROJECT TYPE: \`get_entity_details entity_type=project entity_id=${projectId}\` Look for: "is_flags_enabled" field CORRECT FORMAT FOR YOUR PROJECT: If Web Experimentation: \`"conditions": "[\\"and\\", {\\"type\\": \\"custom_attribute\\", \\"name\\": \\"region\\", \\"value\\": \\"Italy 7\\"}]"\` If Feature Experimentation: \`"conditions": "[\\"and\\", {\\"type\\": \\"attribute\\", \\"name\\": \\"region\\", \\"value\\": \\"Italy 7\\"}]"\` TIP: KEY INSIGHT: The attribute type depends on your project platform! WARNING: IMPORTANT: - Always use the attribute KEY (not display name) in conditions - Ensure proper JSON escaping in the conditions string - Include project_id in the request body `; } // Check for schema validation errors if (entityType === 'variation' && (errorMessage.includes('schema') || errorMessage.includes('variable'))) { return ` SCHEMA COMPLIANCE ERROR: Variation variables don't match flag schema. PRESCRIPTIVE STEPS TO RESOLVE: 1. **GET the flag's variable definitions:** \`list_entities entity_type=variable_definition project_id=${projectId} filters={"flag_key": "YOUR_FLAG_KEY"}\` 2. **CHECK each variable requirement:** - Variable key (exact match required) - Variable type (string, boolean, integer, double, json) - Default value format 3. **CREATE variation with ALL variables:** Example for a flag with 2 variables: \`{ "variables": { "show_banner": true, // boolean type "banner_text": "Welcome!" // string type } }\` 4. **COMMON FIXES:** - Add missing variables - Fix type mismatches (e.g., "true" true) - Remove extra variables not in schema TIP: REMEMBER: Every variation MUST have values for ALL variable definitions `; } // Check for platform mismatch errors if (errorMessage.includes('not supported') || errorMessage.includes('not available')) { if (entityType === 'flag' || entityType === 'ruleset' || entityType === 'rule') { return ` PLATFORM ERROR: ${entityType} is only available in Feature Experimentation projects. PRESCRIPTIVE STEPS TO RESOLVE: 1. **CHECK project type:** \`get_entity_details entity_type=project entity_id=${projectId}\` Look for: "is_flags_enabled": true 2. **IF FALSE (Web Experimentation):** Use these entities instead: - campaigns (group experiments) - experiments (A/B tests) - pages (where tests run) 3. **LIST available entities:** \`list_entities entity_type=campaign project_id=${projectId}\` \`list_entities entity_type=experiment project_id=${projectId}\` TIP: KEY INSIGHT: Feature Experimentation uses flags, Web uses campaigns/experiments `; } } // Check for wrong operation errors if (errorMessage.includes('cannot be deleted') || errorMessage.includes('archive instead')) { return ` OPERATION ERROR: ${entityType} cannot be deleted, only archived. PRESCRIPTIVE STEPS TO RESOLVE: 1. **USE archive operation instead:** \`manage_entity_lifecycle operation=archive entity_type=${entityType} project_id=${projectId} entity_id=["ENTITY_KEY_OR_ID"]\` 2. **TO RESTORE later:** \`manage_entity_lifecycle operation=unarchive entity_type=${entityType} project_id=${projectId} entity_id=["ENTITY_KEY_OR_ID"]\` TIP: WHY: Archiving preserves data integrity and allows restoration WARNING: ENTITIES THAT SUPPORT DELETE: - attributes, events, pages, extensions, webhooks ARCHIVE-ONLY ENTITIES: - flags, experiments, campaigns, audiences `; } // Check for JSON Patch format errors if (entityType === 'flag' && operation === 'update' && errorMessage.includes('patch')) { return ` UPDATE FORMAT ERROR: Flags require JSON Patch format (RFC 6902). PRESCRIPTIVE FIX: WRONG: WRONG (full object): \`{"description": "New description"}\` CORRECT: CORRECT (JSON Patch): \`[{"op": "replace", "path": "/description", "value": "New description"}]\` COMMON OPERATIONS: 1. **REPLACE a field:** \`[{"op": "replace", "path": "/description", "value": "Updated description"}]\` 2. **ADD a field:** \`[{"op": "add", "path": "/new_field", "value": "new value"}]\` 3. **REMOVE a field:** \`[{"op": "remove", "path": "/field_to_remove"}]\` 4. **MULTIPLE changes:** \`[ {"op": "replace", "path": "/name", "value": "New Name"}, {"op": "replace", "path": "/description", "value": "New Description"} ]\` TIP: TIP: Use JSON Patch for flags, regular JSON for most other entities `; } // Check if this is a dependency error we can provide guidance for for (const check of this.dependencies) { if (check.entityType !== entityType) continue; // Pattern match common dependency errors if (errorMessage.toLowerCase().includes('does not exist') || errorMessage.toLowerCase().includes('not found') || errorMessage.toLowerCase().includes('invalid') && errorMessage.toLowerCase().includes(check.dependsOn)) { // Extract the key from error message if possible const keyMatch = errorMessage.match(/['"]([^'"]+)['"]/); if (keyMatch) { const key = keyMatch[1]; return check.prescriptiveGuidance(key, projectId); } } } return null; } /** * Format error response for MCP client consumption */ static formatMCPError(operation, entityType, errorMessage, projectId) { const guidance = this.generateGuidanceForError(entityType, errorMessage, projectId, operation); if (guidance) { return { error: errorMessage, prescriptiveGuidance: guidance, nextSteps: this.extractNextSteps(guidance) }; } return { error: errorMessage }; } /** * Extract actionable next steps from prescriptive guidance */ static extractNextSteps(guidance) { const steps = []; const lines = guidance.split('\n'); for (const line of lines) { // Look for numbered steps with backticks (commands) const commandMatch = line.match(/^\d️⃣.*?`([^`]+)`/); if (commandMatch) { steps.push(commandMatch[1]); } } return steps; } } //# sourceMappingURL=DependencyValidator.js.map