UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

610 lines (608 loc) 23.4 kB
/** * AnalyticalFunctions - Pre-built query templates for common analytics tasks */ import { TEMPLATE_CATEGORIES } from './constants.js'; export class AnalyticalFunctions { templates = new Map(); constructor() { this.initializeTemplates(); } /** * Get all available templates */ getTemplates() { return Array.from(this.templates.values()); } /** * Get templates by category */ getTemplatesByCategory(category) { return this.getTemplates().filter(t => t.category === category); } /** * Get a specific template */ getTemplate(id) { return this.templates.get(id); } /** * Apply a template with parameters */ applyTemplate(templateId, params) { const template = this.templates.get(templateId); if (!template) { throw new Error(`Template not found: ${templateId}`); } // Validate parameters this.validateTemplateParams(template, params); // Build intent from template return this.buildIntentFromTemplate(template, params); } /** * Register a custom template */ registerTemplate(template) { this.templates.set(template.id, template); } initializeTemplates() { // Variable Analysis Templates this.templates.set('variable_analysis', { id: 'variable_analysis', name: 'Flag Variable Analysis', description: 'Analyze flags that use a specific variable', category: TEMPLATE_CATEGORIES.VARIABLE_ANALYSIS, parameters: [ { name: 'variable_name', type: 'string', required: true, description: 'Name of the variable to analyze (e.g., cdnVariationSettings)' }, { name: 'include_empty', type: 'boolean', required: false, default: false, description: 'Include flags where the variable exists but has no value' }, { name: 'group_by', type: 'array', required: false, default: ['environment'], description: 'Fields to group results by' } ], baseQuery: ` SELECT f.key as flag_key, f.name as flag_name, f.project_id, fe.environment_key, COUNT(DISTINCT v.key) as variation_count, COUNT(DISTINCT JSON_EXTRACT(v.variables, '$.{variable_name}')) as variations_with_variable FROM flags f LEFT JOIN flag_environments fe ON f.project_id = fe.project_id AND f.key = fe.flag_key LEFT JOIN variations v ON f.project_id = v.project_id AND f.key = v.flag_key WHERE JSON_EXTRACT(f.data_json, '$.variable_definitions.{variable_name}') IS NOT NULL GROUP BY f.key, f.name, f.project_id, fe.environment_key `, jsonataExpression: ` $[variable_definitions.{variable_name} exists].{ "flag": key, "name": name, "environment": environment, "variable_type": variable_definitions.{variable_name}.type, "default_value": variable_definitions.{variable_name}.default_value, "variations_using": variations[variable_values.{variable_name} exists].$count(), "variations_total": variations.$count() } `, examples: [ 'Find all flags using cdnVariationSettings variable', 'Show flags with payment_enabled variable grouped by project' ] }); this.templates.set('variable_usage_summary', { id: 'variable_usage_summary', name: 'Variable Usage Summary', description: 'Summary of all variables used across flags', category: TEMPLATE_CATEGORIES.VARIABLE_ANALYSIS, parameters: [ { name: 'project_id', type: 'string', required: false, description: 'Filter by specific project' } ], baseQuery: ` SELECT JSON_EXTRACT(value.value, '$.key') as variable_name, JSON_EXTRACT(value.value, '$.type') as variable_type, COUNT(DISTINCT f.key) as flag_count FROM flags f, json_each(f.data_json, '$.variable_definitions') as value WHERE 1=1 GROUP BY variable_name, variable_type ORDER BY flag_count DESC `, jsonataExpression: ` $**.variable_definitions.$spread().( $keys := $keys($); $keys.{ "variable": $, "type": $lookup($parent, $).type, "flags_using": $count($parent.^[variable_definitions.$exists($)]) } ) ` }); // Complexity Analysis Templates this.templates.set('flag_complexity', { id: 'flag_complexity', name: 'Flag Complexity Analysis', description: 'Identify complex flags based on variations, rules, and variables', category: TEMPLATE_CATEGORIES.COMPLEXITY_ANALYSIS, parameters: [ { name: 'min_complexity_score', type: 'number', required: false, default: 50, description: 'Minimum complexity score to include' } ], baseQuery: ` SELECT f.key, f.name, f.project_id, COUNT(DISTINCT v.key) as variation_count, COUNT(DISTINCT r.id) as rule_count, JSON_ARRAY_LENGTH(f.data_json, '$.variable_definitions') as variable_count, COUNT(DISTINCT fe.environment_key) as environment_count, (COUNT(DISTINCT v.key) * 10 + COUNT(DISTINCT r.id) * 15 + JSON_ARRAY_LENGTH(f.data_json, '$.variable_definitions') * 5 + COUNT(DISTINCT fe.environment_key) * 5) as complexity_score FROM flags f LEFT JOIN variations v ON f.project_id = v.project_id AND f.key = v.flag_key LEFT JOIN rules r ON f.project_id = r.project_id AND f.key = r.flag_key LEFT JOIN flag_environments fe ON f.project_id = fe.project_id AND f.key = fe.flag_key WHERE f.archived = 0 GROUP BY f.key, f.name, f.project_id HAVING complexity_score >= ? ORDER BY complexity_score DESC `, jsonataExpression: ` $.{ "flag": key, "name": name, "complexity_score": ( variations.$count() * 10 + rules.$count() * 15 + $keys(variable_definitions).$count() * 5 + environments.$count() * 5 ), "breakdown": { "variations": variations.$count(), "rules": rules.$count(), "variables": $keys(variable_definitions).$count(), "environments": environments.$count() } }[$$.complexity_score >= {min_complexity_score}] ` }); this.templates.set('audience_complexity', { id: 'audience_complexity', name: 'Audience Complexity Analysis', description: 'Analyze audience targeting complexity', category: TEMPLATE_CATEGORIES.COMPLEXITY_ANALYSIS, parameters: [], baseQuery: ` SELECT a.id, a.name, a.project_id, JSON_ARRAY_LENGTH(a.conditions) as condition_count, COUNT(DISTINCT e.id) as experiment_count, CASE WHEN a.conditions LIKE '%custom_attribute%' THEN 1 ELSE 0 END as uses_custom_attributes FROM audiences a LEFT JOIN experiments e ON EXISTS (SELECT 1 FROM json_each(e.data_json, '$.audience_ids') WHERE value = a.id) WHERE a.archived = 0 GROUP BY a.id, a.name, a.project_id, a.conditions ORDER BY condition_count DESC `, jsonataExpression: ` $.{ "audience": name, "id": id, "condition_complexity": conditions.( $type($) = "array" ? $count($) : $type($) = "object" ? $keys($).$count() : 1 ), "uses_custom": $contains($string(conditions), "custom_attribute"), "experiment_usage": experiments.$count() } ` }); // Performance Trends Templates this.templates.set('experiment_performance_trends', { id: 'experiment_performance_trends', name: 'Experiment Performance Trends', description: 'Analyze experiment performance over time', category: TEMPLATE_CATEGORIES.PERFORMANCE_TRENDS, parameters: [ { name: 'time_range', type: 'object', required: false, default: { value: 30, unit: 'days' }, description: 'Time range for analysis' }, { name: 'status', type: 'string', required: false, validation: { enum: ['running', 'paused', 'not_started', 'concluded'] } } ], baseQuery: ` SELECT e.id, e.name, e.status, e.type, e.created_time, e.updated_time, er.confidence_level, er.total_count as visitor_count, DATE(e.created_time) as start_date, JULIANDAY('now') - JULIANDAY(e.created_time) as days_running FROM experiments e LEFT JOIN experiment_results er ON e.id = er.experiment_id WHERE e.created_time >= datetime('now', '-{time_value} {time_unit}') AND e.archived = 0 ORDER BY e.created_time DESC `, jsonataExpression: ` $[created_time >= $now() - {time_range}].{ "experiment": name, "status": status, "days_running": $daysSince(created_time), "performance": { "confidence": confidence_level, "visitors": visitor_count } } ` }); // Audience Usage Templates this.templates.set('audience_usage_analysis', { id: 'audience_usage_analysis', name: 'Audience Usage Analysis', description: 'Analyze how audiences are used across experiments', category: TEMPLATE_CATEGORIES.AUDIENCE_USAGE, parameters: [ { name: 'min_usage', type: 'number', required: false, default: 0, description: 'Minimum number of experiments using the audience' } ], baseQuery: ` SELECT a.id, a.name, a.project_id, COUNT(DISTINCT e.id) as experiment_count, GROUP_CONCAT(DISTINCT e.status) as experiment_statuses, MAX(e.updated_time) as last_used FROM audiences a LEFT JOIN experiments e ON EXISTS (SELECT 1 FROM json_each(e.data_json, '$.audience_ids') WHERE value = a.id) WHERE a.archived = 0 GROUP BY a.id, a.name, a.project_id HAVING experiment_count >= ? ORDER BY experiment_count DESC `, jsonataExpression: ` audiences.{ "audience": name, "id": id, "usage": { "experiment_count": experiments.$count(), "active_experiments": experiments[status = "running"].$count(), "last_used": experiments.updated_time.$max() }, "recommendations": experiments.$count() = 0 ? ["Consider archiving unused audience"] : experiments.$count() > 10 ? ["High usage audience - ensure conditions are optimized"] : [] } ` }); this.templates.set('audience_overlap', { id: 'audience_overlap', name: 'Audience Overlap Analysis', description: 'Find experiments using multiple audiences', category: TEMPLATE_CATEGORIES.AUDIENCE_USAGE, parameters: [], baseQuery: ` SELECT e.id, e.name, e.status, JSON_ARRAY_LENGTH(e.data_json, '$.audience_ids') as audience_count, e.data_json FROM experiments e WHERE JSON_ARRAY_LENGTH(e.data_json, '$.audience_ids') > 1 AND e.archived = 0 ORDER BY audience_count DESC `, jsonataExpression: ` experiments[audience_ids.$count() > 1].{ "experiment": name, "audience_count": audience_ids.$count(), "audiences": audience_ids.( $lookup($$.audiences, $).name ), "complexity_warning": audience_ids.$count() > 3 ? "High audience complexity may reduce reach" : null } ` }); // Change Tracking Templates this.templates.set('recent_changes', { id: 'recent_changes', name: 'Recent Changes', description: 'Track recent changes across all entity types', category: TEMPLATE_CATEGORIES.CHANGE_TRACKING, parameters: [ { name: 'days_back', type: 'number', required: false, default: 7, description: 'Number of days to look back' }, { name: 'entity_type', type: 'string', required: false, description: 'Filter by entity type' } ], baseQuery: ` SELECT ch.entity_type, ch.entity_id, ch.entity_name, ch.action, ch.timestamp, ch.changed_by, ch.change_summary FROM change_history ch WHERE ch.timestamp >= datetime('now', '-{days_back} days') AND ch.archived = 0 ORDER BY ch.timestamp DESC LIMIT 100 `, jsonataExpression: ` change_history[timestamp >= $now() - {days_back} * 86400000].{ "entity": entity_type & ": " & entity_name, "action": action, "when": timestamp, "who": changed_by, "summary": change_summary } ` }); // Cross-Entity Templates this.templates.set('entity_relationships', { id: 'entity_relationships', name: 'Entity Relationship Analysis', description: 'Analyze relationships between different entity types', category: TEMPLATE_CATEGORIES.CROSS_ENTITY, parameters: [ { name: 'entity_id', type: 'string', required: true, description: 'ID of the entity to analyze relationships for' }, { name: 'entity_type', type: 'string', required: true, validation: { enum: ['flag', 'experiment', 'audience'] } } ], baseQuery: ` -- This is a complex query that varies based on entity_type -- The actual query is built dynamically SELECT * FROM flags WHERE key = ? `, jsonataExpression: ` $.{ "entity": name, "type": "{entity_type}", "relationships": { "experiments": experiments.$count(), "audiences": audiences.$count(), "variations": variations.$count(), "rules": rules.$count() } } ` }); this.templates.set('unused_entities', { id: 'unused_entities', name: 'Unused Entities Report', description: 'Find entities that are not being used', category: TEMPLATE_CATEGORIES.CROSS_ENTITY, parameters: [], baseQuery: ` -- Union query to find all unused entities SELECT 'audience' as entity_type, id, name, project_id FROM audiences a WHERE NOT EXISTS ( SELECT 1 FROM experiments e WHERE EXISTS (SELECT 1 FROM json_each(e.data_json, '$.audience_ids') WHERE value = a.id) ) AND a.archived = 0 UNION ALL SELECT 'event' as entity_type, id, name, project_id FROM events ev WHERE NOT EXISTS ( SELECT 1 FROM experiments e WHERE EXISTS (SELECT 1 FROM json_each(e.data_json, '$.metrics') WHERE JSON_EXTRACT(value, '$.event_id') = ev.id) ) AND ev.archived = 0 `, jsonataExpression: ` ( audiences[experiments.$count() = 0].{"type": "audience", "name": name, "id": id} ~> $append(events[experiments.$count() = 0].{"type": "event", "name": name, "id": id}) ) ` }); } validateTemplateParams(template, params) { for (const param of template.parameters) { const value = params[param.name]; // Check required if (param.required && (value === undefined || value === null)) { throw new Error(`Required parameter missing: ${param.name}`); } // Check type if (value !== undefined && value !== null) { const actualType = Array.isArray(value) ? 'array' : typeof value; if (actualType !== param.type) { throw new Error(`Parameter ${param.name} must be of type ${param.type}`); } // Check validation rules if (param.validation) { if (param.validation.enum && !param.validation.enum.includes(value)) { throw new Error(`Parameter ${param.name} must be one of: ${param.validation.enum.join(', ')}`); } if (param.validation.min !== undefined && value < param.validation.min) { throw new Error(`Parameter ${param.name} must be at least ${param.validation.min}`); } if (param.validation.max !== undefined && value > param.validation.max) { throw new Error(`Parameter ${param.name} must be at most ${param.validation.max}`); } if (param.validation.pattern) { const regex = new RegExp(param.validation.pattern); if (!regex.test(value)) { throw new Error(`Parameter ${param.name} does not match required pattern`); } } } } } } buildIntentFromTemplate(template, params) { // Apply defaults const finalParams = {}; for (const param of template.parameters) { finalParams[param.name] = params[param.name] ?? param.default; } // Substitute parameters in queries const substitutedQuery = this.substituteParams(template.baseQuery, finalParams); const substitutedJsonata = template.jsonataExpression ? this.substituteParams(template.jsonataExpression, finalParams) : undefined; // Build enhanced intent const intent = { action: 'analyze', primaryEntity: this.inferEntityFromQuery(template.baseQuery), filters: [], aggregations: this.inferAggregations(template.baseQuery), requiresJsonProcessing: !!template.jsonataExpression, jsonataExpression: substitutedJsonata, // Apply default limit from environment variable limit: parseInt(process.env.ANALYTICS_DEFAULT_PAGE_SIZE || '10') }; // Add filters from parameters if (finalParams.entity_type) { intent.filters.push({ field: 'entity_type', operator: 'eq', value: finalParams.entity_type }); } if (finalParams.project_id) { intent.filters.push({ field: 'project_id', operator: 'eq', value: finalParams.project_id }); } return intent; } substituteParams(query, params) { let result = query; for (const [key, value] of Object.entries(params)) { // Handle different parameter types if (typeof value === 'object' && value.value && value.unit) { // Time range parameters result = result .replace(new RegExp(`{${key}}`, 'g'), `${value.value} ${value.unit}`) .replace(new RegExp(`{time_value}`, 'g'), value.value) .replace(new RegExp(`{time_unit}`, 'g'), value.unit); } else { // Simple substitution result = result.replace(new RegExp(`{${key}}`, 'g'), String(value)); } } return result; } inferEntityFromQuery(query) { const upperQuery = query.toUpperCase(); if (upperQuery.includes('FROM FLAGS')) return 'flags'; if (upperQuery.includes('FROM EXPERIMENTS')) return 'experiments'; if (upperQuery.includes('FROM AUDIENCES')) return 'audiences'; if (upperQuery.includes('FROM VARIATIONS')) return 'variations'; if (upperQuery.includes('FROM RULES')) return 'rules'; if (upperQuery.includes('FROM EVENTS')) return 'events'; if (upperQuery.includes('FROM ATTRIBUTES')) return 'attributes'; if (upperQuery.includes('FROM CHANGE_HISTORY')) return 'changes'; return 'flags'; // default } inferAggregations(query) { const aggregations = []; const upperQuery = query.toUpperCase(); if (upperQuery.includes('COUNT(')) aggregations.push('count'); if (upperQuery.includes('SUM(')) aggregations.push('sum'); if (upperQuery.includes('AVG(') || upperQuery.includes('AVERAGE(')) aggregations.push('avg'); if (upperQuery.includes('MIN(')) aggregations.push('min'); if (upperQuery.includes('MAX(')) aggregations.push('max'); if (upperQuery.includes('DISTINCT')) aggregations.push('distinct'); return aggregations; } } //# sourceMappingURL=AnalyticalFunctions.js.map