UNPKG

cakemail-mcp-server

Version:

Enterprise MCP server for Cakemail API integration with Claude AI - includes comprehensive template management, list management, sub-account management, BEEeditor visual email design, and advanced analytics

763 lines 32.9 kB
// Logs API operations import { BaseApiClient } from './base-client.js'; import logger from '../utils/logger.js'; export class LogsApi extends BaseApiClient { /** * Get campaign logs with filtering and pagination */ async getCampaignLogs(campaignId, params) { const apiParams = {}; const filters = []; // Handle pagination if (params?.page) apiParams.page = params.page; if (params?.per_page) apiParams.per_page = params.per_page; if (params?.with_count !== undefined) apiParams.with_count = params.with_count; // Handle cursor pagination if (params?.cursor) apiParams.cursor = params.cursor; // Handle sorting if (params?.sort) { apiParams.sort = params.sort; } // Handle time range filtering if (params?.start_time) apiParams.start_time = params.start_time; if (params?.end_time) apiParams.end_time = params.end_time; // Handle filtering with correct API syntax (term==value;term2==value2) if (params?.type) { filters.push(`type==${params.type}`); } // If custom filter is provided, use it (advanced users) if (params?.filter) { if (filters.length > 0) { filters.push(params.filter); } else { apiParams.filter = params.filter; } } if (filters.length > 0 && !params?.filter) { apiParams.filter = filters.join(';'); } // Add account_id support for proper scoping if (params?.account_id) { apiParams.account_id = params.account_id; } else { // Try to get current account ID for better results const accountId = await this.getCurrentAccountId(); if (accountId) { apiParams.account_id = accountId; } } // Validate per_page limits if (apiParams.per_page && apiParams.per_page > 100) { throw new Error('per_page cannot exceed 100 (API limit)'); } const query = Object.keys(apiParams).length > 0 ? `?${new URLSearchParams(apiParams)}` : ''; if (this.debugMode) { logger.info(`[Logs API] Getting campaign logs for campaign ${campaignId}`); logger.info(`[Logs API] Query parameters:`, apiParams); logger.info(`[Logs API] Final URL: GET /logs/campaigns/${campaignId}${query}`); } return this.makeRequest(`/logs/campaigns/${campaignId}${query}`); } /** * Get campaign logs with intelligent categorization and analysis */ async getCampaignLogsWithAnalysis(campaignId, params) { // Get the logs const logs = await this.getCampaignLogs(campaignId, params); // Analyze the logs const analysis = this.analyzeCampaignLogs(logs.data); return { logs, analysis }; } /** * Analyze campaign logs to provide insights with advanced event sequence analysis */ analyzeCampaignLogs(logData) { const summary = { total_events: logData.length, event_types: {}, time_range: {}, top_events: [] }; const insights = []; const recommendations = []; if (logData.length === 0) { insights.push("No log events found for this campaign"); recommendations.push("Check if the campaign has been sent or if there are permission issues"); return { summary, insights, recommendations }; } // Count event types and track time range let minTime = Infinity; let maxTime = -Infinity; logData.forEach(log => { const eventType = log.type || 'unknown'; summary.event_types[eventType] = (summary.event_types[eventType] || 0) + 1; // Track time range if (log.time) { minTime = Math.min(minTime, log.time); maxTime = Math.max(maxTime, log.time); } }); if (minTime !== Infinity) { summary.time_range.start = minTime; } if (maxTime !== -Infinity) { summary.time_range.end = maxTime; } // Get top events summary.top_events = Object.entries(summary.event_types) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([type]) => type); // Advanced sequence analysis const sequence_analysis = this.performSequenceAnalysis(logData); // Generate insights based on sequence analysis const sequenceInsights = this.generateSequenceInsights(sequence_analysis); insights.push(...sequenceInsights); // Generate recommendations based on sequence analysis const sequenceRecommendations = this.generateSequenceRecommendations(sequence_analysis); recommendations.push(...sequenceRecommendations); // Original basic insights const bounces = summary.event_types['bounce'] || 0; const opens = summary.event_types['open'] || 0; const clicks = summary.event_types['click'] || 0; const delivered = summary.event_types['delivered'] || summary.event_types['sent'] || 0; if (delivered > 0) { const bounceRate = (bounces / delivered) * 100; const openRate = (opens / delivered) * 100; const clickRate = (clicks / delivered) * 100; insights.push(`Delivery: ${delivered} emails delivered`); if (bounceRate > 0) { insights.push(`Bounce rate: ${bounceRate.toFixed(1)}% (${bounces} bounces)`); if (bounceRate > 5) { recommendations.push("High bounce rate detected - consider cleaning your email list"); } } if (openRate > 0) { insights.push(`Open rate: ${openRate.toFixed(1)}% (${opens} opens)`); if (openRate < 15) { recommendations.push("Low open rate - consider improving subject lines"); } } if (clickRate > 0) { insights.push(`Click rate: ${clickRate.toFixed(1)}% (${clicks} clicks)`); if (clickRate < 2) { recommendations.push("Low click rate - consider improving email content and CTAs"); } } } // Time-based insights if (summary.time_range.start && summary.time_range.end) { const duration = summary.time_range.end - summary.time_range.start; const hours = duration / 3600; if (hours > 0) { insights.push(`Campaign activity span: ${hours.toFixed(1)} hours`); } } return { summary, insights, recommendations, sequence_analysis }; } /** * Perform advanced event sequence analysis */ performSequenceAnalysis(logData) { // Group events by email/contact for sequence tracking const emailSequences = new Map(); logData.forEach(log => { const emailKey = log.email || log.contact_id || 'unknown'; if (!emailSequences.has(emailKey)) { emailSequences.set(emailKey, []); } emailSequences.get(emailKey).push(log); }); // Sort sequences by time for each email emailSequences.forEach(sequence => { sequence.sort((a, b) => (a.time || 0) - (b.time || 0)); }); // Calculate funnel metrics const funnel_metrics = { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, unsubscribed: 0 }; // Track unique emails at each stage const uniqueEmails = { sent: new Set(), delivered: new Set(), opened: new Set(), clicked: new Set(), bounced: new Set(), unsubscribed: new Set() }; // Timing analysis data const openTimes = []; const clickTimes = []; const hourlyEngagement = new Array(24).fill(0); // User journey tracking let complete_journey = 0; let opened_not_clicked = 0; let delivered_not_opened = 0; let bounced_immediately = 0; // Process each email sequence emailSequences.forEach((sequence, emailKey) => { const sortedEvents = sequence.sort((a, b) => (a.time || 0) - (b.time || 0)); // Track funnel progression for this email let sent = false, delivered = false, opened = false, clicked = false, bounced = false, unsubscribed = false; let deliveredTime; let openedTime; sortedEvents.forEach(log => { const eventType = log.type; switch (eventType) { case 'sent': case 'submitted': case 'queued': if (!sent) { sent = true; uniqueEmails.sent.add(emailKey); } break; case 'delivered': if (!delivered) { delivered = true; deliveredTime = log.time; uniqueEmails.delivered.add(emailKey); } break; case 'open': if (!opened) { opened = true; openedTime = log.time; uniqueEmails.opened.add(emailKey); // Track time to open if (deliveredTime && log.time) { openTimes.push(log.time - deliveredTime); } // Track hourly engagement if (log.time) { const hour = new Date(log.time * 1000).getHours(); hourlyEngagement[hour]++; } } break; case 'click': if (!clicked) { clicked = true; uniqueEmails.clicked.add(emailKey); // Track time to click if (openedTime && log.time) { clickTimes.push(log.time - openedTime); } else if (deliveredTime && log.time) { clickTimes.push(log.time - deliveredTime); } // Track hourly engagement if (log.time) { const hour = new Date(log.time * 1000).getHours(); hourlyEngagement[hour]++; } } break; case 'bounce': if (!bounced) { bounced = true; uniqueEmails.bounced.add(emailKey); } break; case 'unsubscribe': case 'global_unsubscribe': if (!unsubscribed) { unsubscribed = true; uniqueEmails.unsubscribed.add(emailKey); } break; } }); // Analyze user journey for this email if (sent && delivered && opened && clicked) { complete_journey++; } else if (sent && delivered && opened && !clicked) { opened_not_clicked++; } else if (sent && delivered && !opened) { delivered_not_opened++; } else if (sent && bounced) { bounced_immediately++; } }); // Set funnel metrics from unique counts funnel_metrics.sent = uniqueEmails.sent.size; funnel_metrics.delivered = uniqueEmails.delivered.size; funnel_metrics.opened = uniqueEmails.opened.size; funnel_metrics.clicked = uniqueEmails.clicked.size; funnel_metrics.bounced = uniqueEmails.bounced.size; funnel_metrics.unsubscribed = uniqueEmails.unsubscribed.size; // Calculate conversion rates const conversion_rates = { delivery_rate: funnel_metrics.sent > 0 ? (funnel_metrics.delivered / funnel_metrics.sent) * 100 : 0, open_rate: funnel_metrics.delivered > 0 ? (funnel_metrics.opened / funnel_metrics.delivered) * 100 : 0, click_through_rate: funnel_metrics.delivered > 0 ? (funnel_metrics.clicked / funnel_metrics.delivered) * 100 : 0, click_to_open_rate: funnel_metrics.opened > 0 ? (funnel_metrics.clicked / funnel_metrics.opened) * 100 : 0, bounce_rate: funnel_metrics.sent > 0 ? (funnel_metrics.bounced / funnel_metrics.sent) * 100 : 0, unsubscribe_rate: funnel_metrics.delivered > 0 ? (funnel_metrics.unsubscribed / funnel_metrics.delivered) * 100 : 0 }; // Calculate timing analysis const avg_time_to_open = openTimes.length > 0 ? openTimes.reduce((a, b) => a + b, 0) / openTimes.length : undefined; const avg_time_to_click = clickTimes.length > 0 ? clickTimes.reduce((a, b) => a + b, 0) / clickTimes.length : undefined; // Find peak engagement hour const maxEngagement = Math.max(...hourlyEngagement); const peak_engagement_hour = maxEngagement > 0 ? hourlyEngagement.indexOf(maxEngagement) : undefined; // Determine engagement pattern let engagement_pattern = 'unknown'; if (openTimes.length > 0) { const immediateOpens = openTimes.filter(time => time < 3600).length; // Within 1 hour const immediateRatio = immediateOpens / openTimes.length; if (immediateRatio >= 0.7) { engagement_pattern = 'immediate'; } else if (immediateRatio <= 0.3) { engagement_pattern = 'delayed'; } else { engagement_pattern = 'mixed'; } } // Calculate drop-off analysis const delivery_drop_off = funnel_metrics.sent > 0 ? ((funnel_metrics.sent - funnel_metrics.delivered) / funnel_metrics.sent) * 100 : 0; const open_drop_off = funnel_metrics.delivered > 0 ? ((funnel_metrics.delivered - funnel_metrics.opened) / funnel_metrics.delivered) * 100 : 0; const click_drop_off = funnel_metrics.opened > 0 ? ((funnel_metrics.opened - funnel_metrics.clicked) / funnel_metrics.opened) * 100 : 0; // Identify primary drop-off stage let primary_drop_off_stage = 'delivery'; if (open_drop_off > delivery_drop_off && open_drop_off > click_drop_off) { primary_drop_off_stage = 'opening'; } else if (click_drop_off > delivery_drop_off && click_drop_off > open_drop_off) { primary_drop_off_stage = 'clicking'; } const timing_analysis = { engagement_pattern }; if (avg_time_to_open !== undefined) { timing_analysis.avg_time_to_open = avg_time_to_open; } if (avg_time_to_click !== undefined) { timing_analysis.avg_time_to_click = avg_time_to_click; } if (peak_engagement_hour !== undefined) { timing_analysis.peak_engagement_hour = peak_engagement_hour; } return { funnel_metrics, conversion_rates, timing_analysis, user_journeys: { complete_journey, opened_not_clicked, delivered_not_opened, bounced_immediately }, drop_off_analysis: { delivery_drop_off, open_drop_off, click_drop_off, primary_drop_off_stage } }; } /** * Generate insights based on sequence analysis */ generateSequenceInsights(analysis) { const insights = []; // Funnel insights if (analysis.funnel_metrics.sent > 0) { insights.push(`Email funnel: ${analysis.funnel_metrics.sent} sent → ${analysis.funnel_metrics.delivered} delivered → ${analysis.funnel_metrics.opened} opened → ${analysis.funnel_metrics.clicked} clicked`); } // Timing insights if (analysis.timing_analysis.avg_time_to_open) { const avgOpenHours = (analysis.timing_analysis.avg_time_to_open / 3600).toFixed(1); insights.push(`Average time to open: ${avgOpenHours} hours`); } if (analysis.timing_analysis.avg_time_to_click) { const avgClickMinutes = (analysis.timing_analysis.avg_time_to_click / 60).toFixed(1); insights.push(`Average time from open to click: ${avgClickMinutes} minutes`); } // Engagement pattern insights switch (analysis.timing_analysis.engagement_pattern) { case 'immediate': insights.push('Immediate engagement pattern - recipients open emails quickly'); break; case 'delayed': insights.push('Delayed engagement pattern - recipients take time to open emails'); break; case 'mixed': insights.push('Mixed engagement pattern - varied opening behaviors'); break; } // Peak engagement time if (analysis.timing_analysis.peak_engagement_hour !== undefined) { insights.push(`Peak engagement time: ${analysis.timing_analysis.peak_engagement_hour}:00 hours`); } // User journey insights const total_journeys = analysis.user_journeys.complete_journey + analysis.user_journeys.opened_not_clicked + analysis.user_journeys.delivered_not_opened + analysis.user_journeys.bounced_immediately; if (total_journeys > 0) { const complete_rate = (analysis.user_journeys.complete_journey / total_journeys) * 100; insights.push(`Complete journey rate: ${complete_rate.toFixed(1)}% (sent → delivered → opened → clicked)`); } // Drop-off insights insights.push(`Primary drop-off stage: ${analysis.drop_off_analysis.primary_drop_off_stage} (${analysis.drop_off_analysis[analysis.drop_off_analysis.primary_drop_off_stage + '_drop_off'].toFixed(1)}% loss)`); return insights; } /** * Generate recommendations based on sequence analysis */ generateSequenceRecommendations(analysis) { const recommendations = []; // Delivery recommendations if (analysis.conversion_rates.delivery_rate < 95) { recommendations.push('Improve deliverability - delivery rate is below optimal (95%+)'); } // Drop-off specific recommendations switch (analysis.drop_off_analysis.primary_drop_off_stage) { case 'delivery': recommendations.push('Focus on deliverability: authenticate your domain, clean your list, monitor sender reputation'); break; case 'opening': recommendations.push('Focus on open rates: optimize subject lines, sender name, and send timing'); break; case 'clicking': recommendations.push('Focus on content engagement: improve CTAs, email design, and value proposition'); break; } // Timing-based recommendations if (analysis.timing_analysis.engagement_pattern === 'delayed') { recommendations.push('Consider follow-up emails - many recipients engage with delay'); } else if (analysis.timing_analysis.engagement_pattern === 'immediate') { recommendations.push('Optimize for immediate impact - recipients engage quickly with your content'); } // Peak time recommendations if (analysis.timing_analysis.peak_engagement_hour !== undefined) { const peakHour = analysis.timing_analysis.peak_engagement_hour; recommendations.push(`Consider sending future campaigns around ${peakHour}:00 for maximum engagement`); } // User journey recommendations if (analysis.user_journeys.opened_not_clicked > analysis.user_journeys.complete_journey) { recommendations.push('High open-to-click drop-off - strengthen your call-to-action and content relevance'); } if (analysis.user_journeys.delivered_not_opened > analysis.user_journeys.opened_not_clicked) { recommendations.push('High delivery-to-open drop-off - focus on subject line optimization and sender reputation'); } // Click-to-open rate recommendations if (analysis.conversion_rates.click_to_open_rate < 15) { recommendations.push('Low click-to-open rate - improve email content relevance and CTA effectiveness'); } else if (analysis.conversion_rates.click_to_open_rate > 25) { recommendations.push('Excellent click-to-open rate - content is highly relevant to openers'); } return recommendations; } /** * Debug logs access and test different access patterns */ async debugLogsAccess(campaignId) { const results = { timestamp: new Date().toISOString(), tests: [] }; // Test 1: Try to get campaign logs if ID provided if (campaignId) { try { const logs = await this.getCampaignLogs(campaignId, { per_page: 5 }); results.tests.push({ test: 'campaign-logs', success: true, campaignId, logCount: Array.isArray(logs.data) ? logs.data.length : 0, hasLogs: Array.isArray(logs.data) && logs.data.length > 0 }); } catch (error) { results.tests.push({ test: 'campaign-logs', success: false, campaignId, error: error.message }); } } // Test 2: Try with account scoping try { const accountId = await this.getCurrentAccountId(); if (accountId && campaignId) { const logs = await this.getCampaignLogs(campaignId, { account_id: accountId, per_page: 5 }); results.tests.push({ test: 'account-scoped-logs', success: true, accountId, campaignId, logCount: Array.isArray(logs.data) ? logs.data.length : 0 }); } } catch (error) { results.tests.push({ test: 'account-scoped-logs', success: false, error: error.message }); } return results; } // Additional methods for email logs async getEmailLogs(logType, params) { // Validate log type const validTypes = ['all', 'submitted', 'queued', 'delivered', 'rejected', 'error', 'open', 'click', 'bounce', 'spam', 'unsubscribe', 'global_unsubscribe']; if (!validTypes.includes(logType)) { throw new Error('Invalid log type'); } const queryParams = new URLSearchParams(); queryParams.append('log_type', logType); queryParams.append('page', params?.page || '1'); queryParams.append('per_page', params?.per_page || '50'); queryParams.append('with_count', params?.with_count !== undefined ? params.with_count.toString() : 'false'); // Add account ID const accountId = await this.getCurrentAccountId(); if (accountId) queryParams.append('account_id', accountId.toString()); // Add other parameters if (params?.email_id) queryParams.append('email_id', params.email_id); if (params?.start_time) queryParams.append('start_time', params.start_time.toString()); if (params?.end_time) queryParams.append('end_time', params.end_time.toString()); if (params?.tags) queryParams.append('tags', params.tags); if (params?.providers) queryParams.append('providers', params.providers); if (params?.iso_time) queryParams.append('iso_time', params.iso_time.toString()); return this.makeRequest(`/logs/emails?${queryParams.toString()}`); } async getEmailLogsWithAnalysis(logType, params) { const logs = await this.getEmailLogs(logType, params); const analysis = { total_logs: logs.data?.length || 0, by_type: {}, performance: { delivery_rate: 0, open_rate: 0, click_rate: 0, bounce_rate: 0 }, timeline: {} }; // Count by type if (logs.data) { let delivered = 0, opens = 0, clicks = 0, bounces = 0; logs.data.forEach((log) => { const type = log.type || 'unknown'; analysis.by_type[type] = (analysis.by_type[type] || 0) + 1; if (type === 'delivered') delivered++; if (type === 'open') opens++; if (type === 'click') clicks++; if (type === 'bounce') bounces++; }); const total = delivered + bounces; if (total > 0) { analysis.performance.delivery_rate = delivered / total; analysis.performance.bounce_rate = bounces / total; } if (delivered > 0) { analysis.performance.open_rate = opens / delivered; analysis.performance.click_rate = clicks / delivered; } } return { logs, analysis }; } async getCampaignEngagementLogs(campaignId) { const response = await this.getCampaignLogs(campaignId, { filter: 'type==open;type==click;type==unsubscribe' }); return response.data || []; } async getCampaignBounceAndSpamLogs(campaignId) { const response = await this.getCampaignLogs(campaignId, { filter: 'type==bounce;type==spam' }); return response.data || []; } async getEmailJourney(emailId) { const logs = await this.getEmailLogs('all', { email_id: emailId }); const events = logs.data || []; // Sort events by timestamp events.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); // Calculate journey duration let duration = 0; if (events.length > 1) { duration = (events[events.length - 1].timestamp || 0) - (events[0].timestamp || 0); } // Determine final status let finalStatus = 'pending'; if (events.some((e) => e.type === 'click')) { finalStatus = 'engaged'; } else if (events.some((e) => e.type === 'open')) { finalStatus = 'opened'; } else if (events.some((e) => e.type === 'delivered')) { finalStatus = 'delivered'; } else if (events.some((e) => e.type === 'bounce')) { finalStatus = 'bounced'; } return { email_id: emailId, events, journey_duration: duration, final_status: finalStatus }; } async aggregateCampaignLogsByType(campaignId) { const logs = await this.getCampaignLogs(campaignId, { per_page: 100 }); const aggregated = {}; if (logs.data) { logs.data.forEach((log) => { const type = log.type || 'unknown'; if (!aggregated[type]) { aggregated[type] = { total: 0, unique: 0, contacts: [] }; } aggregated[type].total++; if (log.contact_id && !aggregated[type].contacts.includes(log.contact_id)) { aggregated[type].contacts.push(log.contact_id); aggregated[type].unique = aggregated[type].contacts.length; } }); } return aggregated; } async getClickPatterns(campaignId) { const logs = await this.getCampaignLogs(campaignId, { filter: 'type==click', per_page: 100 }); const clickData = {}; const uniqueClickers = new Set(); let totalClicks = 0; if (logs.data) { logs.data.forEach((log) => { if (log.type === 'click' && log.clickthru_url) { totalClicks++; if (log.contact_id) uniqueClickers.add(log.contact_id); const url = log.clickthru_url; if (!clickData[url]) { clickData[url] = { url, clicks: 0, unique_clicks: 0, clickers: new Set(), first_click: log.timestamp, last_click: log.timestamp }; } clickData[url].clicks++; if (log.contact_id) { clickData[url].clickers.add(log.contact_id); clickData[url].unique_clicks = clickData[url].clickers.size; } if (log.timestamp < clickData[url].first_click) { clickData[url].first_click = log.timestamp; } if (log.timestamp > clickData[url].last_click) { clickData[url].last_click = log.timestamp; } } }); } // Convert to array and clean up const links = Object.values(clickData).map((link) => ({ url: link.url, clicks: link.clicks, unique_clicks: link.unique_clicks, first_click: link.first_click, last_click: link.last_click })); // Sort by clicks descending links.sort((a, b) => b.clicks - a.clicks); return { total_clicks: totalClicks, unique_clickers: uniqueClickers.size, links }; } async *iterateCampaignLogs(campaignId, params) { let cursor = undefined; let hasMore = true; while (hasMore) { const response = await this.getCampaignLogs(campaignId, { ...params, cursor, per_page: params?.per_page || 100 }); if (response.data && response.data.length > 0) { yield response.data; } cursor = response.pagination?.cursor; hasMore = !!cursor; } } async processCampaignLogsInBatches(campaignId, processor, params) { for await (const batch of this.iterateCampaignLogs(campaignId, params)) { await processor(batch); } } /** * Get list logs with filtering and pagination */ async getListLogs(listId, params) { const apiParams = {}; if (params?.page) apiParams.page = params.page; if (params?.per_page) apiParams.per_page = params.per_page; if (params?.with_count !== undefined) apiParams.with_count = params.with_count; if (params?.start_time) apiParams.start_time = params.start_time; if (params?.end_time) apiParams.end_time = params.end_time; if (params?.filter) apiParams.filter = params.filter; if (params?.account_id) apiParams.account_id = params.account_id; const query = Object.keys(apiParams).length > 0 ? `?${new URLSearchParams(apiParams)}` : ''; return this.makeRequest(`/logs/lists/${listId}${query}`); } } //# sourceMappingURL=logs-api.js.map