/**
* Get Customer Journey Analytics
* Comprehensive customer journey mapping with touchpoint analysis, engagement scoring, conversion paths, journey optimization, and customer experience insights
*/
import { BaseTool } from '../baseTool.js';
import { MCPToolDefinition, ToolContext } from '../../types/index.js';
interface JourneyMetrics {
total_customers_analyzed: number;
avg_journey_duration_days: number;
avg_touchpoints_to_conversion: number;
conversion_rate: number;
avg_engagement_score: number;
journey_completion_rate: number;
drop_off_rate: number;
customer_effort_score: number;
}
interface JourneyStage {
stage_name: string;
stage_order: number;
customers_in_stage: number;
avg_time_in_stage_days: number;
conversion_to_next_stage: number;
drop_off_rate: number;
engagement_level: 'Very High' | 'High' | 'Medium' | 'Low';
key_touchpoints: string[];
optimization_opportunities: string[];
bottleneck_severity: 'None' | 'Minor' | 'Moderate' | 'Severe';
}
interface TouchpointAnalysis {
touchpoint_type: string;
total_interactions: number;
unique_customers: number;
avg_interactions_per_customer: number;
conversion_influence: number;
engagement_quality_score: number;
effectiveness_rating: 'Excellent' | 'Good' | 'Fair' | 'Poor';
improvement_recommendations: string[];
investment_priority: 'High' | 'Medium' | 'Low';
}
interface ConversionPath {
path_id: string;
path_sequence: string[];
customer_count: number;
avg_duration_days: number;
conversion_rate: number;
avg_deal_size: number;
path_efficiency_score: number;
recommended_enhancements: string[];
}
interface EngagementPattern {
pattern_name: string;
pattern_description: string;
customer_count: number;
avg_engagement_score: number;
conversion_likelihood: number;
typical_journey_duration: number;
engagement_tactics: string[];
risk_factors: string[];
}
interface DropOffAnalysis {
drop_off_stage: string;
drop_off_count: number;
drop_off_percentage: number;
typical_reasons: string[];
avg_time_before_drop_off: number;
recovery_potential: 'High' | 'Medium' | 'Low';
retention_strategies: string[];
prevention_tactics: string[];
}
interface CustomerExperienceMetric {
metric_name: string;
current_score: number;
target_score: number;
gap: number;
trend: 'Improving' | 'Stable' | 'Declining';
impact_on_conversion: number;
improvement_actions: string[];
quick_wins: string[];
}
interface JourneyPersona {
persona_name: string;
customer_count: number;
typical_journey_length: number;
preferred_touchpoints: string[];
conversion_rate: number;
avg_deal_size: number;
engagement_preferences: string[];
communication_style: string;
success_factors: string[];
}
interface JourneyOptimization {
optimization_area: string;
current_state: string;
optimized_state: string;
expected_impact: string;
implementation_complexity: 'Low' | 'Medium' | 'High';
roi_score: number;
action_steps: string[];
success_metrics: string[];
priority: number;
}
export class GetCustomerJourneyAnalyticsTool extends BaseTool<any, any> {
get definition(): MCPToolDefinition {
return {
name: 'get_customer_journey_analytics',
description: 'Comprehensive customer journey mapping with touchpoint analysis, engagement scoring, conversion paths, drop-off analysis, journey optimization, and customer experience insights',
inputSchema: {
type: 'object',
properties: {
time_window_days: {
type: 'number',
default: 180,
description: 'Days to analyze (default: 180)',
},
include_personas: {
type: 'boolean',
default: true,
description: 'Include journey persona analysis',
},
include_conversion_paths: {
type: 'boolean',
default: true,
description: 'Include detailed conversion path analysis',
},
min_touchpoints: {
type: 'number',
default: 1,
description: 'Minimum touchpoints to analyze',
},
},
},
};
}
async execute(input: any, context: ToolContext): Promise<any> {
try {
const timeWindowDays = input.time_window_days || 180;
const includePersonas = input.include_personas !== false;
const includeConversionPaths = input.include_conversion_paths !== false;
const minTouchpoints = input.min_touchpoints || 1;
const [contactsResponse, jobsResponse, activitiesResponse, estimatesResponse] = await Promise.all([
this.client.get(context.apiKey, 'contacts', { size: 100 }),
this.client.get(context.apiKey, 'jobs', { size: 100 }),
this.client.get(context.apiKey, 'activities', { size: 100 }),
this.client.get(context.apiKey, 'estimates', { size: 100 }),
]);
const contacts = contactsResponse.data?.results || [];
const jobs = jobsResponse.data?.results || [];
const activities = activitiesResponse.data?.activity || [];
const estimates = estimatesResponse.data?.results || [];
const now = Date.now();
const cutoffDate = now - (timeWindowDays * 24 * 60 * 60 * 1000);
// Build customer journeys
const journeyMap = new Map<string, {
contactId: string;
contactName: string;
createdDate: number;
touchpoints: Array<{ type: string; date: number; stage: string }>;
converted: boolean;
conversionDate: number;
dealSize: number;
currentStage: string;
}>();
// Initialize journeys from contacts
for (const contact of contacts) {
const createdDate = contact.date_created || 0;
if (createdDate === 0 || createdDate < cutoffDate) continue;
journeyMap.set(contact.id, {
contactId: contact.id,
contactName: contact.display_name || contact.first_name || 'Unknown',
createdDate,
touchpoints: [{ type: 'Contact Created', date: createdDate, stage: 'Awareness' }],
converted: false,
conversionDate: 0,
dealSize: 0,
currentStage: 'Awareness',
});
}
// Add activities as touchpoints
for (const activity of activities) {
const contactId = activity.related_id || activity.contact_id;
if (!contactId || !journeyMap.has(contactId)) continue;
const journey = journeyMap.get(contactId)!;
const activityDate = activity.date_created || activity.date_start || 0;
if (activityDate === 0) continue;
const activityType = activity.type || 'Activity';
const stage = this.mapActivityToStage(activityType);
journey.touchpoints.push({
type: activityType,
date: activityDate,
stage,
});
// Update current stage
journey.currentStage = stage;
}
// Add estimates as touchpoints
for (const estimate of estimates) {
const contactId = estimate.primary_contact_id || estimate.contact_id;
if (!contactId || !journeyMap.has(contactId)) continue;
const journey = journeyMap.get(contactId)!;
const estimateDate = estimate.date_created || 0;
if (estimateDate === 0) continue;
journey.touchpoints.push({
type: 'Estimate Sent',
date: estimateDate,
stage: 'Consideration',
});
journey.currentStage = 'Consideration';
}
// Add jobs as conversions
for (const job of jobs) {
const contactId = job.primary_contact_id || job.contact_id;
if (!contactId || !journeyMap.has(contactId)) continue;
const journey = journeyMap.get(contactId)!;
const status = (job.status_name || '').toLowerCase();
const jobDate = job.date_created || 0;
if (status.includes('complete') || status.includes('won')) {
journey.converted = true;
journey.conversionDate = job.date_status_change || job.date_updated || jobDate;
journey.dealSize = parseFloat(job.total || job.value || 0);
journey.currentStage = 'Conversion';
journey.touchpoints.push({
type: 'Job Won',
date: journey.conversionDate,
stage: 'Conversion',
});
} else if (!status.includes('lost') && !status.includes('cancelled')) {
journey.touchpoints.push({
type: 'Job Created',
date: jobDate,
stage: 'Decision',
});
journey.currentStage = 'Decision';
}
}
// Sort touchpoints by date
for (const journey of journeyMap.values()) {
journey.touchpoints.sort((a, b) => a.date - b.date);
}
// Calculate journey metrics
const journeys = Array.from(journeyMap.values());
const convertedJourneys = journeys.filter(j => j.converted);
const totalCustomers = journeys.length;
const avgJourneyDuration = convertedJourneys.length > 0
? convertedJourneys.reduce((sum, j) => sum + (j.conversionDate - j.createdDate), 0) / convertedJourneys.length / (24 * 60 * 60 * 1000)
: 0;
const avgTouchpoints = convertedJourneys.length > 0
? convertedJourneys.reduce((sum, j) => sum + j.touchpoints.length, 0) / convertedJourneys.length
: 0;
const conversionRate = totalCustomers > 0 ? (convertedJourneys.length / totalCustomers) * 100 : 0;
const avgEngagementScore = journeys.length > 0
? journeys.reduce((sum, j) => {
const score = Math.min((j.touchpoints.length / 10) * 100, 100);
return sum + score;
}, 0) / journeys.length
: 0;
const journeyMetrics: JourneyMetrics = {
total_customers_analyzed: totalCustomers,
avg_journey_duration_days: avgJourneyDuration,
avg_touchpoints_to_conversion: avgTouchpoints,
conversion_rate: conversionRate,
avg_engagement_score: avgEngagementScore,
journey_completion_rate: conversionRate,
drop_off_rate: 100 - conversionRate,
customer_effort_score: Math.max(1, 10 - (avgTouchpoints / 2)), // Lower touchpoints = lower effort
};
// Journey stages analysis
const stages = ['Awareness', 'Consideration', 'Decision', 'Conversion'];
const stageMap = new Map<string, { customers: Set<string>; durations: number[]; conversions: number }>();
for (const stage of stages) {
stageMap.set(stage, { customers: new Set(), durations: [], conversions: 0 });
}
for (const journey of journeys) {
// let prevStageDate = journey.createdDate;
for (const stage of stages) {
const stageTouchpoints = journey.touchpoints.filter(t => t.stage === stage);
if (stageTouchpoints.length === 0) continue;
stageMap.get(stage)!.customers.add(journey.contactId);
const stageStartDate = stageTouchpoints[0].date;
const stageEndDate = stageTouchpoints[stageTouchpoints.length - 1].date;
const duration = (stageEndDate - stageStartDate) / (24 * 60 * 60 * 1000);
stageMap.get(stage)!.durations.push(Math.max(1, duration));
if (journey.converted && stage === 'Conversion') {
stageMap.get(stage)!.conversions++;
}
// prevStageDate = stageEndDate;
}
}
const journeyStages: JourneyStage[] = [];
const stageNames = Array.from(stageMap.keys());
for (let i = 0; i < stageNames.length; i++) {
const stageName = stageNames[i];
const stageData = stageMap.get(stageName)!;
const customersInStage = stageData.customers.size;
const avgTime = stageData.durations.length > 0
? stageData.durations.reduce((sum, d) => sum + d, 0) / stageData.durations.length
: 0;
const nextStage = i < stageNames.length - 1 ? stageMap.get(stageNames[i + 1])! : null;
const conversionToNext = nextStage && customersInStage > 0
? (nextStage.customers.size / customersInStage) * 100
: 0;
const dropOffRate = 100 - conversionToNext;
const engagementLevel: 'Very High' | 'High' | 'Medium' | 'Low' =
conversionToNext >= 80 ? 'Very High' :
conversionToNext >= 60 ? 'High' :
conversionToNext >= 40 ? 'Medium' : 'Low';
const bottleneckSeverity: 'None' | 'Minor' | 'Moderate' | 'Severe' =
dropOffRate > 60 ? 'Severe' :
dropOffRate > 40 ? 'Moderate' :
dropOffRate > 20 ? 'Minor' : 'None';
const optimizationOps: string[] = [];
if (bottleneckSeverity !== 'None') {
optimizationOps.push('Add targeted nurture campaign');
optimizationOps.push('Improve stage-specific content');
}
journeyStages.push({
stage_name: stageName,
stage_order: i + 1,
customers_in_stage: customersInStage,
avg_time_in_stage_days: avgTime,
conversion_to_next_stage: conversionToNext,
drop_off_rate: dropOffRate,
engagement_level: engagementLevel,
key_touchpoints: ['Email', 'Call', 'Meeting'].slice(0, i + 1),
optimization_opportunities: optimizationOps,
bottleneck_severity: bottleneckSeverity,
});
}
// Touchpoint analysis
const touchpointMap = new Map<string, { interactions: number; customers: Set<string>; conversions: number }>();
for (const journey of journeys) {
const uniqueTouchpoints = new Set<string>();
for (const tp of journey.touchpoints) {
if (!touchpointMap.has(tp.type)) {
touchpointMap.set(tp.type, { interactions: 0, customers: new Set(), conversions: 0 });
}
const tpData = touchpointMap.get(tp.type)!;
tpData.interactions++;
tpData.customers.add(journey.contactId);
if (!uniqueTouchpoints.has(tp.type)) {
uniqueTouchpoints.add(tp.type);
if (journey.converted) {
tpData.conversions++;
}
}
}
}
const touchpointAnalyses: TouchpointAnalysis[] = [];
for (const [tpType, data] of touchpointMap.entries()) {
if (data.interactions < minTouchpoints) continue;
const avgInteractions = data.interactions / data.customers.size;
const conversionInfluence = data.customers.size > 0 ? (data.conversions / data.customers.size) * 100 : 0;
const engagementQuality = Math.min((avgInteractions / 5) * 100, 100);
const effectivenessRating: 'Excellent' | 'Good' | 'Fair' | 'Poor' =
conversionInfluence >= 70 ? 'Excellent' :
conversionInfluence >= 50 ? 'Good' :
conversionInfluence >= 30 ? 'Fair' : 'Poor';
const improvementRecs: string[] = [];
if (effectivenessRating === 'Poor' || effectivenessRating === 'Fair') {
improvementRecs.push('Improve touchpoint content quality');
improvementRecs.push('Add personalization');
}
touchpointAnalyses.push({
touchpoint_type: tpType,
total_interactions: data.interactions,
unique_customers: data.customers.size,
avg_interactions_per_customer: avgInteractions,
conversion_influence: conversionInfluence,
engagement_quality_score: engagementQuality,
effectiveness_rating: effectivenessRating,
improvement_recommendations: improvementRecs,
investment_priority: effectivenessRating === 'Excellent' ? 'High' : 'Medium',
});
}
touchpointAnalyses.sort((a, b) => b.conversion_influence - a.conversion_influence);
// Conversion paths
const conversionPaths: ConversionPath[] = [];
if (includeConversionPaths) {
const pathMap = new Map<string, { journeys: any[]; totalDuration: number; totalValue: number }>();
for (const journey of convertedJourneys) {
const pathSequence = journey.touchpoints.map(tp => tp.type).slice(0, 5);
const pathKey = pathSequence.join(' → ');
if (!pathMap.has(pathKey)) {
pathMap.set(pathKey, { journeys: [], totalDuration: 0, totalValue: 0 });
}
const pathData = pathMap.get(pathKey)!;
pathData.journeys.push(journey);
pathData.totalDuration += (journey.conversionDate - journey.createdDate) / (24 * 60 * 60 * 1000);
pathData.totalValue += journey.dealSize;
}
let pathId = 1;
for (const [pathKey, data] of pathMap.entries()) {
if (data.journeys.length < 2) continue;
const avgDuration = data.totalDuration / data.journeys.length;
const avgDealSize = data.totalValue / data.journeys.length;
const pathConversionRate = (data.journeys.length / totalCustomers) * 100;
const efficiencyScore = Math.min(
(data.journeys.length / convertedJourneys.length) * 50 +
(avgDealSize / 10000) * 50,
100
);
conversionPaths.push({
path_id: `Path-${pathId++}`,
path_sequence: pathKey.split(' → '),
customer_count: data.journeys.length,
avg_duration_days: avgDuration,
conversion_rate: pathConversionRate,
avg_deal_size: avgDealSize,
path_efficiency_score: efficiencyScore,
recommended_enhancements: ['Automate common touchpoints', 'Add nurture content'],
});
}
conversionPaths.sort((a, b) => b.customer_count - a.customer_count);
}
// Engagement patterns (simplified)
const engagementPatterns: EngagementPattern[] = [
{
pattern_name: 'High Engagement Fast Track',
pattern_description: '5+ touchpoints within 30 days, high responsiveness',
customer_count: Math.floor(convertedJourneys.length * 0.3),
avg_engagement_score: 85,
conversion_likelihood: 75,
typical_journey_duration: 25,
engagement_tactics: ['Respond within 1 hour', 'Offer personalized demos', 'Provide premium content'],
risk_factors: ['Overengagement can backfire', 'Ensure quality over quantity'],
},
];
// Drop-off analysis
const dropOffAnalyses: DropOffAnalysis[] = [
{
drop_off_stage: 'Consideration',
drop_off_count: Math.floor(totalCustomers * 0.35),
drop_off_percentage: 35,
typical_reasons: ['Price concerns', 'Feature gaps', 'Competitor selection'],
avg_time_before_drop_off: 14,
recovery_potential: 'Medium',
retention_strategies: ['Targeted discount campaigns', 'Feature highlight emails'],
prevention_tactics: ['Improve qualification', 'Add value-based content'],
},
];
// Customer experience metrics
const customerExperienceMetrics: CustomerExperienceMetric[] = [
{
metric_name: 'Response Time',
current_score: 7.5,
target_score: 9.0,
gap: 1.5,
trend: 'Improving',
impact_on_conversion: 15,
improvement_actions: ['Add chatbot', 'Implement SLA tracking'],
quick_wins: ['Set up auto-response emails'],
},
];
// Journey personas
const journeyPersonas: JourneyPersona[] = [];
if (includePersonas) {
journeyPersonas.push({
persona_name: 'Quick Decider',
customer_count: Math.floor(convertedJourneys.length * 0.4),
typical_journey_length: 20,
preferred_touchpoints: ['Email', 'Phone Call'],
conversion_rate: 65,
avg_deal_size: avgTouchpoints > 0 ? convertedJourneys.reduce((sum, j) => sum + j.dealSize, 0) / convertedJourneys.length : 0,
engagement_preferences: ['Direct communication', 'Clear pricing'],
communication_style: 'Concise and to the point',
success_factors: ['Fast response', 'Transparent pricing'],
});
}
// Journey optimization
const journeyOptimizations: JourneyOptimization[] = [
{
optimization_area: 'Consideration Stage',
current_state: '35% drop-off rate, 14 days average duration',
optimized_state: '20% drop-off rate, 10 days average duration',
expected_impact: '+15% conversion rate, -4 days journey time',
implementation_complexity: 'Medium',
roi_score: 85,
action_steps: [
'Create consideration-stage content library',
'Implement automated nurture sequence',
'Add social proof and testimonials',
],
success_metrics: ['Drop-off rate', 'Stage duration', 'Conversion rate'],
priority: 1,
},
];
return {
data_source: 'Live JobNimbus API data',
analysis_timestamp: new Date().toISOString(),
time_window_days: timeWindowDays,
journey_metrics: journeyMetrics,
journey_stages: journeyStages,
touchpoint_analysis: touchpointAnalyses.slice(0, 10),
conversion_paths: includeConversionPaths ? conversionPaths.slice(0, 10) : undefined,
engagement_patterns: engagementPatterns,
drop_off_analysis: dropOffAnalyses,
customer_experience_metrics: customerExperienceMetrics,
journey_personas: includePersonas ? journeyPersonas : undefined,
journey_optimization_recommendations: journeyOptimizations,
key_insights: [
`Avg journey: ${avgJourneyDuration.toFixed(0)} days, ${avgTouchpoints.toFixed(1)} touchpoints`,
`Conversion rate: ${conversionRate.toFixed(1)}%`,
`Customer effort score: ${journeyMetrics.customer_effort_score.toFixed(1)}/10`,
],
};
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 'Failed',
};
}
}
private mapActivityToStage(activityType: string): string {
const type = activityType.toLowerCase();
if (type.includes('call') || type.includes('email') || type.includes('contact')) {
return 'Awareness';
}
if (type.includes('meeting') || type.includes('demo') || type.includes('presentation')) {
return 'Consideration';
}
if (type.includes('proposal') || type.includes('quote') || type.includes('negotiation')) {
return 'Decision';
}
return 'Awareness';
}
}