UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

287 lines 14.1 kB
/** * COMPREHENSIVE AUDIENCE REFERENCE RESOLVER * * This class provides the MOST ROBUST solution for handling audience references * in ANY format across the entire pipeline. It prevents format mismatch errors * by normalizing ALL possible audience reference formats to the correct API format. * * DESIGN PRINCIPLES: * 1. Accept ANY reasonable audience reference format * 2. Normalize to the correct API format * 3. Provide clear logging and error messages * 4. Never fail silently - always log what transformations were made * 5. Defensive programming - handle edge cases gracefully */ import { getLogger } from '../logging/Logger.js'; export class AudienceReferenceResolver { logger = getLogger(); entityRouter; constructor(entityRouter) { this.entityRouter = entityRouter; } /** * MAIN METHOD: Resolve ANY audience reference format to API format * * Handles ALL possible input formats: * - audience: "Target Users" * - audience: { ref: { name: "Target Users" } } * - audience: { ref: { id: 123 } } * - audiences: { ref: { name: "Target Users" } } * - audience_conditions: ["or", { "audience_id": 123 }] * - Direct audience ID/name strings */ async resolveAudienceReference(input, projectId) { const transformations = []; let sourceFormat = 'unknown'; try { // Check for auto_create directive first this.logger.info('AudienceReferenceResolver: Checking for auto_create'); this.logger.info('Input details: ' + JSON.stringify({ inputType: typeof input, hasAutoCreate: input?.auto_create, hasRefAutoCreate: input?.ref?.auto_create, hasTemplate: input?.template, hasRefTemplate: input?.ref?.template, input: JSON.stringify(input) })); // Check for auto_create in both direct format and ref format const isAutoCreate = (input?.auto_create && input?.template) || (input?.ref?.auto_create && input?.ref?.template); const template = input?.template || input?.ref?.template; // Debug logging removed - was causing JSON parsing errors in MCP communication if (input && typeof input === 'object' && isAutoCreate && template) { transformations.push('Auto-create directive detected'); const createdAudience = await this.createAudienceFromTemplate(template, projectId, transformations); const audienceConditions = this.convertToAPIFormat(createdAudience, transformations); this.logTransformations(input, audienceConditions, transformations, 'auto_create'); return { audience_conditions: audienceConditions, transformations_applied: transformations, source_format: 'auto_create', created: true, createdAudience }; } // Step 1: Detect and normalize input format const normalized = this.normalizeInputFormat(input, transformations); if (!normalized) { return null; // No audience reference found } sourceFormat = normalized.sourceFormat; // Step 2: Resolve reference to actual audience const resolvedAudience = await this.resolveReference(normalized.reference, projectId, transformations); if (!resolvedAudience) { throw new Error(`Audience not found: ${JSON.stringify(normalized.reference)}`); } // Step 3: Convert to API format const audienceConditions = this.convertToAPIFormat(resolvedAudience, transformations); // Step 4: Log all transformations this.logTransformations(input, audienceConditions, transformations, sourceFormat); return { audience_conditions: audienceConditions, transformations_applied: transformations, source_format: sourceFormat }; } catch (error) { this.logger.error('AudienceReferenceResolver: Failed to resolve audience reference', error instanceof Error ? error.message : String(error)); throw error; } } /** * STEP 1: Normalize ALL possible input formats to a standard reference object */ normalizeInputFormat(input, transformations) { if (!input) return null; // Format 1: Already in API format if (Array.isArray(input) && input[0] === 'or' && input[1]?.audience_id) { transformations.push('Input already in API format'); return { reference: { audience_id: input[1].audience_id }, sourceFormat: 'api_format' }; } // Format 2: Direct string (audience name) if (typeof input === 'string') { transformations.push('Converted direct string to name reference'); return { reference: { name: input }, sourceFormat: 'direct_string' }; } // Format 3: Direct number (audience ID) if (typeof input === 'number') { transformations.push('Converted direct number to ID reference'); return { reference: { id: input }, sourceFormat: 'direct_number' }; } // Format 4: Object with ref property if (input.ref) { transformations.push('Extracted reference from ref object'); return { reference: input.ref, sourceFormat: 'ref_object' }; } // Format 5: Direct reference object if (input.id || input.name || input.key || input.audience_id) { transformations.push('Used direct reference properties'); return { reference: input, sourceFormat: 'direct_reference' }; } return null; } /** * STEP 2: Resolve reference to actual audience entity */ async resolveReference(reference, projectId, transformations) { if (!this.entityRouter) { throw new Error('EntityRouter not provided - cannot resolve audience references'); } // Try ID first if (reference.id || reference.audience_id) { const id = reference.id || reference.audience_id; transformations.push(`Searching by ID: ${id}`); const result = await this.entityRouter.findEntityByIdOrKey('audience', String(id), projectId); if (result) return result; } // Try name if (reference.name) { transformations.push(`Searching by name: ${reference.name}`); const results = await this.entityRouter.searchEntities({ entityType: 'audience', projectId, searchCriteria: { name: reference.name } }); if (results.length > 0) return results[0]; } // Try key if (reference.key) { transformations.push(`Searching by key: ${reference.key}`); const result = await this.entityRouter.findEntityByIdOrKey('audience', reference.key, projectId); if (result) return result; } return null; } /** * STEP 3: Convert resolved audience to API format */ convertToAPIFormat(audience, transformations) { const audienceId = parseInt(String(audience.id), 10); transformations.push(`Converted to API format with audience_id: ${audienceId}`); return ["or", { "audience_id": audienceId }]; } /** * STEP 4: Log all transformations for debugging */ logTransformations(input, output, transformations, sourceFormat) { this.logger.info('AudienceReferenceResolver: Successfully resolved audience reference'); } /** * UTILITY: Extract audience references from ab_test object */ static extractAudienceFromABTest(abTestData) { // Check all possible field names return abTestData?.audience || abTestData?.audiences || abTestData?.audience_conditions || null; } /** * UTILITY: Apply resolved audience conditions to ab_test object * ENHANCED: Process multiple audiences before deleting the field */ static applyAudienceConditions(abTestData, audienceConditions) { // CRITICAL FIX: Process multiple audiences BEFORE deleting the field if (abTestData.audiences && Array.isArray(abTestData.audiences) && abTestData.audiences.length > 0) { getLogger().info(`AudienceReferenceResolver: Processing multiple audiences before field cleanup - count: ${abTestData.audiences.length}, operator: ${abTestData.audiences_operator || 'or'}`); // Instead of preserving for later processing, we'll process the multiple audiences HERE // This ensures they get converted to the correct audience_conditions format try { const resolvedAudienceConditions = []; // Process each audience ref to get the audience ID for (const audienceItem of abTestData.audiences) { if (audienceItem && audienceItem.ref && audienceItem.ref.id) { const audienceId = parseInt(audienceItem.ref.id, 10); resolvedAudienceConditions.push({ "audience_id": audienceId }); getLogger().info(`AudienceReferenceResolver: Resolved audience ID ${audienceId} for multiple audiences`); } } if (resolvedAudienceConditions.length > 0) { const operator = abTestData.audiences_operator || "or"; const finalConditions = [operator, ...resolvedAudienceConditions]; abTestData.audience_conditions = finalConditions; getLogger().info(`AudienceReferenceResolver: Set multiple audience conditions with ${operator} operator: ${JSON.stringify(finalConditions)}`); } else { // Fallback if no audiences could be resolved abTestData.audience_conditions = audienceConditions.length > 0 ? audienceConditions : "everyone"; getLogger().warn('AudienceReferenceResolver: No audiences could be resolved from multiple audiences, using fallback'); } } catch (error) { getLogger().error(`AudienceReferenceResolver: Error processing multiple audiences: ${error instanceof Error ? error.message : String(error)}`); // Fallback to provided conditions or everyone abTestData.audience_conditions = audienceConditions.length > 0 ? audienceConditions : "everyone"; } // Clean up template fields delete abTestData.audience; delete abTestData.audiences; delete abTestData.audiences_operator; return; // Exit early after processing multiple audiences } // Original logic for single audience processing // Set the correct API field abTestData.audience_conditions = audienceConditions; // Clean up any template fields that might interfere delete abTestData.audience; delete abTestData.audiences; } /** * Create a new audience from template specification * This handles the auto_create directive for inline audience definitions */ async createAudienceFromTemplate(template, projectId, transformations) { if (!this.entityRouter) { throw new Error('EntityRouter not provided - cannot create audiences'); } try { transformations.push(`Creating audience: ${template.name}`); // Prepare audience data with proper formatting const audienceData = { name: template.name, conditions: typeof template.conditions === 'string' ? template.conditions : JSON.stringify(template.conditions), description: template.description || `Auto-created audience`, segmentation: template.segmentation !== undefined ? template.segmentation : true, project_id: parseInt(projectId) // CRITICAL: Audiences require project_id in the data payload }; // Create the audience using EntityRouter getLogger().debug({ audienceData }, 'Calling EntityRouter for audience creation'); const createResult = await this.entityRouter.routeEntityOperation({ operation: 'create', entityType: 'audience', projectId, entityId: '', data: audienceData, options: { mode: 'direct', // Bypass template mode check _internal: true // Mark as internal operation } }); getLogger().debug({ createResult }, 'EntityRouter returned'); const createdAudience = createResult.data || createResult; transformations.push(`Created audience with ID: ${createdAudience.id}`); return createdAudience; } catch (error) { getLogger().debug({ error }, 'Audience creation failed'); // If creation fails due to duplicate name, try to look it up if (error instanceof Error && error.message && error.message.includes('already exists')) { transformations.push('Creation failed - audience name already exists, attempting lookup'); const existing = await this.resolveReference({ name: template.name }, projectId, transformations); if (existing) { transformations.push('Found existing audience with same name'); return existing; } } throw error; } } } export default AudienceReferenceResolver; //# sourceMappingURL=AudienceReferenceResolver.js.map