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