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