import type { PostAnalytics, Alert, PostMetrics, } from "../models/types.js"; import { ALERT_THRESHOLDS } from "../models/types.js"; import { mean, deviationsFromMean, percentChange, } from "./stats.js"; /** * Analyze posts for spikes and drops based on standard deviation thresholds. * For each post, checks if its metric value deviates significantly from the mean. * Returns array of alerts sorted by severity (critical first). */ export function detectAlerts( posts: PostAnalytics[], metricKey: keyof PostMetrics = "impressions" ): Alert[] { if (posts.length === 0) return []; const alerts: Alert[] = []; // Extract metric values const values = posts.map((post) => post.metrics[metricKey]); const avg = mean(values); // Check each post for significant deviations for (const post of posts) { const value = post.metrics[metricKey]; const deviations = deviationsFromMean(value, values); // Spike detection if (deviations > ALERT_THRESHOLDS.spike) { alerts.push({ type: "spike", severity: "info", metric: metricKey, message: `Post "${post.title}" has unusually high ${metricKey}: ${value.toLocaleString()} (${deviations.toFixed(1)} std deviations above mean)`, postId: post.id, value, baseline: avg, deviations, }); } // Drop detection if (deviations < ALERT_THRESHOLDS.drop) { alerts.push({ type: "drop", severity: "warning", metric: metricKey, message: `Post "${post.title}" has unusually low ${metricKey}: ${value.toLocaleString()} (${Math.abs(deviations).toFixed(1)} std deviations below mean)`, postId: post.id, value, baseline: avg, deviations, }); } } // Sort by severity: critical > warning > info const severityOrder = { critical: 0, warning: 1, info: 2 }; alerts.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); return alerts; } /** * Compare week-over-week metrics and generate alerts for significant changes. * Uses percentChange and ALERT_THRESHOLDS for weekly drops and spikes. */ export function detectWeeklyAlerts( currentWeekMetrics: { impressions: number; engagementRate: number }, previousWeekMetrics: { impressions: number; engagementRate: number } ): Alert[] { const alerts: Alert[] = []; // Analyze impressions const impressionChange = percentChange( currentWeekMetrics.impressions, previousWeekMetrics.impressions ); if (impressionChange < ALERT_THRESHOLDS.weeklyDropCritical) { alerts.push({ type: "drop", severity: "critical", metric: "impressions", message: `Critical drop in weekly impressions: ${impressionChange.toFixed(1)}% (from ${previousWeekMetrics.impressions.toLocaleString()} to ${currentWeekMetrics.impressions.toLocaleString()})`, value: currentWeekMetrics.impressions, baseline: previousWeekMetrics.impressions, deviations: impressionChange / 10, // Rough conversion to deviations }); } else if (impressionChange < ALERT_THRESHOLDS.weeklyDropWarning) { alerts.push({ type: "drop", severity: "warning", metric: "impressions", message: `Weekly impressions dropped by ${Math.abs(impressionChange).toFixed(1)}%: from ${previousWeekMetrics.impressions.toLocaleString()} to ${currentWeekMetrics.impressions.toLocaleString()}`, value: currentWeekMetrics.impressions, baseline: previousWeekMetrics.impressions, deviations: impressionChange / 10, }); } else if (impressionChange > ALERT_THRESHOLDS.weeklySpikeInfo) { alerts.push({ type: "spike", severity: "info", metric: "impressions", message: `Strong growth in weekly impressions: +${impressionChange.toFixed(1)}% (from ${previousWeekMetrics.impressions.toLocaleString()} to ${currentWeekMetrics.impressions.toLocaleString()})`, value: currentWeekMetrics.impressions, baseline: previousWeekMetrics.impressions, deviations: impressionChange / 10, }); } // Analyze engagement rate const engagementChange = percentChange( currentWeekMetrics.engagementRate, previousWeekMetrics.engagementRate ); if (engagementChange < ALERT_THRESHOLDS.weeklyDropCritical) { alerts.push({ type: "drop", severity: "critical", metric: "engagementRate", message: `Critical drop in weekly engagement rate: ${engagementChange.toFixed(1)}% (from ${previousWeekMetrics.engagementRate.toFixed(2)}% to ${currentWeekMetrics.engagementRate.toFixed(2)}%)`, value: currentWeekMetrics.engagementRate, baseline: previousWeekMetrics.engagementRate, deviations: engagementChange / 10, }); } else if (engagementChange < ALERT_THRESHOLDS.weeklyDropWarning) { alerts.push({ type: "drop", severity: "warning", metric: "engagementRate", message: `Weekly engagement rate dropped by ${Math.abs(engagementChange).toFixed(1)}%: from ${previousWeekMetrics.engagementRate.toFixed(2)}% to ${currentWeekMetrics.engagementRate.toFixed(2)}%`, value: currentWeekMetrics.engagementRate, baseline: previousWeekMetrics.engagementRate, deviations: engagementChange / 10, }); } else if (engagementChange > ALERT_THRESHOLDS.weeklySpikeInfo) { alerts.push({ type: "spike", severity: "info", metric: "engagementRate", message: `Strong growth in weekly engagement rate: +${engagementChange.toFixed(1)}% (from ${previousWeekMetrics.engagementRate.toFixed(2)}% to ${currentWeekMetrics.engagementRate.toFixed(2)}%)`, value: currentWeekMetrics.engagementRate, baseline: previousWeekMetrics.engagementRate, deviations: engagementChange / 10, }); } // Sort by severity: critical > warning > info const severityOrder = { critical: 0, warning: 1, info: 2 }; alerts.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]); return alerts; }