UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

409 lines 17 kB
/** * Analytics Transformer * @description JSONata-based transformation pipeline for converting complex database results * into simplified, paginated analytics responses that are client-friendly. * * Key Features: * - Extracts Feature Experimentation A/B tests from nested rulesets * - Unified response format across Web and Feature Experimentation * - Cursor-based pagination for large datasets * - Environment variable controlled complexity * * @author Optimizely MCP Server * @version 1.0.0 */ import jsonata from 'jsonata'; import { getLogger } from '../logging/Logger.js'; export class AnalyticsTransformer { logger = getLogger(); defaultPageSize; maxPageSize; responseMode; // JSONata expressions for different entity transformations transformations = { // Feature Experimentation Flags - SIMPLIFIED for database structure featureFlags: ` $.{ "entity_type": "flag", "platform": "feature", "key": key, "name": name, "description": description, "status": { "state": archived ? "archived" : "active", "enabled_environments": 0, "has_targeting": false }, "details": {}, "timestamps": { "created": created_time ? created_time : created, "last_modified": updated_time ? updated_time : last_modified } } `, // Extract A/B tests from Feature Experimentation flags featureExperiments: ` $[environments[data_json ~> $eval('$.rules[type="a/b" or type="experiment"]')]].{ "experiments": environments[data_json ~> $eval('$.rules[type="a/b" or type="experiment"]')].{ "flag_key": $parent.key, "flag_name": $parent.name, "environment_key": environment_key, "rules": data_json ~> $eval('$.rules[type="a/b" or type="experiment"]') } }.experiments.rules.{ "entity_type": "experiment", "platform": "feature", "key": $parent.flag_key & "_" & key, "name": name ? name : $parent.flag_name & " - " & key, "description": "A/B test from " & $parent.flag_name & " flag", "status": { "state": enabled ? "running" : "paused", "traffic_allocation": percentage_included, "has_targeting": $exists(audience_conditions[0]) }, "details": { "variations_count": $count(variations), "parent_flag": $parent.flag_key, "environment": $parent.environment_key }, "timestamps": { "last_modified": $parent.$parent.last_modified } } `, // Web Experimentation Experiments webExperiments: ` $.{ "entity_type": "experiment", "platform": "web", "key": key ? key : $string(id), "name": name, "description": description, "status": { "state": status, "traffic_allocation": traffic_allocation, "has_targeting": $exists(audience_conditions[0]) }, "details": { "variations_count": $count(variations) }, "timestamps": { "created": created, "last_modified": last_modified } } `, // Web Experimentation Campaigns webCampaigns: ` $.{ "entity_type": "campaign", "platform": "web", "key": $string($."campaigns.id" ? $."campaigns.id" : id), "name": $."campaigns.name" ? $."campaigns.name" : name, "description": $."campaigns.description" ? $."campaigns.description" : description, "status": { "state": $."campaigns.status" ? $."campaigns.status" : status, "has_targeting": $exists(($."campaigns.page_ids" ? $."campaigns.page_ids" : page_ids)[0]) }, "details": { "experiments_count": $count($."campaigns.experiments" ? $."campaigns.experiments" : experiments) }, "timestamps": { "created": $."campaigns.created" ? $."campaigns.created" : created, "last_modified": $."campaigns.last_modified" ? $."campaigns.last_modified" : last_modified } } `, // Audiences (shared across platforms) audiences: ` $.{ "entity_type": "audience", "platform": "auto", "key": $string($."audiences.id" ? $."audiences.id" : id), "name": $."audiences.name" ? $."audiences.name" : name, "description": $."audiences.description" ? $."audiences.description" : description, "status": { "state": ($."audiences.archived" ? $."audiences.archived" : archived) ? "archived" : "active", "has_targeting": true }, "details": { "conditions_count": $count($eval($."audiences.conditions" ? $."audiences.conditions" : conditions)[0]) }, "timestamps": { "created": $."audiences.created" ? $."audiences.created" : created, "last_modified": $."audiences.last_modified" ? $."audiences.last_modified" : last_modified } } ` }; constructor() { this.defaultPageSize = parseInt(process.env.ANALYTICS_DEFAULT_PAGE_SIZE || '10'); this.maxPageSize = parseInt(process.env.ANALYTICS_MAX_PAGE_SIZE || '100'); this.responseMode = (process.env.ANALYTICS_RESPONSE_MODE || 'simplified'); // Ensure page sizes are within valid range (4-100) this.defaultPageSize = Math.max(4, Math.min(this.defaultPageSize, this.maxPageSize)); this.maxPageSize = Math.max(4, Math.min(this.maxPageSize, 100)); this.logger.info({ defaultPageSize: this.defaultPageSize, maxPageSize: this.maxPageSize, responseMode: this.responseMode }, 'AnalyticsTransformer: Initialized with configuration'); } /** * Transform database results into simplified analytics response */ async transformAnalyticsResults(dbResults, entityType, platform, options = {}) { try { this.logger.info({ entityType, platform, dbResultsCount: dbResults.length, responseMode: this.responseMode, keysOnly: options.keysOnly }, 'AnalyticsTransformer: Starting transformation'); // Handle keys-only requests - return simple array of keys if (options.keysOnly) { const keys = dbResults.map(entity => entity.key || entity.id?.toString()).filter(Boolean); // Apply pagination to keys (enforce 4-maxPageSize range) const requestedPageSize = options.pagination?.page_size || this.defaultPageSize; const pageSize = Math.max(4, Math.min(requestedPageSize, this.maxPageSize)); // For keys-only, just apply simple array slicing const startIndex = 0; // TODO: Implement cursor decoding for keys const endIndex = startIndex + pageSize; const paginatedKeys = keys.slice(startIndex, endIndex); return paginatedKeys; } // Step 1: Apply JSONata transformation based on entity type and platform const transformedEntities = await this.applyTransformation(dbResults, entityType, platform); // Step 2: Apply pagination (enforce 4-maxPageSize range) const requestedPageSize = options.pagination?.page_size || this.defaultPageSize; const paginationOptions = { page_size: Math.max(4, Math.min(requestedPageSize, this.maxPageSize)), cursor: options.pagination?.cursor }; const paginatedResult = this.applyPagination(transformedEntities, paginationOptions); // Step 3: Build final response const response = { entities: paginatedResult.entities, entity_type: entityType, platform: platform, pagination: { total_count: transformedEntities.length, returned_count: paginatedResult.entities.length, has_more: paginatedResult.hasMore, next_cursor: paginatedResult.nextCursor, page_size: paginationOptions.page_size, current_page: paginatedResult.currentPage }, summary: { total_entities: transformedEntities.length, environment_filter: options.environment_filter, project_id: options.project_id, project_name: options.project_name, query_executed_at: new Date().toISOString(), usage_hint: this.generateUsageHint(entityType, paginatedResult.hasMore) } }; this.logger.info({ originalCount: dbResults.length, transformedCount: transformedEntities.length, returnedCount: paginatedResult.entities.length, hasMore: paginatedResult.hasMore }, 'AnalyticsTransformer: Transformation completed successfully'); return response; } catch (error) { this.logger.error({ entityType, platform, error: error.message, stack: error.stack }, 'AnalyticsTransformer: Transformation failed'); throw new Error(`Analytics transformation failed: ${error.message}`); } } /** * Apply JSONata transformation based on entity type and platform */ async applyTransformation(dbResults, entityType, platform) { let transformationKey; this.logger.debug({ entityType, platform, resultCount: dbResults.length, sampleResult: dbResults[0] ? Object.keys(dbResults[0]) : [] }, 'AnalyticsTransformer: Determining transformation type'); // Determine which transformation to use - simplified logic if (entityType === 'flag') { transformationKey = 'featureFlags'; } else if (entityType === 'experiment' && platform === 'feature') { transformationKey = 'featureExperiments'; } else if (entityType === 'experiment' && platform === 'web') { transformationKey = 'webExperiments'; } else if (entityType === 'campaign') { transformationKey = 'webCampaigns'; } else if (entityType === 'audience') { transformationKey = 'audiences'; } else { // If no match, just return simplified version without JSONata this.logger.warn({ entityType, platform }, 'AnalyticsTransformer: No transformation found, returning simplified manually'); // Manual simplification for unmatched types - enforce pagination here too const simplified = dbResults.map(item => ({ entity_type: entityType, platform: platform, key: item.key || item.id?.toString(), name: item.name || 'Unnamed', description: item.description || null, status: { state: item.archived ? 'archived' : 'active', enabled_environments: 0, has_targeting: false }, details: {}, timestamps: { created: item.created || item.created_time, last_modified: item.last_modified || item.updated_time } })); // Apply pagination even for manual simplification const pageSize = Math.max(4, Math.min(50, this.defaultPageSize)); return simplified.slice(0, pageSize); } const transformationExpression = this.transformations[transformationKey]; try { const expression = jsonata(transformationExpression); // Ensure we await the evaluation const result = await expression.evaluate(dbResults); // Ensure result is always an array const transformedEntities = Array.isArray(result) ? result : (result ? [result] : []); // Filter out any empty objects that might have been created const filteredEntities = transformedEntities.filter(entity => entity && typeof entity === 'object' && Object.keys(entity).length > 0); this.logger.debug({ transformationKey, inputCount: dbResults.length, outputCount: filteredEntities.length, sampleResult: filteredEntities[0] }, 'AnalyticsTransformer: JSONata transformation applied successfully'); return filteredEntities; } catch (jsonataError) { this.logger.warn({ transformationKey, error: jsonataError.message, errorCode: jsonataError.code, dbResultsCount: dbResults.length, sampleKeys: dbResults.slice(0, 3).map(r => r.key || r.id) }, 'AnalyticsTransformer: JSONata evaluation failed, falling back to manual'); // Fall back to manual transformation instead of throwing return this.manualSimplification(dbResults, entityType, platform); } } /** * Manual simplification fallback when JSONata fails */ manualSimplification(dbResults, entityType, platform) { return dbResults.map(item => ({ entity_type: entityType, platform: platform, key: item.key || item.id?.toString(), name: item.name || 'Unnamed', description: item.description || null, status: { state: item.archived ? 'archived' : 'active', enabled_environments: 0, has_targeting: false }, details: {}, timestamps: { created: item.created || item.created_time, last_modified: item.last_modified || item.updated_time } })); } /** * Apply cursor-based pagination to transformed entities */ applyPagination(entities, options) { const { page_size, cursor } = options; let startIndex = 0; let currentPage = 1; // Decode cursor if provided if (cursor) { try { const decodedCursor = JSON.parse(Buffer.from(cursor, 'base64').toString()); startIndex = decodedCursor.start_index || 0; currentPage = decodedCursor.page || 1; } catch (error) { this.logger.warn({ cursor }, 'AnalyticsTransformer: Invalid cursor, starting from beginning'); startIndex = 0; currentPage = 1; } } const endIndex = startIndex + page_size; const paginatedEntities = entities.slice(startIndex, endIndex); const hasMore = endIndex < entities.length; // Generate next cursor if there are more results let nextCursor; if (hasMore) { const cursorData = { start_index: endIndex, page: currentPage + 1, timestamp: Date.now() }; nextCursor = Buffer.from(JSON.stringify(cursorData)).toString('base64'); } return { entities: paginatedEntities, hasMore, nextCursor, currentPage }; } /** * Generate helpful usage hints for agents */ generateUsageHint(entityType, hasMore) { const hints = []; if (hasMore) { hints.push("Say 'continue' or 'next page' for more results"); } hints.push(`Use 'get_entity_details' with a specific ${entityType} key for complete information`); if (entityType === 'experiment' && hasMore) { hints.push("Consider filtering by environment or status to narrow results"); } return hints.join('. '); } /** * Get optimal page size based on response mode and entity type */ getOptimalPageSize(entityType) { const pageSizes = { simplified: { flag: 100, experiment: 75, campaign: 50, audience: 100, default: 50 }, detailed: { flag: 25, experiment: 20, campaign: 15, audience: 30, default: 25 } }; return pageSizes[this.responseMode][entityType] || pageSizes[this.responseMode].default; } } //# sourceMappingURL=AnalyticsTransformer.js.map