@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
1,190 lines โข 69.3 kB
JavaScript
/**
* Template Generator - Interactive template creation
*/
import * as fs from 'fs';
import * as path from 'path';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { OrchestrationRulesLayer } from './OrchestrationRulesLayer';
import { TemplateStore } from '../../orchestration/storage/TemplateStore';
export class TemplateGenerator {
rulesLayer;
constructor() {
this.rulesLayer = new OrchestrationRulesLayer();
}
/**
* Generate a new template
*/
async generate(options = {}) {
const platform = options.platform || 'web';
if (options.pattern) {
const template = await this.fromPattern(options.pattern, platform, options.platformExplicit);
// Allow customization of pattern-based templates unless explicitly disabled
if (options.customize !== false) {
return this.customizeTemplate(template, platform);
}
return template;
}
if (options.interactive) {
return this.interactiveWizard(platform);
}
// Default: basic template structure
return this.createBasicTemplate(platform);
}
/**
* Create from a pattern
*/
async fromPattern(pattern, platform, platformExplicit) {
const patterns = this.getAvailablePatterns();
const selectedPattern = patterns[pattern];
if (!selectedPattern) {
throw new Error(`Unknown pattern: ${pattern}. Available: ${Object.keys(patterns).join(', ')}`);
}
// For platform-agnostic patterns, ask which platform to use if not explicitly set
const platformAgnosticPatterns = ['full-stack', 'e-commerce', 'event-tracking', 'audience-segmentation'];
// Only ask if platform wasn't explicitly set via --platform flag
if (platformAgnosticPatterns.includes(pattern) && !platformExplicit) {
try {
const { selectedPlatform } = await inquirer.prompt([
{
type: 'list',
name: 'selectedPlatform',
message: 'Which platform are you building for?',
choices: [
{ name: '๐ฉ Feature Experimentation (Flags, Rules, Variables)', value: 'feature' },
{ name: '๐งช Web Experimentation (A/B Tests, Campaigns, Pages)', value: 'web' }
],
default: platform
}
]);
platform = selectedPlatform;
}
catch (error) {
// If prompt fails, use the default platform
console.log(`Using ${platform} platform...`);
}
}
return selectedPattern.generate(platform);
}
/**
* Interactive wizard - Guided template creation
*/
async interactiveWizard(platform) {
// Skip TTY check - all our environments support TTY
// The check was failing incorrectly when run through npm scripts
console.log('\n๐ Welcome to the Optimizely Template Creator!\n');
try {
// Step 1: Basic template information
const templateInfo = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: 'Enter a description for your template:',
default: `${platform === 'web' ? 'Web' : 'Feature'} Experimentation Template`
},
{
type: 'list',
name: 'goal',
message: 'What would you like to create?',
choices: [
{ name: '๐งช A/B Test', value: 'ab-test' },
{ name: '๐ฉ Feature Flag with Rollout', value: 'feature-flag' },
{ name: '๐ฏ Audience-based Experiment', value: 'audience-experiment' },
{ name: '๐๏ธ Custom Template', value: 'custom' }
]
}
]);
// Initialize template
const template = {
version: '1.0',
description: templateInfo.description,
parameters: [
{
name: 'project_id',
type: 'integer',
required: true,
description: 'The project ID where entities will be created'
}
],
steps: []
};
// Handle different template goals
switch (templateInfo.goal) {
case 'ab-test':
await this.buildABTestInteractive(template, platform);
break;
case 'feature-flag':
await this.buildFeatureFlagInteractive(template);
break;
case 'audience-experiment':
await this.buildAudienceExperimentInteractive(template, platform);
break;
case 'custom':
await this.buildCustomInteractive(template, platform);
break;
}
// Step: Review and confirmation
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Template created! Would you like to review it?',
default: true
}
]);
if (confirmed) {
console.log('\n๐ Template Preview:\n');
console.log(JSON.stringify(template, null, 2));
}
return template;
}
catch (error) {
console.log('\nโ ๏ธ Interactive mode was interrupted or not available.');
console.log(' Use command-line options to customize:');
console.log(' --pattern <pattern-name>');
console.log(' --name "My Template"');
console.log(' --add-event "Event Name"');
console.log(' --experiment-name "Test Name" (web)');
console.log(' --flag-name "Flag Name" (feature)\n');
// Throw error to prevent saving an empty template
throw new Error('Interactive mode interrupted');
}
}
/**
* Customize a template with user input
*/
async customizeTemplate(template, platform) {
try {
console.log('\n๐ Customize your template:\n');
const customization = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Template name:',
default: template.description || 'My Template'
},
{
type: 'input',
name: 'project_id',
message: 'Default project ID (optional):',
validate: (input) => {
if (!input)
return true;
return !isNaN(parseInt(input)) || 'Must be a number';
}
},
{
type: 'confirm',
name: 'customize_names',
message: 'Would you like to customize entity names?',
default: true
}
]);
// Update template description
template.description = customization.name;
// Update default project ID if provided
if (customization.project_id) {
const projectParam = template.parameters.find(p => p.name === 'project_id');
if (projectParam) {
projectParam.default = parseInt(customization.project_id);
}
}
// Customize entity names if requested
if (customization.customize_names) {
console.log('\n๐ท๏ธ Customize entity names (press enter to keep defaults):\n');
for (const step of template.steps) {
if (step.template?.inputs?.name) {
const currentName = step.template.inputs.name;
const entityType = step.id.replace('create_', '').replace(/_/g, ' ');
const { newName } = await inquirer.prompt([
{
type: 'input',
name: 'newName',
message: `${entityType}: `,
default: currentName
}
]);
if (newName !== currentName) {
step.template.inputs.name = newName;
// Update key if it exists
if (step.template.inputs.key) {
step.template.inputs.key = newName.toLowerCase().replace(/\s+/g, '_');
}
}
}
}
}
return template;
}
catch (error) {
console.log('\nโ ๏ธ Using default values...');
return template;
}
}
/**
* Create a basic template structure
*/
createBasicTemplate(platform) {
const template = {
version: '1.0',
description: 'Basic orchestration template',
parameters: [
{
name: 'project_id',
type: 'integer',
required: true,
description: 'The project ID'
}
],
steps: []
};
// Add a default event to avoid empty template
this.addCreateStep(template, 'event', 'create_default_event', {
name: 'Default Event',
key: 'default_event',
description: 'Default tracking event'
});
return template;
}
/**
* Add a step to create an entity
*/
addCreateStep(template, entityType, stepId, inputs = {}) {
const step = {
id: stepId,
name: `Create ${entityType}`,
type: 'template',
template: {
system_template_id: `optimizely/${entityType}/create`,
inputs: {
project_id: '${project_id}',
...inputs
}
}
};
template.steps.push(step);
}
/**
* Get available patterns
*/
getAvailablePatterns() {
return {
// Basic patterns
'ab-test': {
name: 'A/B Test',
description: 'Create an A/B test with variations',
generate: (platform) => this.createABTestTemplate(platform)
},
'feature-flag': {
name: 'Feature Flag',
description: 'Create a feature flag with rollout rules',
generate: () => this.createFeatureFlagTemplate()
},
'campaign': {
name: 'Campaign',
description: 'Create a campaign with experiments',
generate: () => this.createCampaignTemplate()
},
// Advanced patterns
'personalization': {
name: 'Personalization Campaign',
description: 'Multi-experience personalization with audiences',
generate: () => this.createPersonalizationTemplate()
},
'campaign-builder': {
name: 'Campaign Builder',
description: 'Build a custom campaign with multiple experiments interactively',
generate: () => this.buildCampaignInteractive()
},
'feature-rollout': {
name: 'Progressive Feature Rollout',
description: 'Gradual feature rollout with percentage stages',
generate: () => this.createProgressiveRolloutTemplate()
},
'targeted-delivery': {
name: 'Targeted Delivery',
description: 'Single variation delivery without metrics',
generate: () => this.createTargetedDeliveryTemplate()
},
'multi-page-test': {
name: 'Multi-Page Test',
description: 'A/B test across multiple pages',
generate: () => this.createMultiPageTestTemplate()
},
'multi-armed-bandit': {
name: 'Multi-Armed Bandit',
description: 'Auto-optimizing experiment',
generate: () => this.createMABTemplate()
},
// Complete setups
'full-stack': {
name: 'Full Stack Setup',
description: 'Complete setup with audiences, events, and experiments',
generate: (platform) => this.createFullStackTemplate(platform)
},
'e-commerce': {
name: 'E-commerce Optimization',
description: 'Complete e-commerce testing setup',
generate: () => this.createEcommerceTemplate()
},
'saas-onboarding': {
name: 'SaaS Onboarding Flow',
description: 'Optimize user onboarding experience',
generate: () => this.createSaaSOnboardingTemplate()
}
};
}
/**
* Pattern: A/B Test Template
*/
createABTestTemplate(platform) {
const template = this.createBasicTemplate(platform);
template.description = 'A/B Test Template';
// Add event
this.addCreateStep(template, 'event', 'create_event', {
name: 'Conversion Event',
key: 'conversion_event',
description: 'Track conversions'
});
if (platform === 'web') {
// Web experiment
this.addCreateStep(template, 'experiment', 'create_experiment', {
name: 'A/B Test Experiment',
key: 'ab_test_experiment',
status: 'not_started',
type: 'a/b',
variations: [
{
name: 'Control',
key: 'control',
weight: 5000
},
{
name: 'Treatment',
key: 'treatment',
weight: 5000
}
],
metrics: [
{
event_id: '${create_event.id}',
scope: 'visitor'
}
]
});
}
else {
// Feature experiment (ruleset)
this.addCreateStep(template, 'flag', 'create_flag', {
name: 'Test Feature Flag',
key: 'test_feature_flag',
description: 'Feature flag for A/B test',
variations: [
{
key: 'control',
name: 'Control',
value: 'false'
},
{
key: 'treatment',
name: 'Treatment',
value: 'true'
}
]
});
this.addCreateStep(template, 'ruleset', 'create_ruleset', {
flag_key: '${create_flag.key}',
type: 'a/b_test',
rules: [
{
key: 'ab_test_rule',
variations: [
{
variation_key: 'control',
weight: 5000
},
{
variation_key: 'treatment',
weight: 5000
}
],
metrics: [
{
event_key: '${create_event.key}',
scope: 'visitor'
}
]
}
]
});
}
return template;
}
/**
* Pattern: Feature Flag Template
*/
createFeatureFlagTemplate() {
const template = this.createBasicTemplate('feature');
template.description = 'Feature Flag Template';
this.addCreateStep(template, 'flag', 'create_flag', {
name: 'New Feature Flag',
key: 'new_feature_flag',
description: 'Control feature availability',
variations: [
{
key: 'off',
name: 'Off',
value: 'false'
},
{
key: 'on',
name: 'On',
value: 'true'
}
]
});
this.addCreateStep(template, 'ruleset', 'create_rollout', {
flag_key: '${create_flag.key}',
type: 'rollout',
rules: [
{
key: 'rollout_rule',
percentage_included: 0,
deliver: 'off'
}
]
});
return template;
}
/**
* Pattern: Campaign Template
*/
createCampaignTemplate() {
const template = this.createBasicTemplate('web');
template.description = 'Campaign Template';
this.addCreateStep(template, 'campaign', 'create_campaign', {
name: 'Marketing Campaign',
holdback: 1000,
metrics: {
scope: 'session' // Required for campaigns!
}
});
return template;
}
/**
* Pattern: Full Stack Template
*/
createFullStackTemplate(platform) {
const template = this.createBasicTemplate(platform);
template.description = 'Full Stack Setup';
// Create audience
this.addCreateStep(template, 'audience', 'create_audience', {
name: 'Beta Users',
conditions: JSON.stringify([
'and',
['or', ['custom_attribute', 'plan_type', 'equals', 'beta']]
])
});
// Create events
this.addCreateStep(template, 'event', 'create_conversion_event', {
name: 'Purchase Complete',
key: 'purchase_complete',
description: 'Track completed purchases'
});
this.addCreateStep(template, 'event', 'create_metric_event', {
name: 'Revenue',
key: 'revenue',
description: 'Track revenue amount'
});
// Platform-specific setup
if (platform === 'web') {
// Web Experimentation: Pages, Experiments, Campaigns
this.addCreateStep(template, 'page', 'create_page', {
name: 'Checkout Page',
edit_url: 'https://example.com/checkout',
activation_type: 'immediate',
activation_code: 'window.optimizely.push({"type": "page", "pageName": "checkout"});'
});
this.addCreateStep(template, 'experiment', 'create_experiment', {
name: 'Checkout Flow Test',
status: 'not_started',
type: 'a/b',
audience_conditions: 'everyone',
page_ids: ['${create_page.id}'],
variations: [
{
name: 'Original',
weight: 5000
},
{
name: 'Simplified',
weight: 5000
}
],
metrics: [
{
event_id: '${create_conversion_event.id}',
scope: 'visitor'
}
]
});
}
else {
// Feature Experimentation: Flags, Rules, Variables
this.addCreateStep(template, 'flag', 'create_feature_flag', {
name: 'Checkout Feature',
key: 'checkout_feature',
description: 'Controls checkout flow experience',
variations: [
{
key: 'off',
name: 'Off',
value: 'false'
},
{
key: 'on',
name: 'On',
value: 'true'
}
]
});
// Add variable definitions for configuration
this.addCreateStep(template, 'variable_definition', 'create_config_variable', {
flag_key: '${create_feature_flag.key}',
name: 'checkout_config',
type: 'json',
default_value: JSON.stringify({
flow_type: 'multi-step',
show_progress_bar: true,
enable_guest_checkout: false
})
});
// Add rollout rules
this.addCreateStep(template, 'ruleset', 'create_rollout_rules', {
flag_key: '${create_feature_flag.key}',
type: 'rollout',
rules: [
{
key: 'beta_users_rule',
audience_conditions: 'audience_ids=${create_audience.id}',
percentage_included: 10000, // 100% of beta users
deliver: 'on'
},
{
key: 'general_rollout',
percentage_included: 1000, // 10% general rollout
deliver: 'on'
}
]
});
// Add A/B test ruleset
this.addCreateStep(template, 'ruleset', 'create_ab_test', {
flag_key: '${create_feature_flag.key}',
type: 'a/b_test',
rules: [
{
key: 'checkout_test',
variations: [
{
variation_key: 'off',
weight: 5000
},
{
variation_key: 'on',
weight: 5000
}
],
metrics: [
{
event_key: '${create_conversion_event.key}'
}
]
}
]
});
}
return template;
}
/**
* Save template to database and optionally to file
*/
async saveTemplate(cliTemplate, outputPath) {
// Generate a proper template ID
const templateId = `tpl_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
// Determine directory from env variable or use default
const templateDir = process.env.ORCHESTRATION_TEMPLATES_DIR ||
path.join(process.cwd(), 'orchestration-templates');
// Determine file path
let filePath;
if (outputPath) {
// If output path is absolute, use it as-is
// If relative, put it in the templates directory
filePath = path.isAbsolute(outputPath) ? outputPath : path.join(templateDir, outputPath);
}
else {
// Default: save in templates directory with template ID as filename
filePath = path.join(templateDir, `${templateId}.json`);
}
// Extract template name from description or file path
let templateName = cliTemplate.description || 'Orchestration Template';
if (outputPath && !cliTemplate.description) {
const fileName = path.basename(outputPath, '.json');
templateName = fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
// Convert CLI template to proper OrchestrationTemplate format
const orchestrationTemplate = {
id: templateId,
name: templateName,
description: cliTemplate.description,
version: cliTemplate.version || '1.0.0',
type: 'user',
platform: 'both', // Default to both, should be determined from template content
author: process.env.USER || 'cli-user',
// Convert parameters from array to object format
parameters: cliTemplate.parameters.reduce((acc, param) => {
acc[param.name] = {
type: param.type,
description: param.description || '',
required: param.required,
default: param.default
};
return acc;
}, {}),
// Convert steps - ensure they have the right structure
steps: cliTemplate.steps.map(step => ({
id: step.id,
name: step.name,
type: 'template',
template: step.template ? {
entity_type: step.template.entity_type || 'unknown', // Required for new format
system_template_id: step.template.system_template_id,
operation: 'create', // Default to create
inputs: step.template.inputs
} : undefined
}))
};
try {
// Save to database
const templateStore = new TemplateStore();
await templateStore.initialize();
await templateStore.createTemplate(orchestrationTemplate);
console.log(`\nโ
Template saved to database with ID: ${templateId}`);
// Also save to file for reference
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Save the full template (with database metadata) to file
const fullTemplate = {
...orchestrationTemplate,
_metadata: {
cli_generated: true,
file_path: filePath,
database_id: templateId
}
};
fs.writeFileSync(filePath, JSON.stringify(fullTemplate, null, 2));
console.log(`๐ Template also saved to file: ${filePath}`);
return templateId;
}
catch (error) {
console.error('โ Failed to save template to database:', error);
// Fallback: save to file only
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(cliTemplate, null, 2));
console.log(`โ ๏ธ Template saved to file only: ${filePath}`);
console.log(' (Database storage failed - template won\'t be available to MCP tools)');
throw error;
}
}
/**
* Build A/B Test template interactively
*/
async buildABTestInteractive(template, platform) {
console.log('\n๐งช Setting up A/B Test...\n');
// Get test details
const testDetails = await inquirer.prompt([
{
type: 'input',
name: 'testName',
message: 'Enter the name for your A/B test:',
validate: (input) => input.length > 0 || 'Test name is required'
},
{
type: 'input',
name: 'testKey',
message: 'Enter the key (URL-safe identifier):',
default: (answers) => answers.testName.toLowerCase().replace(/\s+/g, '_'),
validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only'
}
]);
// Get variation details
const { variationCount } = await inquirer.prompt([
{
type: 'number',
name: 'variationCount',
message: 'How many variations (including control)?',
default: 2,
validate: (input) => (input !== undefined && input >= 2) || 'Must have at least 2 variations'
}
]);
const variations = [];
let totalWeight = 0;
for (let i = 0; i < variationCount; i++) {
const isLast = i === variationCount - 1;
const remainingWeight = 10000 - totalWeight;
const variation = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: `Enter name for variation ${i + 1}:`,
default: i === 0 ? 'Control' : `Treatment ${i}`
},
{
type: 'input',
name: 'key',
message: `Enter key for variation ${i + 1}:`,
default: (answers) => answers.name.toLowerCase().replace(/\s+/g, '_')
},
{
type: 'number',
name: 'weight',
message: `Enter traffic allocation (0-${remainingWeight}):`,
default: isLast ? remainingWeight : Math.floor(remainingWeight / (variationCount - i)),
validate: (input) => {
if (input === undefined || input < 0 || input > remainingWeight) {
return `Weight must be between 0 and ${remainingWeight}`;
}
return true;
}
}
]);
totalWeight += variation.weight;
variations.push(variation);
}
// Create event for tracking
this.addCreateStep(template, 'event', 'create_conversion_event', {
name: `${testDetails.testName} Conversion`,
key: `${testDetails.testKey}_conversion`,
description: 'Conversion event for A/B test'
});
if (platform === 'web') {
// Web experiment
this.addCreateStep(template, 'experiment', 'create_experiment', {
name: testDetails.testName,
key: testDetails.testKey,
status: 'not_started',
type: 'a/b',
variations: variations,
metrics: [
{
event_id: '${create_conversion_event.id}',
scope: 'visitor'
}
]
});
}
else {
// Feature flag + ruleset for Feature Experimentation
this.addCreateStep(template, 'flag', 'create_flag', {
name: testDetails.testName,
key: testDetails.testKey,
description: 'Feature flag for A/B test',
variations: variations.map(v => ({
key: v.key,
name: v.name,
value: v.key === 'control' ? 'false' : 'true'
}))
});
this.addCreateStep(template, 'ruleset', 'create_ruleset', {
flag_key: '${create_flag.key}',
type: 'a/b_test',
rules: [
{
key: `${testDetails.testKey}_rule`,
variations: variations.map(v => ({
variation_key: v.key,
weight: v.weight
})),
metrics: [
{
event_key: '${create_conversion_event.key}',
scope: 'visitor'
}
]
}
]
});
}
}
/**
* Build Feature Flag template interactively
*/
async buildFeatureFlagInteractive(template) {
console.log('\n๐ฉ Setting up Feature Flag...\n');
const flagDetails = await inquirer.prompt([
{
type: 'input',
name: 'flagName',
message: 'Enter the name for your feature flag:',
validate: (input) => input.length > 0 || 'Flag name is required'
},
{
type: 'input',
name: 'flagKey',
message: 'Enter the key (URL-safe identifier):',
default: (answers) => answers.flagName.toLowerCase().replace(/\s+/g, '_'),
validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only'
},
{
type: 'input',
name: 'description',
message: 'Enter a description for the flag:',
default: 'Feature flag for controlling feature availability'
},
{
type: 'number',
name: 'initialRollout',
message: 'Initial rollout percentage (0-100):',
default: 0,
validate: (input) => (input !== undefined && input >= 0 && input <= 100) || 'Must be between 0 and 100'
}
]);
// Create flag
this.addCreateStep(template, 'flag', 'create_flag', {
name: flagDetails.flagName,
key: flagDetails.flagKey,
description: flagDetails.description,
variations: [
{
key: 'off',
name: 'Off',
value: 'false'
},
{
key: 'on',
name: 'On',
value: 'true'
}
]
});
// Create rollout ruleset
this.addCreateStep(template, 'ruleset', 'create_rollout', {
flag_key: '${create_flag.key}',
type: 'rollout',
rules: [
{
key: 'rollout_rule',
percentage_included: flagDetails.initialRollout * 100, // Convert to basis points
deliver: flagDetails.initialRollout > 0 ? 'on' : 'off'
}
]
});
}
/**
* Build Audience-based Experiment interactively
*/
async buildAudienceExperimentInteractive(template, platform) {
console.log('\n๐ฏ Setting up Audience-based Experiment...\n');
// Get audience details
const audienceDetails = await inquirer.prompt([
{
type: 'input',
name: 'audienceName',
message: 'Enter the name for your audience:',
validate: (input) => input.length > 0 || 'Audience name is required'
},
{
type: 'list',
name: 'conditionType',
message: 'What type of audience condition?',
choices: [
{ name: 'Custom Attribute', value: 'custom_attribute' },
{ name: 'Browser Type', value: 'browser_type' },
{ name: 'Device Type', value: 'device_type' },
{ name: 'Location', value: 'location' }
]
}
]);
let conditions;
switch (audienceDetails.conditionType) {
case 'custom_attribute':
const attrDetails = await inquirer.prompt([
{
type: 'input',
name: 'attributeName',
message: 'Enter the attribute name:',
default: 'plan_type'
},
{
type: 'input',
name: 'attributeValue',
message: 'Enter the attribute value:',
default: 'premium'
}
]);
conditions = JSON.stringify([
'and',
['or', ['custom_attribute', attrDetails.attributeName, 'equals', attrDetails.attributeValue]]
]);
break;
default:
// Simplified for other types
conditions = JSON.stringify(['and', ['or', [audienceDetails.conditionType, 'equals', 'value']]]);
}
// Create audience
this.addCreateStep(template, 'audience', 'create_audience', {
name: audienceDetails.audienceName,
conditions: conditions
});
// Now create an experiment using this audience
await this.buildABTestInteractive(template, platform);
// Update the last experiment/ruleset step to include audience
const lastStep = template.steps[template.steps.length - 1];
if (lastStep && lastStep.template) {
if (platform === 'web') {
lastStep.template.inputs.audience_conditions = 'audience_ids=${create_audience.id}';
}
else {
// For feature flags, add audience to the rule
if (lastStep.template.inputs.rules && lastStep.template.inputs.rules[0]) {
lastStep.template.inputs.rules[0].audience_conditions = [
{
audience_id: '${create_audience.id}'
}
];
}
}
}
}
/**
* Build custom template interactively
*/
async buildCustomInteractive(template, platform) {
console.log('\n๐๏ธ Building Custom Template...\n');
let addMore = true;
while (addMore) {
const entityChoices = this.getEntityChoices(platform);
const { entityType } = await inquirer.prompt([
{
type: 'list',
name: 'entityType',
message: 'What entity would you like to add?',
choices: entityChoices.map(e => ({ name: e.name, value: e.value }))
}
]);
const { stepId, entityName } = await inquirer.prompt([
{
type: 'input',
name: 'stepId',
message: 'Enter a unique ID for this step:',
default: `create_${entityType}`,
validate: (input) => {
if (!input)
return 'Step ID is required';
if (template.steps.some(s => s.id === input))
return 'Step ID must be unique';
return true;
}
},
{
type: 'input',
name: 'entityName',
message: `Enter the name for the ${entityType}:`,
validate: (input) => input.length > 0 || 'Name is required'
}
]);
// Get required fields for this entity type
const entityChoice = entityChoices.find(e => e.value === entityType);
const inputs = {
name: entityName
};
// Add entity-specific fields
if (entityType === 'flag' || entityType === 'event') {
const { key } = await inquirer.prompt([
{
type: 'input',
name: 'key',
message: 'Enter the key:',
default: entityName.toLowerCase().replace(/\s+/g, '_'),
validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only'
}
]);
inputs.key = key;
}
// Add page-specific fields
if (entityType === 'page') {
const { editUrl } = await inquirer.prompt([
{
type: 'input',
name: 'editUrl',
message: 'Enter the page URL:',
validate: (input) => input.startsWith('http') || 'Must be a valid URL'
}
]);
inputs.edit_url = editUrl;
}
this.addCreateStep(template, entityType, stepId, inputs);
const { continueAdding } = await inquirer.prompt([
{
type: 'confirm',
name: 'continueAdding',
message: 'Would you like to add another entity?',
default: false
}
]);
addMore = continueAdding;
}
}
/**
* Get entity choices for a platform
*/
getEntityChoices(platform) {
// Web Experimentation specific entities
const webEntities = [
{ name: '๐งช Experiment', value: 'experiment', description: 'A/B test or personalization', requiredFields: ['name', 'project_id'] },
{ name: '๐ญ Campaign', value: 'campaign', description: 'Group of experiments', requiredFields: ['name', 'project_id'] },
{ name: '๐ Page', value: 'page', description: 'Page for targeting', requiredFields: ['name', 'project_id', 'edit_url'] },
{ name: '๐ Extension', value: 'extension', description: 'Custom code extension', requiredFields: ['name', 'project_id', 'implementation'] },
{ name: '๐ฒ Variation', value: 'variation', description: 'Experiment variation', requiredFields: ['name', 'weight'] }
];
// Feature Experimentation specific entities
const featureEntities = [
{ name: '๐ฉ Feature Flag', value: 'flag', description: 'Feature toggle with variables', requiredFields: ['name', 'key', 'project_id'] },
{ name: '๐ Ruleset', value: 'ruleset', description: 'Flag delivery rules (rollout/a-b test)', requiredFields: ['flag_key', 'project_id'] },
{ name: '๐ Rule', value: 'rule', description: 'Individual targeting rule', requiredFields: ['name', 'percentage_included'] },
{ name: '๐ง Variable Definition', value: 'variable_definition', description: 'Flag variable configuration', requiredFields: ['key', 'type', 'default_value'] },
{ name: '๐ Feature', value: 'feature', description: 'Feature grouping', requiredFields: ['name', 'key'] }
];
// Shared entities (available on both platforms)
const sharedEntities = [
{ name: '๐ฏ Audience', value: 'audience', description: 'User segment with conditions', requiredFields: ['name', 'project_id', 'conditions'] },
{ name: '๐ Event', value: 'event', description: 'Tracking event for metrics', requiredFields: ['name', 'key', 'project_id'] },
{ name: '๐ท๏ธ Attribute', value: 'attribute', description: 'Custom user attribute', requiredFields: ['name', 'key', 'project_id'] },
{ name: '๐ List Attribute', value: 'list_attribute', description: 'List-type custom attribute', requiredFields: ['name', 'key', 'list_type'] },
{ name: '๐ Environment', value: 'environment', description: 'Project environment', requiredFields: ['name', 'key'] },
{ name: '๐ Webhook', value: 'webhook', description: 'Webhook integration', requiredFields: ['name', 'url', 'event_types'] },
{ name: '๐ฅ Group', value: 'group', description: 'User exclusion group', requiredFields: ['name', 'policy'] },
{ name: '๐ค Collaborator', value: 'collaborator', description: 'Project collaborator', requiredFields: ['email', 'role'] },
{ name: '๐ข Project', value: 'project', description: 'New project', requiredFields: ['name', 'platform'] },
{ name: '๐ Segment', value: 'segment', description: 'Analytics segment', requiredFields: ['name', 'conditions'] }
];
// Advanced/specialized entities
const advancedEntities = [
{ name: '๐ Targeted Delivery', value: 'targeted_delivery', description: 'Single variation delivery (no metrics)', requiredFields: ['name', 'audience'] },
{ name: '๐ฐ Multi-Armed Bandit', value: 'mab', description: 'Auto-optimizing experiment', requiredFields: ['name', 'metric'] },
{ name: 'โก Stats Accelerator', value: 'stats_accelerator', description: 'Fast statistical testing', requiredFields: ['name', 'confidence_level'] },
{ name: '๐ Mutually Exclusive Group', value: 'mutex_group', description: 'Experiment exclusion group', requiredFields: ['name', 'experiments'] }
];
// Return entities based on platform
if (platform === 'web') {
return [...webEntities, ...sharedEntities, ...advancedEntities.filter(e => ['targeted_delivery', 'mab', 'stats_accelerator', 'mutex_group'].includes(e.value))];
}
else if (platform === 'feature') {
return [...featureEntities, ...sharedEntities];
}
else {
// 'both' platform - return all entities
return [...webEntities, ...featureEntities, ...sharedEntities, ...advancedEntities];
}
}
/**
* Interactive Campaign Builder
*/
async buildCampaignInteractive() {
const template = this.createBasicTemplate('web');
template.description = 'Custom Campaign with Multiple Experiments';
console.log('\n๐ฏ Campaign Builder - Create a personalization campaign with custom experiments\n');
// Get campaign details
const { campaignName, holdback } = await inquirer.prompt([
{
type: 'input',
name: 'campaignName',
message: 'Campaign name:',
default: 'Personalization Campaign'
},
{
type: 'number',
name: 'holdback',
message: 'Campaign holdback % (0-10):',
default: 0,
validate: (input) => (input !== undefined && input >= 0 && input <= 10) || 'Must be between 0 and 10'
}
]);
// Create campaign step
this.addCreateStep(template, 'campaign', 'create_campaign', {
name: campaignName,
holdback: holdback * 1000 // Convert to basis points
});
// Ask about pages
const { pageCount } = await inquirer.prompt([
{
type: 'number',
name: 'pageCount',
message: 'How many pages will this campaign run on?',
default: 1,
validate: (input) => (input !== undefined && input >= 1) || 'At least one page required'
}
]);
const pageIds = [];
for (let i = 0; i < pageCount; i++) {
const { pageName, pageUrl } = await inquirer.prompt([
{
type: 'input',
name: 'pageName',
message: `Page ${i + 1} name:`,
default: i === 0 ? 'Homepage' : `Page ${i + 1}`
},
{
type: 'input',
name: 'pageUrl',
message: `Page ${i + 1} URL:`,
default: 'https://example.com'
}
]);
const pageStepId = `create_page_${i + 1}`;
this.addCreateStep(template, 'page', pageStepId, {
name: pageName,
edit_url: pageUrl
});
pageIds.push(`\${${pageStepId}.id}`);
}
// Build experiments
let addMoreExperiments = true;
let experimentCount = 0;
while (addMoreExperiments) {
experimentCount++;
console.log(chalk.blue(`\n๐ Experiment ${experimentCount}:`));
const { experimentName, audienceType } = await inquirer.prompt([
{
type: 'input',
name: 'experimentName',
message: 'Experiment name:',
default: `Experience ${experimentCount}`
},
{
type: 'list',
name: 'audienceType',
message: 'Target audience:',
choices: [
{ name: 'Everyone', value: 'everyone' },
{ name: 'New visitors', value: 'new' },
{ name: 'Returning visitors', value: 'returning' },
{ name: 'Mobile users', value: 'mobile' },
{ name: 'Desktop users', value: 'desktop' },
{ name: 'Custom audience (existing)', value: 'custom' },
{ name: 'Create new audience', value: 'create' }
]
}
]);
let audienceCondition = 'everyone';
if (audienceType === 'create') {
const { audienceName, attributeName, operator, value } = await inquirer.prompt([
{
type: 'input',
name: 'audienceName',
message: 'Audience name:',
default: `Audience ${experi