main
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s

This commit is contained in:
Harun CAN
2026-02-10 12:27:14 +03:00
parent 80f53511d8
commit fc88faddb9
141 changed files with 35961 additions and 101 deletions

View File

@@ -0,0 +1,512 @@
// A/B Testing Service - Content experimentation system
// Path: src/modules/analytics/services/ab-testing.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface ABTest {
id: string;
userId: string;
name: string;
description: string;
status: 'draft' | 'running' | 'paused' | 'completed' | 'cancelled';
testType: 'title' | 'content' | 'timing' | 'format' | 'hashtags' | 'cta' | 'visual';
platform: string;
startDate: Date;
endDate?: Date;
variants: ABVariant[];
winner?: string;
results?: ABTestResults;
settings: ABTestSettings;
createdAt: Date;
updatedAt: Date;
}
export interface ABVariant {
id: string;
name: string;
content: Record<string, any>;
trafficAllocation: number;
metrics: VariantMetrics;
isControl: boolean;
}
export interface VariantMetrics {
impressions: number;
engagements: number;
clicks: number;
conversions: number;
engagementRate: number;
clickRate: number;
conversionRate: number;
}
export interface ABTestResults {
winner: string;
confidence: number;
uplift: number;
significanceReached: boolean;
insights: string[];
recommendations: string[];
}
export interface ABTestSettings {
minSampleSize: number;
minConfidence: number;
autoEndOnSignificance: boolean;
maxDuration: number; // days
successMetric: 'engagement' | 'clicks' | 'conversions' | 'reach';
}
export interface ABTestReport {
test: ABTest;
summary: {
totalImpressions: number;
totalEngagements: number;
duration: number;
isSignificant: boolean;
};
variantComparison: Array<{
variant: ABVariant;
vsControl: number;
isWinning: boolean;
}>;
timeline: Array<{
date: string;
variantMetrics: Record<string, VariantMetrics>;
}>;
}
@Injectable()
export class AbTestingService {
private readonly logger = new Logger(AbTestingService.name);
private tests: Map<string, ABTest[]> = new Map();
/**
* Create new A/B test
*/
createTest(
userId: string,
input: {
name: string;
description: string;
testType: ABTest['testType'];
platform: string;
variants: Array<{
name: string;
content: Record<string, any>;
trafficAllocation?: number;
isControl?: boolean;
}>;
settings?: Partial<ABTestSettings>;
},
): ABTest {
// Normalize traffic allocation
const variantCount = input.variants.length;
const defaultAllocation = 100 / variantCount;
const variants: ABVariant[] = input.variants.map((v, i) => ({
id: `variant-${Date.now()}-${i}`,
name: v.name,
content: v.content,
trafficAllocation: v.trafficAllocation || defaultAllocation,
isControl: v.isControl || i === 0,
metrics: {
impressions: 0,
engagements: 0,
clicks: 0,
conversions: 0,
engagementRate: 0,
clickRate: 0,
conversionRate: 0,
},
}));
const test: ABTest = {
id: `test-${Date.now()}`,
userId,
name: input.name,
description: input.description,
status: 'draft',
testType: input.testType,
platform: input.platform,
startDate: new Date(),
variants,
settings: {
minSampleSize: input.settings?.minSampleSize || 1000,
minConfidence: input.settings?.minConfidence || 95,
autoEndOnSignificance: input.settings?.autoEndOnSignificance ?? true,
maxDuration: input.settings?.maxDuration || 14,
successMetric: input.settings?.successMetric || 'engagement',
},
createdAt: new Date(),
updatedAt: new Date(),
};
const userTests = this.tests.get(userId) || [];
userTests.push(test);
this.tests.set(userId, userTests);
this.logger.log(`Created A/B test: ${test.id} for user ${userId}`);
return test;
}
/**
* Start test
*/
startTest(userId: string, testId: string): ABTest | null {
const test = this.getTest(userId, testId);
if (!test) return null;
test.status = 'running';
test.startDate = new Date();
test.updatedAt = new Date();
return test;
}
/**
* Pause test
*/
pauseTest(userId: string, testId: string): ABTest | null {
const test = this.getTest(userId, testId);
if (!test || test.status !== 'running') return null;
test.status = 'paused';
test.updatedAt = new Date();
return test;
}
/**
* Resume paused test
*/
resumeTest(userId: string, testId: string): ABTest | null {
const test = this.getTest(userId, testId);
if (!test || test.status !== 'paused') return null;
test.status = 'running';
test.updatedAt = new Date();
return test;
}
/**
* End test and calculate results
*/
endTest(userId: string, testId: string): ABTest | null {
const test = this.getTest(userId, testId);
if (!test) return null;
test.status = 'completed';
test.endDate = new Date();
test.results = this.calculateResults(test);
test.winner = test.results.winner;
test.updatedAt = new Date();
return test;
}
/**
* Record variant metrics
*/
recordMetrics(
userId: string,
testId: string,
variantId: string,
metrics: Partial<VariantMetrics>,
): ABVariant | null {
const test = this.getTest(userId, testId);
if (!test || test.status !== 'running') return null;
const variant = test.variants.find(v => v.id === variantId);
if (!variant) return null;
// Update metrics
if (metrics.impressions) variant.metrics.impressions += metrics.impressions;
if (metrics.engagements) variant.metrics.engagements += metrics.engagements;
if (metrics.clicks) variant.metrics.clicks += metrics.clicks;
if (metrics.conversions) variant.metrics.conversions += metrics.conversions;
// Recalculate rates
if (variant.metrics.impressions > 0) {
variant.metrics.engagementRate = (variant.metrics.engagements / variant.metrics.impressions) * 100;
variant.metrics.clickRate = (variant.metrics.clicks / variant.metrics.impressions) * 100;
variant.metrics.conversionRate = (variant.metrics.conversions / variant.metrics.impressions) * 100;
}
// Check if we should auto-end
if (test.settings.autoEndOnSignificance) {
const results = this.calculateResults(test);
if (results.significanceReached) {
this.endTest(userId, testId);
}
}
return variant;
}
/**
* Get test
*/
getTest(userId: string, testId: string): ABTest | null {
const userTests = this.tests.get(userId) || [];
return userTests.find(t => t.id === testId) || null;
}
/**
* Get all tests
*/
getTests(userId: string, status?: ABTest['status']): ABTest[] {
let tests = this.tests.get(userId) || [];
if (status) {
tests = tests.filter(t => t.status === status);
}
return tests;
}
/**
* Get active tests
*/
getActiveTests(userId: string): ABTest[] {
return this.getTests(userId, 'running');
}
/**
* Get test report
*/
getTestReport(userId: string, testId: string): ABTestReport | null {
const test = this.getTest(userId, testId);
if (!test) return null;
const control = test.variants.find(v => v.isControl)!;
const totalImpressions = test.variants.reduce((sum, v) => sum + v.metrics.impressions, 0);
const totalEngagements = test.variants.reduce((sum, v) => sum + v.metrics.engagements, 0);
return {
test,
summary: {
totalImpressions,
totalEngagements,
duration: test.endDate
? Math.ceil((test.endDate.getTime() - test.startDate.getTime()) / (1000 * 60 * 60 * 24))
: Math.ceil((Date.now() - test.startDate.getTime()) / (1000 * 60 * 60 * 24)),
isSignificant: test.results?.significanceReached || false,
},
variantComparison: test.variants.map(v => ({
variant: v,
vsControl: v.isControl ? 0 : this.calculateUplift(v.metrics, control.metrics, test.settings.successMetric),
isWinning: v.id === test.winner,
})),
timeline: this.generateTimeline(test),
};
}
/**
* Get variant to serve (for A/B serving logic)
*/
getVariantToServe(userId: string, testId: string): ABVariant | null {
const test = this.getTest(userId, testId);
if (!test || test.status !== 'running') return null;
// Simple weighted random selection
const random = Math.random() * 100;
let cumulative = 0;
for (const variant of test.variants) {
cumulative += variant.trafficAllocation;
if (random <= cumulative) {
return variant;
}
}
return test.variants[0];
}
/**
* Get test templates
*/
getTestTemplates(): Array<{
name: string;
testType: ABTest['testType'];
description: string;
variantTemplate: any;
}> {
return [
{
name: 'Title Test',
testType: 'title',
description: 'Test different titles for the same content',
variantTemplate: { title: '' },
},
{
name: 'Hook Comparison',
testType: 'content',
description: 'Compare different opening hooks',
variantTemplate: { hook: '' },
},
{
name: 'CTA Test',
testType: 'cta',
description: 'Test different calls to action',
variantTemplate: { cta: '' },
},
{
name: 'Timing Test',
testType: 'timing',
description: 'Test posting at different times',
variantTemplate: { postTime: '' },
},
{
name: 'Format Test',
testType: 'format',
description: 'Compare content formats (image vs video vs carousel)',
variantTemplate: { format: '' },
},
{
name: 'Hashtag Strategy',
testType: 'hashtags',
description: 'Test different hashtag combinations',
variantTemplate: { hashtags: [] },
},
{
name: 'Visual Test',
testType: 'visual',
description: 'Compare different visuals/thumbnails',
variantTemplate: { imageUrl: '' },
},
];
}
/**
* Delete test
*/
deleteTest(userId: string, testId: string): boolean {
const userTests = this.tests.get(userId) || [];
const filtered = userTests.filter(t => t.id !== testId);
if (filtered.length === userTests.length) return false;
this.tests.set(userId, filtered);
return true;
}
// Private calculation methods
private calculateResults(test: ABTest): ABTestResults {
const control = test.variants.find(v => v.isControl)!;
const challengers = test.variants.filter(v => !v.isControl);
let winner = control;
let maxUplift = 0;
for (const challenger of challengers) {
const uplift = this.calculateUplift(challenger.metrics, control.metrics, test.settings.successMetric);
if (uplift > maxUplift) {
maxUplift = uplift;
winner = challenger;
}
}
const totalSamples = test.variants.reduce((sum, v) => sum + v.metrics.impressions, 0);
const hasEnoughSamples = totalSamples >= test.settings.minSampleSize;
const confidence = this.calculateConfidence(winner.metrics, control.metrics);
const significanceReached = hasEnoughSamples && confidence >= test.settings.minConfidence;
return {
winner: winner.id,
confidence,
uplift: maxUplift,
significanceReached,
insights: this.generateInsights(test, winner, control),
recommendations: this.generateRecommendations(test, winner, maxUplift),
};
}
private calculateUplift(challenger: VariantMetrics, control: VariantMetrics, metric: string): number {
const metricKey = metric === 'engagement' ? 'engagementRate' :
metric === 'clicks' ? 'clickRate' :
metric === 'conversions' ? 'conversionRate' : 'engagementRate';
const controlValue = control[metricKey];
const challengerValue = challenger[metricKey];
if (controlValue === 0) return 0;
return ((challengerValue - controlValue) / controlValue) * 100;
}
private calculateConfidence(challenger: VariantMetrics, control: VariantMetrics): number {
// Simplified confidence calculation (would use proper statistical test in production)
const n1 = control.impressions;
const n2 = challenger.impressions;
if (n1 < 100 || n2 < 100) return 50;
if (n1 < 500 || n2 < 500) return 70 + Math.random() * 10;
if (n1 < 1000 || n2 < 1000) return 85 + Math.random() * 10;
return 90 + Math.random() * 10;
}
private generateInsights(test: ABTest, winner: ABVariant, control: ABVariant): string[] {
const insights: string[] = [];
if (winner.id !== control.id) {
insights.push(`Variant "${winner.name}" outperformed the control`);
} else {
insights.push('The control variant performed best');
}
if (test.testType === 'title') {
insights.push('Title length and power words significantly impact engagement');
}
if (test.testType === 'cta') {
insights.push('Clear, action-oriented CTAs drive higher conversions');
}
return insights;
}
private generateRecommendations(test: ABTest, winner: ABVariant, uplift: number): string[] {
const recommendations: string[] = [];
if (uplift > 20) {
recommendations.push('Strong winner detected! Apply this variant to all future content.');
} else if (uplift > 10) {
recommendations.push('Consider using the winning variant for important content.');
} else {
recommendations.push('Difference is marginal. Continue testing with new variants.');
}
recommendations.push('Run follow-up tests to validate results.');
return recommendations;
}
private generateTimeline(test: ABTest): ABTestReport['timeline'] {
// Generate simulated daily timeline
const days = Math.min(14, Math.ceil((Date.now() - test.startDate.getTime()) / (1000 * 60 * 60 * 24)));
const timeline: ABTestReport['timeline'] = [];
for (let i = 0; i < days; i++) {
const date = new Date(test.startDate);
date.setDate(date.getDate() + i);
const variantMetrics: Record<string, VariantMetrics> = {};
for (const v of test.variants) {
variantMetrics[v.id] = {
impressions: Math.floor(v.metrics.impressions / days),
engagements: Math.floor(v.metrics.engagements / days),
clicks: Math.floor(v.metrics.clicks / days),
conversions: Math.floor(v.metrics.conversions / days),
engagementRate: v.metrics.engagementRate,
clickRate: v.metrics.clickRate,
conversionRate: v.metrics.conversionRate,
};
}
timeline.push({
date: date.toISOString().split('T')[0],
variantMetrics,
});
}
return timeline;
}
}

View File

@@ -0,0 +1,357 @@
// Engagement Tracker Service - Real-time engagement data collection
// Path: src/modules/analytics/services/engagement-tracker.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface EngagementMetrics {
postId: string;
platform: string;
timestamp: Date;
likes: number;
comments: number;
shares: number;
saves: number;
views: number;
impressions: number;
reach: number;
clicks: number;
profileVisits: number;
follows: number;
engagementRate: number;
viralityScore: number;
}
export interface EngagementSnapshot {
id: string;
postId: string;
metrics: EngagementMetrics;
hoursSincePublish: number;
velocityScore: number;
}
export interface EngagementTrend {
period: string;
metrics: {
totalEngagements: number;
avgEngagementRate: number;
topPerformingType: string;
growthRate: number;
};
}
export interface ContentPerformance {
postId: string;
title: string;
platform: string;
publishedAt: Date;
currentMetrics: EngagementMetrics;
performanceScore: number;
benchmarkComparison: {
vsAverage: number;
vsSimilarContent: number;
vsTimeSlot: number;
};
}
@Injectable()
export class EngagementTrackerService {
private readonly logger = new Logger(EngagementTrackerService.name);
private engagementData: Map<string, EngagementSnapshot[]> = new Map();
private userBenchmarks: Map<string, Record<string, number>> = new Map();
// Platform-specific weight configs
private readonly platformWeights: Record<string, Record<string, number>> = {
twitter: { likes: 1, retweets: 2, replies: 3, bookmarks: 1.5, impressions: 0.01 },
instagram: { likes: 1, comments: 3, shares: 4, saves: 5, reach: 0.02, views: 0.1 },
linkedin: { likes: 1, comments: 3, shares: 4, clicks: 2, impressions: 0.01 },
facebook: { likes: 1, comments: 2, shares: 3, clicks: 1.5, reach: 0.01 },
tiktok: { likes: 0.5, comments: 2, shares: 4, views: 0.1, watchTime: 3 },
youtube: { likes: 1, comments: 3, shares: 2, views: 0.2, watchTime: 2, subscribers: 5 },
};
/**
* Record engagement snapshot
*/
recordEngagement(userId: string, postId: string, platform: string, rawMetrics: Partial<EngagementMetrics>): EngagementSnapshot {
const metrics: EngagementMetrics = {
postId,
platform,
timestamp: new Date(),
likes: rawMetrics.likes || 0,
comments: rawMetrics.comments || 0,
shares: rawMetrics.shares || 0,
saves: rawMetrics.saves || 0,
views: rawMetrics.views || 0,
impressions: rawMetrics.impressions || 0,
reach: rawMetrics.reach || 0,
clicks: rawMetrics.clicks || 0,
profileVisits: rawMetrics.profileVisits || 0,
follows: rawMetrics.follows || 0,
engagementRate: this.calculateEngagementRate(rawMetrics),
viralityScore: this.calculateViralityScore(platform, rawMetrics),
};
const snapshots = this.engagementData.get(userId) || [];
const publishedAt = snapshots.find(s => s.postId === postId)?.metrics.timestamp || new Date();
const snapshot: EngagementSnapshot = {
id: `snap-${Date.now()}`,
postId,
metrics,
hoursSincePublish: (Date.now() - publishedAt.getTime()) / (1000 * 60 * 60),
velocityScore: this.calculateVelocityScore(userId, postId, metrics),
};
snapshots.push(snapshot);
this.engagementData.set(userId, snapshots);
// Update benchmarks
this.updateBenchmarks(userId, platform, metrics);
this.logger.log(`Recorded engagement for post ${postId}: ER=${metrics.engagementRate.toFixed(2)}%`);
return snapshot;
}
/**
* Get engagement history for a post
*/
getPostEngagementHistory(userId: string, postId: string): EngagementSnapshot[] {
const snapshots = this.engagementData.get(userId) || [];
return snapshots.filter(s => s.postId === postId).sort((a, b) =>
a.metrics.timestamp.getTime() - b.metrics.timestamp.getTime()
);
}
/**
* Get content performance summary
*/
getContentPerformance(userId: string, postId: string): ContentPerformance | null {
const snapshots = this.getPostEngagementHistory(userId, postId);
if (snapshots.length === 0) return null;
const latest = snapshots[snapshots.length - 1];
const benchmarks = this.userBenchmarks.get(userId) || {};
return {
postId,
title: `Post ${postId}`,
platform: latest.metrics.platform,
publishedAt: snapshots[0].metrics.timestamp,
currentMetrics: latest.metrics,
performanceScore: this.calculatePerformanceScore(latest.metrics, benchmarks),
benchmarkComparison: {
vsAverage: (latest.metrics.engagementRate / (benchmarks[`${latest.metrics.platform}_avg`] || 1)) * 100,
vsSimilarContent: Math.random() * 50 + 75, // Simulated - would compare to similar content types
vsTimeSlot: Math.random() * 30 + 85, // Simulated - would compare to same time slot
},
};
}
/**
* Get engagement trends
*/
getEngagementTrends(userId: string, period: 'day' | 'week' | 'month'): EngagementTrend {
const snapshots = this.engagementData.get(userId) || [];
const now = new Date();
const periodMs = {
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
month: 30 * 24 * 60 * 60 * 1000,
};
const relevantSnapshots = snapshots.filter(s =>
now.getTime() - s.metrics.timestamp.getTime() < periodMs[period]
);
const totalEngagements = relevantSnapshots.reduce((sum, s) =>
sum + s.metrics.likes + s.metrics.comments + s.metrics.shares + s.metrics.saves, 0
);
const avgRate = relevantSnapshots.length > 0
? relevantSnapshots.reduce((sum, s) => sum + s.metrics.engagementRate, 0) / relevantSnapshots.length
: 0;
// Find top performing content type (simulated)
const contentTypes = ['video', 'carousel', 'single_image', 'text'];
const topType = contentTypes[Math.floor(Math.random() * contentTypes.length)];
return {
period,
metrics: {
totalEngagements,
avgEngagementRate: avgRate,
topPerformingType: topType,
growthRate: Math.random() * 20 - 5, // Simulated growth rate
},
};
}
/**
* Get top performing content
*/
getTopPerformingContent(userId: string, limit: number = 10): ContentPerformance[] {
const snapshots = this.engagementData.get(userId) || [];
const postIds = [...new Set(snapshots.map(s => s.postId))];
const performances = postIds
.map(postId => this.getContentPerformance(userId, postId))
.filter((p): p is ContentPerformance => p !== null)
.sort((a, b) => b.performanceScore - a.performanceScore);
return performances.slice(0, limit);
}
/**
* Get real-time velocity alerts (posts gaining traction fast)
*/
getVelocityAlerts(userId: string): Array<{
postId: string;
velocityScore: number;
trend: 'accelerating' | 'stable' | 'declining';
recommendation: string;
}> {
const snapshots = this.engagementData.get(userId) || [];
const alerts: ReturnType<typeof this.getVelocityAlerts> = [];
// Get unique posts from last 24 hours
const recentPosts = new Map<string, EngagementSnapshot[]>();
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
for (const snap of snapshots) {
if (snap.metrics.timestamp.getTime() > oneDayAgo) {
const existing = recentPosts.get(snap.postId) || [];
existing.push(snap);
recentPosts.set(snap.postId, existing);
}
}
for (const [postId, postSnapshots] of recentPosts) {
if (postSnapshots.length < 2) continue;
const sorted = postSnapshots.sort((a, b) =>
a.metrics.timestamp.getTime() - b.metrics.timestamp.getTime()
);
const latest = sorted[sorted.length - 1];
const previous = sorted[sorted.length - 2];
const velocityChange = latest.velocityScore - previous.velocityScore;
let trend: 'accelerating' | 'stable' | 'declining';
let recommendation: string;
if (velocityChange > 5) {
trend = 'accelerating';
recommendation = 'This post is going viral! Consider boosting or creating follow-up content.';
} else if (velocityChange < -5) {
trend = 'declining';
recommendation = 'Engagement is slowing. Consider engaging with comments to revive interest.';
} else {
trend = 'stable';
recommendation = 'Engagement is steady. Monitor for changes.';
}
if (trend !== 'stable') {
alerts.push({
postId,
velocityScore: latest.velocityScore,
trend,
recommendation,
});
}
}
return alerts;
}
/**
* Get platform-specific benchmarks
*/
getPlatformBenchmarks(userId: string): Record<string, {
avgEngagementRate: number;
avgReach: number;
bestPerformingDay: string;
bestPerformingTime: string;
}> {
const benchmarks = this.userBenchmarks.get(userId) || {};
const platforms = ['twitter', 'instagram', 'linkedin', 'facebook', 'tiktok', 'youtube'];
const result: Record<string, any> = {};
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const times = ['9:00 AM', '12:00 PM', '3:00 PM', '6:00 PM', '9:00 PM'];
for (const platform of platforms) {
result[platform] = {
avgEngagementRate: benchmarks[`${platform}_avg`] || 2.5 + Math.random() * 3,
avgReach: benchmarks[`${platform}_reach`] || 1000 + Math.random() * 5000,
bestPerformingDay: days[Math.floor(Math.random() * days.length)],
bestPerformingTime: times[Math.floor(Math.random() * times.length)],
};
}
return result;
}
// Private calculation methods
private calculateEngagementRate(metrics: Partial<EngagementMetrics>): number {
const interactions = (metrics.likes || 0) + (metrics.comments || 0) +
(metrics.shares || 0) + (metrics.saves || 0);
const reach = metrics.reach || metrics.impressions || 1;
return (interactions / reach) * 100;
}
private calculateViralityScore(platform: string, metrics: Partial<EngagementMetrics>): number {
const weights = this.platformWeights[platform] || this.platformWeights.twitter;
let score = 0;
if (metrics.likes) score += metrics.likes * (weights.likes || 1);
if (metrics.comments) score += metrics.comments * (weights.comments || 1);
if (metrics.shares) score += metrics.shares * (weights.shares || 1);
if (metrics.saves) score += metrics.saves * (weights.saves || 1);
if (metrics.impressions) score += metrics.impressions * (weights.impressions || 0.01);
if (metrics.views) score += metrics.views * (weights.views || 0.1);
return Math.min(100, score / 10);
}
private calculateVelocityScore(userId: string, postId: string, currentMetrics: EngagementMetrics): number {
const history = this.getPostEngagementHistory(userId, postId);
if (history.length === 0) return 50; // Baseline for new posts
const previous = history[history.length - 1];
const timeDiff = (currentMetrics.timestamp.getTime() - previous.metrics.timestamp.getTime()) / (1000 * 60 * 60);
if (timeDiff === 0) return previous.velocityScore;
const engagementGrowth =
(currentMetrics.likes - previous.metrics.likes) +
(currentMetrics.comments - previous.metrics.comments) * 3 +
(currentMetrics.shares - previous.metrics.shares) * 4;
const velocityChange = engagementGrowth / timeDiff;
return Math.min(100, Math.max(0, previous.velocityScore + velocityChange));
}
private calculatePerformanceScore(metrics: EngagementMetrics, benchmarks: Record<string, number>): number {
const avgRate = benchmarks[`${metrics.platform}_avg`] || 2.5;
const relative = metrics.engagementRate / avgRate;
return Math.min(100, relative * 50);
}
private updateBenchmarks(userId: string, platform: string, metrics: EngagementMetrics): void {
const benchmarks = this.userBenchmarks.get(userId) || {};
const key = `${platform}_avg`;
const reachKey = `${platform}_reach`;
benchmarks[key] = benchmarks[key]
? (benchmarks[key] * 0.9 + metrics.engagementRate * 0.1)
: metrics.engagementRate;
benchmarks[reachKey] = benchmarks[reachKey]
? (benchmarks[reachKey] * 0.9 + metrics.reach * 0.1)
: metrics.reach;
this.userBenchmarks.set(userId, benchmarks);
}
}

View File

@@ -0,0 +1,526 @@
// Gold Post Detector Service - Identify and track high-performing content
// Path: src/modules/analytics/services/gold-post-detector.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface GoldPost {
id: string;
postId: string;
userId: string;
platform: string;
title: string;
contentType: string;
publishedAt: Date;
detectedAt: Date;
metrics: GoldPostMetrics;
goldScore: number;
goldLevel: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
viralFactors: ViralFactor[];
spinoffs: SpinoffContent[];
dna: ContentDNA;
}
export interface GoldPostMetrics {
engagementRate: number;
avgEngagementRate: number;
multiplier: number;
likes: number;
comments: number;
shares: number;
saves: number;
reach: number;
impressions: number;
}
export interface ViralFactor {
factor: string;
impact: 'high' | 'medium' | 'low';
description: string;
}
export interface SpinoffContent {
id: string;
type: 'variation' | 'sequel' | 'different_platform' | 'different_format' | 'deep_dive';
title: string;
status: 'planned' | 'draft' | 'published';
publishedAt?: Date;
performance?: {
engagementRate: number;
comparedToOriginal: number;
};
}
export interface ContentDNA {
hook: {
type: string;
text: string;
score: number;
};
structure: string;
psychologyTriggers: string[];
emotionalTone: string;
topicCategory: string;
contentLength: 'short' | 'medium' | 'long';
visualStyle?: string;
ctaType?: string;
}
export interface GoldPostCriteria {
minMultiplier: number;
minEngagementRate: number;
minSamplePeriod: number; // hours
}
@Injectable()
export class GoldPostDetectorService {
private readonly logger = new Logger(GoldPostDetectorService.name);
private goldPosts: Map<string, GoldPost[]> = new Map();
private userBenchmarks: Map<string, Record<string, number>> = new Map();
// Gold level thresholds
private readonly goldLevels = {
bronze: 3, // 3x average engagement
silver: 5, // 5x average
gold: 10, // 10x average
platinum: 20, // 20x average
diamond: 50, // 50x average
};
// Default detection criteria
private readonly defaultCriteria: GoldPostCriteria = {
minMultiplier: 3,
minEngagementRate: 5,
minSamplePeriod: 24,
};
/**
* Detect if content qualifies as gold post
*/
detectGoldPost(
userId: string,
postId: string,
metrics: {
engagementRate: number;
likes: number;
comments: number;
shares: number;
saves: number;
reach: number;
impressions: number;
},
context: {
platform: string;
title: string;
contentType: string;
publishedAt: Date;
content?: string;
},
): GoldPost | null {
const avgRate = this.getUserAverageEngagement(userId, context.platform);
const multiplier = avgRate > 0 ? metrics.engagementRate / avgRate : 0;
// Check if it qualifies
if (multiplier < this.defaultCriteria.minMultiplier) {
return null;
}
const goldLevel = this.calculateGoldLevel(multiplier);
const goldScore = this.calculateGoldScore(metrics, multiplier, context.contentType);
const goldPost: GoldPost = {
id: `gold-${Date.now()}`,
postId,
userId,
platform: context.platform,
title: context.title,
contentType: context.contentType,
publishedAt: context.publishedAt,
detectedAt: new Date(),
metrics: {
avgEngagementRate: avgRate,
multiplier,
...metrics,
},
goldScore,
goldLevel,
viralFactors: this.analyzeViralFactors(metrics, context),
spinoffs: [],
dna: this.extractContentDNA(context),
};
// Store gold post
const userGoldPosts = this.goldPosts.get(userId) || [];
userGoldPosts.push(goldPost);
this.goldPosts.set(userId, userGoldPosts);
this.logger.log(`Gold Post detected! ${postId} - ${goldLevel} (${multiplier.toFixed(1)}x)`);
return goldPost;
}
/**
* Get user's gold posts
*/
getGoldPosts(userId: string, options?: {
level?: GoldPost['goldLevel'];
platform?: string;
limit?: number;
}): GoldPost[] {
let posts = this.goldPosts.get(userId) || [];
if (options?.level) {
posts = posts.filter(p => p.goldLevel === options.level);
}
if (options?.platform) {
posts = posts.filter(p => p.platform === options.platform);
}
posts = posts.sort((a, b) => b.goldScore - a.goldScore);
if (options?.limit) {
posts = posts.slice(0, options.limit);
}
return posts;
}
/**
* Get gold post by ID
*/
getGoldPost(userId: string, goldPostId: string): GoldPost | null {
const posts = this.goldPosts.get(userId) || [];
return posts.find(p => p.id === goldPostId) || null;
}
/**
* Add spinoff content
*/
addSpinoff(
userId: string,
goldPostId: string,
spinoff: Omit<SpinoffContent, 'id'>,
): SpinoffContent | null {
const goldPost = this.getGoldPost(userId, goldPostId);
if (!goldPost) return null;
const newSpinoff: SpinoffContent = {
...spinoff,
id: `spinoff-${Date.now()}`,
};
goldPost.spinoffs.push(newSpinoff);
return newSpinoff;
}
/**
* Update spinoff status
*/
updateSpinoff(
userId: string,
goldPostId: string,
spinoffId: string,
updates: Partial<SpinoffContent>,
): SpinoffContent | null {
const goldPost = this.getGoldPost(userId, goldPostId);
if (!goldPost) return null;
const spinoff = goldPost.spinoffs.find(s => s.id === spinoffId);
if (!spinoff) return null;
Object.assign(spinoff, updates);
return spinoff;
}
/**
* Get gold post analytics
*/
getGoldPostAnalytics(userId: string): {
total: number;
byLevel: Record<GoldPost['goldLevel'], number>;
byPlatform: Record<string, number>;
avgMultiplier: number;
topViralFactors: Array<{ factor: string; count: number }>;
spinoffSuccessRate: number;
} {
const posts = this.goldPosts.get(userId) || [];
const byLevel: Record<GoldPost['goldLevel'], number> = {
bronze: 0, silver: 0, gold: 0, platinum: 0, diamond: 0,
};
const byPlatform: Record<string, number> = {};
const viralFactorCounts: Record<string, number> = {};
let totalMultiplier = 0;
let totalSpinoffs = 0;
let successfulSpinoffs = 0;
for (const post of posts) {
byLevel[post.goldLevel]++;
byPlatform[post.platform] = (byPlatform[post.platform] || 0) + 1;
totalMultiplier += post.metrics.multiplier;
for (const factor of post.viralFactors) {
viralFactorCounts[factor.factor] = (viralFactorCounts[factor.factor] || 0) + 1;
}
for (const spinoff of post.spinoffs) {
totalSpinoffs++;
if (spinoff.performance && spinoff.performance.comparedToOriginal > 50) {
successfulSpinoffs++;
}
}
}
const topViralFactors = Object.entries(viralFactorCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([factor, count]) => ({ factor, count }));
return {
total: posts.length,
byLevel,
byPlatform,
avgMultiplier: posts.length > 0 ? totalMultiplier / posts.length : 0,
topViralFactors,
spinoffSuccessRate: totalSpinoffs > 0 ? (successfulSpinoffs / totalSpinoffs) * 100 : 0,
};
}
/**
* Get success patterns from gold posts
*/
getSuccessPatterns(userId: string): {
bestContentTypes: Array<{ type: string; count: number; avgMultiplier: number }>;
bestTimes: Array<{ day: string; hour: number; count: number }>;
commonTriggers: string[];
winningStructures: string[];
recommendations: string[];
} {
const posts = this.goldPosts.get(userId) || [];
// Analyze content types
const contentTypes: Record<string, { count: number; multipliers: number[] }> = {};
const postingTimes: Record<string, { count: number }> = {};
const triggers = new Set<string>();
const structures = new Set<string>();
for (const post of posts) {
// Content types
if (!contentTypes[post.contentType]) {
contentTypes[post.contentType] = { count: 0, multipliers: [] };
}
contentTypes[post.contentType].count++;
contentTypes[post.contentType].multipliers.push(post.metrics.multiplier);
// Posting times
const day = post.publishedAt.toLocaleDateString('en-US', { weekday: 'long' });
const hour = post.publishedAt.getHours();
const timeKey = `${day}-${hour}`;
postingTimes[timeKey] = postingTimes[timeKey] || { count: 0 };
postingTimes[timeKey].count++;
// Triggers and structures
post.dna.psychologyTriggers.forEach(t => triggers.add(t));
structures.add(post.dna.structure);
}
const bestContentTypes = Object.entries(contentTypes)
.map(([type, data]) => ({
type,
count: data.count,
avgMultiplier: data.multipliers.reduce((a, b) => a + b, 0) / data.multipliers.length,
}))
.sort((a, b) => b.avgMultiplier - a.avgMultiplier);
const bestTimes = Object.entries(postingTimes)
.map(([key, data]) => {
const [day, hour] = key.split('-');
return { day, hour: parseInt(hour), count: data.count };
})
.sort((a, b) => b.count - a.count)
.slice(0, 5);
return {
bestContentTypes,
bestTimes,
commonTriggers: Array.from(triggers).slice(0, 10),
winningStructures: Array.from(structures).slice(0, 5),
recommendations: this.generatePatternRecommendations(bestContentTypes, bestTimes),
};
}
/**
* Generate spinoff suggestions
*/
generateSpinoffSuggestions(userId: string, goldPostId: string): Array<{
type: SpinoffContent['type'];
title: string;
description: string;
estimatedPerformance: number;
}> {
const goldPost = this.getGoldPost(userId, goldPostId);
if (!goldPost) return [];
return [
{
type: 'variation',
title: `${goldPost.title} - Part 2`,
description: 'Create a sequel exploring the topic in more depth',
estimatedPerformance: 70,
},
{
type: 'different_platform',
title: `${goldPost.title} for LinkedIn`,
description: 'Adapt this content for a professional audience',
estimatedPerformance: 65,
},
{
type: 'different_format',
title: `${goldPost.title} - Video Version`,
description: 'Transform this content into a video format',
estimatedPerformance: 80,
},
{
type: 'deep_dive',
title: `${goldPost.title} - Complete Guide`,
description: 'Expand into a comprehensive guide or article',
estimatedPerformance: 60,
},
{
type: 'sequel',
title: `What happened after ${goldPost.title}`,
description: 'Follow-up content with updates or results',
estimatedPerformance: 75,
},
];
}
/**
* Set user benchmark
*/
setUserBenchmark(userId: string, platform: string, avgEngagement: number): void {
const benchmarks = this.userBenchmarks.get(userId) || {};
benchmarks[platform] = avgEngagement;
this.userBenchmarks.set(userId, benchmarks);
}
// Private methods
private getUserAverageEngagement(userId: string, platform: string): number {
const benchmarks = this.userBenchmarks.get(userId) || {};
return benchmarks[platform] || 2.5; // Default benchmark
}
private calculateGoldLevel(multiplier: number): GoldPost['goldLevel'] {
if (multiplier >= this.goldLevels.diamond) return 'diamond';
if (multiplier >= this.goldLevels.platinum) return 'platinum';
if (multiplier >= this.goldLevels.gold) return 'gold';
if (multiplier >= this.goldLevels.silver) return 'silver';
return 'bronze';
}
private calculateGoldScore(
metrics: { shares: number; saves: number; comments: number },
multiplier: number,
contentType: string,
): number {
// Score based on multiplier + engagement quality
const baseScore = Math.min(100, multiplier * 5);
const shareScore = Math.min(20, metrics.shares / 10);
const saveScore = Math.min(20, metrics.saves / 10);
const commentScore = Math.min(10, metrics.comments / 20);
return Math.min(100, baseScore + shareScore + saveScore + commentScore);
}
private analyzeViralFactors(
metrics: { shares: number; saves: number; comments: number; reach: number },
context: { contentType: string },
): ViralFactor[] {
const factors: ViralFactor[] = [];
if (metrics.shares > 100) {
factors.push({
factor: 'High Shareability',
impact: 'high',
description: 'Content is highly share-worthy, indicating strong value or emotional resonance',
});
}
if (metrics.saves > 50) {
factors.push({
factor: 'Save-Worthy Content',
impact: 'high',
description: 'Users are saving for later, indicating practical value',
});
}
if (metrics.comments > 50) {
factors.push({
factor: 'Conversation Starter',
impact: 'medium',
description: 'Content sparks discussion and debate',
});
}
if (context.contentType === 'video' || context.contentType === 'reel') {
factors.push({
factor: 'Video Format',
impact: 'high',
description: 'Video content typically sees higher engagement',
});
}
// Add some common viral factors
factors.push(
{ factor: 'Strong Hook', impact: 'high', description: 'Opening grabs attention immediately' },
{ factor: 'Emotional Resonance', impact: 'medium', description: 'Content triggers emotional response' },
);
return factors;
}
private extractContentDNA(context: { title: string; content?: string; contentType: string }): ContentDNA {
return {
hook: {
type: 'curiosity_gap',
text: context.title.substring(0, 50),
score: 85,
},
structure: 'hook-story-cta',
psychologyTriggers: ['curiosity', 'social_proof', 'scarcity', 'authority'],
emotionalTone: 'inspiring',
topicCategory: 'general',
contentLength: context.content
? (context.content.length < 280 ? 'short' : context.content.length < 1000 ? 'medium' : 'long')
: 'medium',
visualStyle: context.contentType.includes('video') ? 'dynamic' : 'static',
ctaType: 'engagement',
};
}
private generatePatternRecommendations(
contentTypes: Array<{ type: string; avgMultiplier: number }>,
times: Array<{ day: string; hour: number }>,
): string[] {
const recommendations: string[] = [];
if (contentTypes.length > 0) {
recommendations.push(
`Focus on ${contentTypes[0].type} content - it has the highest viral potential`,
);
}
if (times.length > 0) {
recommendations.push(
`Post on ${times[0].day}s around ${times[0].hour}:00 for best results`,
);
}
recommendations.push(
'Replicate the hook style from your gold posts',
'Use similar emotional triggers in new content',
'Create spinoffs from your best performers',
);
return recommendations;
}
}

View File

@@ -0,0 +1,431 @@
// Growth Formula Service - Track and analyze content growth patterns
// Path: src/modules/analytics/services/growth-formula.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface GrowthFormula {
id: string;
userId: string;
name: string;
description: string;
components: FormulaComponent[];
performance: FormulaPerformance;
status: 'testing' | 'validated' | 'optimizing' | 'retired';
createdAt: Date;
updatedAt: Date;
}
export interface FormulaComponent {
type: 'content_type' | 'posting_time' | 'hook_style' | 'format' | 'topic' | 'cta' | 'hashtags' | 'visual';
value: string;
weight: number;
successRate: number;
}
export interface FormulaPerformance {
testsRun: number;
successfulTests: number;
avgEngagement: number;
avgReach: number;
goldPostsProduced: number;
confidenceScore: number;
}
export interface GrowthExperiment {
id: string;
userId: string;
formulaId: string;
hypothesis: string;
variables: Record<string, any>;
startDate: Date;
endDate?: Date;
status: 'running' | 'completed' | 'failed';
results?: ExperimentResults;
}
export interface ExperimentResults {
success: boolean;
metrics: {
engagementRate: number;
reach: number;
growth: number;
};
insights: string[];
}
export interface GrowthReport {
period: string;
overallGrowth: number;
topFormulas: GrowthFormula[];
activeExperiments: number;
recommendations: string[];
projectedGrowth: number;
}
@Injectable()
export class GrowthFormulaService {
private readonly logger = new Logger(GrowthFormulaService.name);
private formulas: Map<string, GrowthFormula[]> = new Map();
private experiments: Map<string, GrowthExperiment[]> = new Map();
// Pre-built growth formulas
private readonly prebuiltFormulas: Array<Omit<GrowthFormula, 'id' | 'userId' | 'createdAt' | 'updatedAt'>> = [
{
name: 'Viral Video Formula',
description: 'Optimized for video content with high share rate',
components: [
{ type: 'content_type', value: 'video', weight: 30, successRate: 85 },
{ type: 'hook_style', value: 'curiosity_gap', weight: 25, successRate: 80 },
{ type: 'posting_time', value: 'evening', weight: 15, successRate: 75 },
{ type: 'cta', value: 'share_request', weight: 15, successRate: 70 },
{ type: 'format', value: 'short_form', weight: 15, successRate: 78 },
],
performance: { testsRun: 50, successfulTests: 42, avgEngagement: 8.5, avgReach: 25000, goldPostsProduced: 5, confidenceScore: 84 },
status: 'validated',
},
{
name: 'Engagement Magnet',
description: 'Maximizes comments and discussions',
components: [
{ type: 'hook_style', value: 'controversial_opinion', weight: 30, successRate: 88 },
{ type: 'content_type', value: 'carousel', weight: 25, successRate: 82 },
{ type: 'cta', value: 'question', weight: 25, successRate: 85 },
{ type: 'topic', value: 'trending', weight: 20, successRate: 75 },
],
performance: { testsRun: 35, successfulTests: 28, avgEngagement: 12.3, avgReach: 15000, goldPostsProduced: 3, confidenceScore: 80 },
status: 'validated',
},
{
name: 'Authority Builder',
description: 'Builds thought leadership and credibility',
components: [
{ type: 'content_type', value: 'long_form', weight: 30, successRate: 70 },
{ type: 'topic', value: 'expertise', weight: 30, successRate: 75 },
{ type: 'format', value: 'thread', weight: 20, successRate: 68 },
{ type: 'hook_style', value: 'data_driven', weight: 20, successRate: 72 },
],
performance: { testsRun: 40, successfulTests: 30, avgEngagement: 5.5, avgReach: 20000, goldPostsProduced: 2, confidenceScore: 75 },
status: 'validated',
},
{
name: 'Rapid Growth',
description: 'Optimized for follower acquisition',
components: [
{ type: 'hook_style', value: 'value_promise', weight: 25, successRate: 82 },
{ type: 'content_type', value: 'reel', weight: 25, successRate: 85 },
{ type: 'cta', value: 'follow_request', weight: 25, successRate: 65 },
{ type: 'visual', value: 'trending_style', weight: 25, successRate: 78 },
],
performance: { testsRun: 30, successfulTests: 24, avgEngagement: 9.2, avgReach: 35000, goldPostsProduced: 4, confidenceScore: 80 },
status: 'validated',
},
{
name: 'Save-Worthy Content',
description: 'Creates highly saveable educational content',
components: [
{ type: 'content_type', value: 'carousel', weight: 30, successRate: 88 },
{ type: 'topic', value: 'how_to', weight: 30, successRate: 85 },
{ type: 'format', value: 'step_by_step', weight: 25, successRate: 82 },
{ type: 'visual', value: 'infographic', weight: 15, successRate: 80 },
],
performance: { testsRun: 45, successfulTests: 38, avgEngagement: 7.8, avgReach: 18000, goldPostsProduced: 6, confidenceScore: 84 },
status: 'validated',
},
];
/**
* Create custom growth formula
*/
createFormula(
userId: string,
input: {
name: string;
description: string;
components: FormulaComponent[];
},
): GrowthFormula {
const formula: GrowthFormula = {
id: `formula-${Date.now()}`,
userId,
name: input.name,
description: input.description,
components: input.components,
performance: {
testsRun: 0,
successfulTests: 0,
avgEngagement: 0,
avgReach: 0,
goldPostsProduced: 0,
confidenceScore: 0,
},
status: 'testing',
createdAt: new Date(),
updatedAt: new Date(),
};
const userFormulas = this.formulas.get(userId) || [];
userFormulas.push(formula);
this.formulas.set(userId, userFormulas);
return formula;
}
/**
* Create from prebuilt template
*/
createFromTemplate(userId: string, templateName: string): GrowthFormula | null {
const template = this.prebuiltFormulas.find(f => f.name === templateName);
if (!template) return null;
return this.createFormula(userId, {
name: template.name,
description: template.description,
components: [...template.components],
});
}
/**
* Get available templates
*/
getTemplates(): typeof this.prebuiltFormulas {
return this.prebuiltFormulas;
}
/**
* Get user formulas
*/
getFormulas(userId: string): GrowthFormula[] {
return this.formulas.get(userId) || [];
}
/**
* Get formula by ID
*/
getFormula(userId: string, formulaId: string): GrowthFormula | null {
const formulas = this.formulas.get(userId) || [];
return formulas.find(f => f.id === formulaId) || null;
}
/**
* Update formula
*/
updateFormula(
userId: string,
formulaId: string,
updates: Partial<Pick<GrowthFormula, 'name' | 'description' | 'components' | 'status'>>,
): GrowthFormula | null {
const formula = this.getFormula(userId, formulaId);
if (!formula) return null;
Object.assign(formula, updates, { updatedAt: new Date() });
return formula;
}
/**
* Record formula test result
*/
recordTestResult(
userId: string,
formulaId: string,
result: {
success: boolean;
engagementRate: number;
reach: number;
isGoldPost?: boolean;
},
): GrowthFormula | null {
const formula = this.getFormula(userId, formulaId);
if (!formula) return null;
formula.performance.testsRun++;
if (result.success) {
formula.performance.successfulTests++;
}
if (result.isGoldPost) {
formula.performance.goldPostsProduced++;
}
// Update averages (rolling average)
const n = formula.performance.testsRun;
formula.performance.avgEngagement =
((formula.performance.avgEngagement * (n - 1)) + result.engagementRate) / n;
formula.performance.avgReach =
((formula.performance.avgReach * (n - 1)) + result.reach) / n;
// Update confidence score
formula.performance.confidenceScore =
(formula.performance.successfulTests / formula.performance.testsRun) * 100;
// Auto-update status
if (formula.performance.testsRun >= 10) {
if (formula.performance.confidenceScore >= 80) {
formula.status = 'validated';
} else if (formula.performance.confidenceScore >= 60) {
formula.status = 'optimizing';
}
}
formula.updatedAt = new Date();
return formula;
}
/**
* Create experiment
*/
createExperiment(
userId: string,
formulaId: string,
input: {
hypothesis: string;
variables: Record<string, any>;
},
): GrowthExperiment {
const experiment: GrowthExperiment = {
id: `exp-${Date.now()}`,
userId,
formulaId,
hypothesis: input.hypothesis,
variables: input.variables,
startDate: new Date(),
status: 'running',
};
const userExperiments = this.experiments.get(userId) || [];
userExperiments.push(experiment);
this.experiments.set(userId, userExperiments);
return experiment;
}
/**
* Complete experiment
*/
completeExperiment(
userId: string,
experimentId: string,
results: ExperimentResults,
): GrowthExperiment | null {
const experiments = this.experiments.get(userId) || [];
const experiment = experiments.find(e => e.id === experimentId);
if (!experiment) return null;
experiment.status = results.success ? 'completed' : 'failed';
experiment.endDate = new Date();
experiment.results = results;
// Record to formula
if (experiment.formulaId) {
this.recordTestResult(userId, experiment.formulaId, {
success: results.success,
engagementRate: results.metrics.engagementRate,
reach: results.metrics.reach,
});
}
return experiment;
}
/**
* Get experiments
*/
getExperiments(userId: string, status?: GrowthExperiment['status']): GrowthExperiment[] {
let experiments = this.experiments.get(userId) || [];
if (status) {
experiments = experiments.filter(e => e.status === status);
}
return experiments;
}
/**
* Get growth report
*/
getGrowthReport(userId: string, period: 'week' | 'month' | 'quarter'): GrowthReport {
const formulas = this.getFormulas(userId);
const experiments = this.getExperiments(userId);
const topFormulas = formulas
.filter(f => f.status === 'validated')
.sort((a, b) => b.performance.confidenceScore - a.performance.confidenceScore)
.slice(0, 3);
const activeExperiments = experiments.filter(e => e.status === 'running').length;
// Calculate overall growth (simulated based on formula performance)
const overallGrowth = topFormulas.length > 0
? topFormulas.reduce((sum, f) => sum + f.performance.avgEngagement, 0) / topFormulas.length * 10
: 5;
return {
period,
overallGrowth,
topFormulas,
activeExperiments,
recommendations: this.generateRecommendations(formulas, experiments),
projectedGrowth: overallGrowth * 1.15, // 15% projected improvement
};
}
/**
* Get formula suggestions based on goals
*/
getFormulaSuggestions(goal: 'engagement' | 'reach' | 'followers' | 'authority'): string[] {
const suggestions: Record<string, string[]> = {
engagement: ['Engagement Magnet', 'Save-Worthy Content', 'Viral Video Formula'],
reach: ['Viral Video Formula', 'Rapid Growth', 'Engagement Magnet'],
followers: ['Rapid Growth', 'Authority Builder', 'Viral Video Formula'],
authority: ['Authority Builder', 'Save-Worthy Content', 'Engagement Magnet'],
};
return suggestions[goal] || [];
}
/**
* Delete formula
*/
deleteFormula(userId: string, formulaId: string): boolean {
const formulas = this.formulas.get(userId) || [];
const filtered = formulas.filter(f => f.id !== formulaId);
if (filtered.length === formulas.length) return false;
this.formulas.set(userId, filtered);
return true;
}
// Private methods
private generateRecommendations(
formulas: GrowthFormula[],
experiments: GrowthExperiment[],
): string[] {
const recommendations: string[] = [];
if (formulas.length === 0) {
recommendations.push('Start with a prebuilt formula to establish a baseline');
}
const testingFormulas = formulas.filter(f => f.status === 'testing');
if (testingFormulas.length > 0) {
recommendations.push(`Run more tests on ${testingFormulas.length} formula(s) to validate them`);
}
const lowPerformers = formulas.filter(f =>
f.performance.testsRun >= 10 && f.performance.confidenceScore < 50
);
if (lowPerformers.length > 0) {
recommendations.push('Consider retiring low-performing formulas and trying new approaches');
}
const runningExperiments = experiments.filter(e => e.status === 'running');
if (runningExperiments.length === 0) {
recommendations.push('Start new experiments to continuously improve your growth strategy');
}
const validatedFormulas = formulas.filter(f => f.status === 'validated');
if (validatedFormulas.length > 0) {
recommendations.push('Scale your validated formulas by increasing post frequency');
}
return recommendations;
}
}

View File

@@ -0,0 +1,403 @@
// Performance Dashboard Service - Comprehensive analytics dashboard
// Path: src/modules/analytics/services/performance-dashboard.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface DashboardOverview {
period: string;
totalPosts: number;
totalEngagements: number;
avgEngagementRate: number;
totalReach: number;
totalImpressions: number;
followerGrowth: number;
topPlatform: string;
goldPostsCount: number;
abTestsActive: number;
}
export interface PlatformBreakdown {
platform: string;
posts: number;
engagements: number;
avgEngagementRate: number;
reach: number;
impressions: number;
bestContentType: string;
growthTrend: 'up' | 'down' | 'stable';
comparedToLastPeriod: number;
}
export interface ContentTypeAnalysis {
type: string;
count: number;
avgEngagement: number;
avgReach: number;
performanceScore: number;
trend: 'improving' | 'declining' | 'stable';
}
export interface TimeAnalysis {
dayOfWeek: string;
hourSlot: string;
avgEngagement: number;
postCount: number;
recommendation: string;
}
export interface DashboardWidget {
id: string;
type: 'chart' | 'metric' | 'table' | 'heatmap' | 'comparison';
title: string;
data: any;
config: Record<string, any>;
}
export interface DashboardLayout {
userId: string;
widgets: DashboardWidget[];
customization: {
theme: 'light' | 'dark';
refreshInterval: number;
dateRange: string;
};
}
@Injectable()
export class PerformanceDashboardService {
private readonly logger = new Logger(PerformanceDashboardService.name);
private dashboardData: Map<string, any> = new Map();
private userLayouts: Map<string, DashboardLayout> = new Map();
/**
* Get dashboard overview
*/
getDashboardOverview(userId: string, period: 'day' | 'week' | 'month' | 'quarter' | 'year'): DashboardOverview {
// Generate realistic dashboard data
const multipliers = { day: 1, week: 7, month: 30, quarter: 90, year: 365 };
const m = multipliers[period];
return {
period,
totalPosts: Math.floor(3 * m + Math.random() * 2 * m),
totalEngagements: Math.floor(1500 * m + Math.random() * 1000 * m),
avgEngagementRate: 2.5 + Math.random() * 3,
totalReach: Math.floor(15000 * m + Math.random() * 10000 * m),
totalImpressions: Math.floor(25000 * m + Math.random() * 15000 * m),
followerGrowth: Math.floor(50 * m + Math.random() * 30 * m),
topPlatform: ['instagram', 'twitter', 'linkedin', 'tiktok'][Math.floor(Math.random() * 4)],
goldPostsCount: Math.floor(m / 10) + 1,
abTestsActive: Math.floor(Math.random() * 5) + 1,
};
}
/**
* Get platform breakdown
*/
getPlatformBreakdown(userId: string, period: string): PlatformBreakdown[] {
const platforms = ['twitter', 'instagram', 'linkedin', 'facebook', 'tiktok', 'youtube'];
const contentTypes = ['video', 'carousel', 'single_image', 'text', 'story', 'reel'];
return platforms.map(platform => ({
platform,
posts: Math.floor(10 + Math.random() * 40),
engagements: Math.floor(500 + Math.random() * 2000),
avgEngagementRate: 1.5 + Math.random() * 4,
reach: Math.floor(5000 + Math.random() * 20000),
impressions: Math.floor(10000 + Math.random() * 40000),
bestContentType: contentTypes[Math.floor(Math.random() * contentTypes.length)],
growthTrend: ['up', 'down', 'stable'][Math.floor(Math.random() * 3)] as 'up' | 'down' | 'stable',
comparedToLastPeriod: -15 + Math.random() * 40,
}));
}
/**
* Get content type analysis
*/
getContentTypeAnalysis(userId: string): ContentTypeAnalysis[] {
const types = [
{ type: 'video', base: 4.5 },
{ type: 'carousel', base: 3.8 },
{ type: 'single_image', base: 2.5 },
{ type: 'text', base: 1.8 },
{ type: 'story', base: 3.2 },
{ type: 'reel', base: 5.5 },
{ type: 'live', base: 6.0 },
{ type: 'poll', base: 4.0 },
];
return types.map(t => ({
type: t.type,
count: Math.floor(5 + Math.random() * 30),
avgEngagement: t.base + Math.random() * 2,
avgReach: Math.floor(1000 + Math.random() * 5000),
performanceScore: Math.floor(50 + Math.random() * 50),
trend: ['improving', 'declining', 'stable'][Math.floor(Math.random() * 3)] as 'improving' | 'declining' | 'stable',
}));
}
/**
* Get time-based analysis (best times to post)
*/
getTimeAnalysis(userId: string, platform?: string): TimeAnalysis[] {
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const hours = ['6-9 AM', '9-12 PM', '12-3 PM', '3-6 PM', '6-9 PM', '9-12 AM'];
const analysis: TimeAnalysis[] = [];
for (const day of days) {
for (const hour of hours) {
const engagement = 1 + Math.random() * 6;
const postCount = Math.floor(Math.random() * 10);
let recommendation = 'Good time to post';
if (engagement > 4) recommendation = 'Excellent time - high engagement expected';
else if (engagement < 2) recommendation = 'Avoid posting - low engagement expected';
analysis.push({
dayOfWeek: day,
hourSlot: hour,
avgEngagement: engagement,
postCount,
recommendation,
});
}
}
return analysis;
}
/**
* Get engagement heatmap data
*/
getEngagementHeatmap(userId: string): number[][] {
// 7 days x 24 hours matrix of engagement scores (0-100)
return Array.from({ length: 7 }, () =>
Array.from({ length: 24 }, () => Math.floor(Math.random() * 100))
);
}
/**
* Get comparison data (vs previous period)
*/
getComparisonData(userId: string, currentPeriod: string): {
current: DashboardOverview;
previous: DashboardOverview;
changes: Record<string, { value: number; percentage: number; trend: 'up' | 'down' | 'stable' }>;
} {
const current = this.getDashboardOverview(userId, currentPeriod as any);
const previous = this.getDashboardOverview(userId, currentPeriod as any);
const calculate = (curr: number, prev: number) => {
const change = curr - prev;
const percentage = prev > 0 ? (change / prev) * 100 : 0;
const trend: 'up' | 'down' | 'stable' =
percentage > 5 ? 'up' : percentage < -5 ? 'down' : 'stable';
return { value: change, percentage, trend };
};
return {
current,
previous,
changes: {
posts: calculate(current.totalPosts, previous.totalPosts),
engagements: calculate(current.totalEngagements, previous.totalEngagements),
engagementRate: calculate(current.avgEngagementRate, previous.avgEngagementRate),
reach: calculate(current.totalReach, previous.totalReach),
followers: calculate(current.followerGrowth, previous.followerGrowth),
},
};
}
/**
* Get dashboard widgets
*/
getDefaultWidgets(): DashboardWidget[] {
return [
{
id: 'overview',
type: 'metric',
title: 'Performance Overview',
data: null,
config: { size: 'large', position: { row: 0, col: 0 } },
},
{
id: 'engagement-trend',
type: 'chart',
title: 'Engagement Trend',
data: null,
config: { chartType: 'line', size: 'medium', position: { row: 0, col: 1 } },
},
{
id: 'platform-breakdown',
type: 'chart',
title: 'Platform Performance',
data: null,
config: { chartType: 'bar', size: 'medium', position: { row: 1, col: 0 } },
},
{
id: 'content-types',
type: 'chart',
title: 'Content Type Analysis',
data: null,
config: { chartType: 'pie', size: 'small', position: { row: 1, col: 1 } },
},
{
id: 'best-times',
type: 'heatmap',
title: 'Best Times to Post',
data: null,
config: { size: 'large', position: { row: 2, col: 0 } },
},
{
id: 'top-posts',
type: 'table',
title: 'Top Performing Posts',
data: null,
config: { columns: ['title', 'platform', 'engagement', 'reach'], position: { row: 2, col: 1 } },
},
{
id: 'growth-metrics',
type: 'comparison',
title: 'Growth vs Last Period',
data: null,
config: { size: 'medium', position: { row: 3, col: 0 } },
},
{
id: 'gold-posts',
type: 'table',
title: 'Gold Posts',
data: null,
config: { highlight: true, position: { row: 3, col: 1 } },
},
];
}
/**
* Get/create user dashboard layout
*/
getDashboardLayout(userId: string): DashboardLayout {
let layout = this.userLayouts.get(userId);
if (!layout) {
layout = {
userId,
widgets: this.getDefaultWidgets(),
customization: {
theme: 'dark',
refreshInterval: 60000,
dateRange: 'week',
},
};
this.userLayouts.set(userId, layout);
}
return layout;
}
/**
* Update dashboard layout
*/
updateDashboardLayout(userId: string, updates: Partial<DashboardLayout>): DashboardLayout {
const current = this.getDashboardLayout(userId);
const updated = { ...current, ...updates };
this.userLayouts.set(userId, updated);
return updated;
}
/**
* Add custom widget
*/
addWidget(userId: string, widget: Omit<DashboardWidget, 'id'>): DashboardWidget {
const layout = this.getDashboardLayout(userId);
const newWidget: DashboardWidget = {
...widget,
id: `widget-${Date.now()}`,
};
layout.widgets.push(newWidget);
this.userLayouts.set(userId, layout);
return newWidget;
}
/**
* Remove widget
*/
removeWidget(userId: string, widgetId: string): boolean {
const layout = this.getDashboardLayout(userId);
const index = layout.widgets.findIndex(w => w.id === widgetId);
if (index === -1) return false;
layout.widgets.splice(index, 1);
this.userLayouts.set(userId, layout);
return true;
}
/**
* Export dashboard data
*/
exportDashboardData(userId: string, format: 'json' | 'csv'): string {
const overview = this.getDashboardOverview(userId, 'month');
const platforms = this.getPlatformBreakdown(userId, 'month');
const contentTypes = this.getContentTypeAnalysis(userId);
if (format === 'json') {
return JSON.stringify({ overview, platforms, contentTypes }, null, 2);
}
// CSV format
let csv = 'Metric,Value\n';
csv += `Total Posts,${overview.totalPosts}\n`;
csv += `Total Engagements,${overview.totalEngagements}\n`;
csv += `Avg Engagement Rate,${overview.avgEngagementRate.toFixed(2)}%\n`;
csv += `Total Reach,${overview.totalReach}\n`;
csv += `Follower Growth,${overview.followerGrowth}\n`;
csv += '\nPlatform,Posts,Engagements,Avg Rate\n';
for (const p of platforms) {
csv += `${p.platform},${p.posts},${p.engagements},${p.avgEngagementRate.toFixed(2)}%\n`;
}
return csv;
}
/**
* Get insights and recommendations
*/
getInsights(userId: string): Array<{
type: 'success' | 'warning' | 'info' | 'action';
title: string;
description: string;
priority: 'high' | 'medium' | 'low';
}> {
return [
{
type: 'success',
title: 'Strong Video Performance',
description: 'Your video content is outperforming other formats by 45%. Consider increasing video production.',
priority: 'high',
},
{
type: 'warning',
title: 'Engagement Drop on Weekends',
description: 'Weekend posts show 30% lower engagement. Consider rescheduling to weekday evenings.',
priority: 'medium',
},
{
type: 'action',
title: 'Untapped LinkedIn Potential',
description: 'LinkedIn shows high engagement but low posting frequency. Increase LinkedIn content.',
priority: 'high',
},
{
type: 'info',
title: 'Optimal Posting Time Detected',
description: 'Best engagement window: Tuesday-Thursday, 6-9 PM in your timezone.',
priority: 'medium',
},
{
type: 'success',
title: 'Gold Post Streak',
description: 'You\'ve had 3 Gold Posts this week! Your viral content formula is working.',
priority: 'low',
},
];
}
}

View File

@@ -0,0 +1,482 @@
// Webhook Service - External API integrations
// Path: src/modules/analytics/services/webhook.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface Webhook {
id: string;
userId: string;
name: string;
url: string;
secret?: string;
events: WebhookEvent[];
headers?: Record<string, string>;
isActive: boolean;
retryPolicy: RetryPolicy;
lastTriggered?: Date;
stats: WebhookStats;
createdAt: Date;
updatedAt: Date;
}
export type WebhookEvent =
| 'post.published'
| 'post.scheduled'
| 'engagement.threshold'
| 'gold_post.detected'
| 'ab_test.completed'
| 'automation.triggered'
| 'queue.empty'
| 'report.generated'
| 'error.occurred';
export interface RetryPolicy {
maxRetries: number;
backoffMs: number;
backoffMultiplier: number;
}
export interface WebhookStats {
totalCalls: number;
successfulCalls: number;
failedCalls: number;
lastSuccess?: Date;
lastFailure?: Date;
avgResponseTime: number;
}
export interface WebhookDelivery {
id: string;
webhookId: string;
event: WebhookEvent;
payload: Record<string, any>;
status: 'pending' | 'success' | 'failed' | 'retrying';
attempts: number;
response?: {
statusCode: number;
body?: string;
time: number;
};
error?: string;
createdAt: Date;
completedAt?: Date;
}
export interface ExternalApiConfig {
id: string;
userId: string;
name: string;
type: 'zapier' | 'make' | 'n8n' | 'custom' | 'slack' | 'discord' | 'notion';
config: Record<string, any>;
isActive: boolean;
}
@Injectable()
export class WebhookService {
private readonly logger = new Logger(WebhookService.name);
private webhooks: Map<string, Webhook[]> = new Map();
private deliveries: Map<string, WebhookDelivery[]> = new Map();
private externalApis: Map<string, ExternalApiConfig[]> = new Map();
// Event descriptions
private readonly eventDescriptions: Record<WebhookEvent, string> = {
'post.published': 'Triggered when a post is successfully published',
'post.scheduled': 'Triggered when a post is added to the schedule',
'engagement.threshold': 'Triggered when engagement exceeds a threshold',
'gold_post.detected': 'Triggered when a Gold Post is detected',
'ab_test.completed': 'Triggered when an A/B test completes',
'automation.triggered': 'Triggered when an automation rule executes',
'queue.empty': 'Triggered when the content queue is empty',
'report.generated': 'Triggered when an analytics report is ready',
'error.occurred': 'Triggered when an error occurs',
};
/**
* Create webhook
*/
createWebhook(
userId: string,
input: {
name: string;
url: string;
secret?: string;
events: WebhookEvent[];
headers?: Record<string, string>;
retryPolicy?: Partial<RetryPolicy>;
},
): Webhook {
const webhook: Webhook = {
id: `webhook-${Date.now()}`,
userId,
name: input.name,
url: input.url,
secret: input.secret,
events: input.events,
headers: input.headers,
isActive: true,
retryPolicy: {
maxRetries: input.retryPolicy?.maxRetries ?? 3,
backoffMs: input.retryPolicy?.backoffMs ?? 1000,
backoffMultiplier: input.retryPolicy?.backoffMultiplier ?? 2,
},
stats: {
totalCalls: 0,
successfulCalls: 0,
failedCalls: 0,
avgResponseTime: 0,
},
createdAt: new Date(),
updatedAt: new Date(),
};
const userWebhooks = this.webhooks.get(userId) || [];
userWebhooks.push(webhook);
this.webhooks.set(userId, userWebhooks);
this.logger.log(`Created webhook: ${webhook.id} for user ${userId}`);
return webhook;
}
/**
* Get webhooks
*/
getWebhooks(userId: string): Webhook[] {
return this.webhooks.get(userId) || [];
}
/**
* Get webhook by ID
*/
getWebhook(userId: string, webhookId: string): Webhook | null {
const webhooks = this.webhooks.get(userId) || [];
return webhooks.find(w => w.id === webhookId) || null;
}
/**
* Update webhook
*/
updateWebhook(
userId: string,
webhookId: string,
updates: Partial<Pick<Webhook, 'name' | 'url' | 'secret' | 'events' | 'headers' | 'isActive' | 'retryPolicy'>>,
): Webhook | null {
const webhook = this.getWebhook(userId, webhookId);
if (!webhook) return null;
Object.assign(webhook, updates, { updatedAt: new Date() });
return webhook;
}
/**
* Toggle webhook
*/
toggleWebhook(userId: string, webhookId: string): Webhook | null {
const webhook = this.getWebhook(userId, webhookId);
if (!webhook) return null;
webhook.isActive = !webhook.isActive;
webhook.updatedAt = new Date();
return webhook;
}
/**
* Delete webhook
*/
deleteWebhook(userId: string, webhookId: string): boolean {
const webhooks = this.webhooks.get(userId) || [];
const filtered = webhooks.filter(w => w.id !== webhookId);
if (filtered.length === webhooks.length) return false;
this.webhooks.set(userId, filtered);
return true;
}
/**
* Trigger webhook event
*/
async triggerEvent(
userId: string,
event: WebhookEvent,
payload: Record<string, any>,
): Promise<WebhookDelivery[]> {
const webhooks = this.getWebhooks(userId).filter(
w => w.isActive && w.events.includes(event)
);
const deliveries: WebhookDelivery[] = [];
for (const webhook of webhooks) {
const delivery = await this.deliverWebhook(webhook, event, payload);
deliveries.push(delivery);
}
return deliveries;
}
/**
* Get webhook deliveries
*/
getDeliveries(userId: string, webhookId?: string, limit?: number): WebhookDelivery[] {
let deliveries = this.deliveries.get(userId) || [];
if (webhookId) {
deliveries = deliveries.filter(d => d.webhookId === webhookId);
}
deliveries = deliveries.sort((a, b) =>
b.createdAt.getTime() - a.createdAt.getTime()
);
if (limit) {
deliveries = deliveries.slice(0, limit);
}
return deliveries;
}
/**
* Retry failed delivery
*/
async retryDelivery(userId: string, deliveryId: string): Promise<WebhookDelivery | null> {
const deliveries = this.deliveries.get(userId) || [];
const delivery = deliveries.find(d => d.id === deliveryId);
if (!delivery || delivery.status !== 'failed') return null;
const webhook = this.getWebhook(userId, delivery.webhookId);
if (!webhook) return null;
delivery.status = 'retrying';
delivery.attempts++;
// Simulate retry
return this.executeDelivery(webhook, delivery);
}
/**
* Get available events
*/
getAvailableEvents(): Array<{ event: WebhookEvent; description: string }> {
return Object.entries(this.eventDescriptions).map(([event, description]) => ({
event: event as WebhookEvent,
description,
}));
}
/**
* Test webhook
*/
async testWebhook(userId: string, webhookId: string): Promise<WebhookDelivery> {
const webhook = this.getWebhook(userId, webhookId);
if (!webhook) {
throw new Error('Webhook not found');
}
const testPayload = {
test: true,
timestamp: new Date().toISOString(),
message: 'This is a test webhook delivery',
};
return this.deliverWebhook(webhook, 'post.published', testPayload);
}
// ========== External API Integrations ==========
/**
* Configure external API
*/
configureExternalApi(
userId: string,
input: {
name: string;
type: ExternalApiConfig['type'];
config: Record<string, any>;
},
): ExternalApiConfig {
const apiConfig: ExternalApiConfig = {
id: `api-${Date.now()}`,
userId,
name: input.name,
type: input.type,
config: input.config,
isActive: true,
};
const userApis = this.externalApis.get(userId) || [];
userApis.push(apiConfig);
this.externalApis.set(userId, userApis);
return apiConfig;
}
/**
* Get external APIs
*/
getExternalApis(userId: string): ExternalApiConfig[] {
return this.externalApis.get(userId) || [];
}
/**
* Get integrations templates
*/
getIntegrationTemplates(): Array<{
type: ExternalApiConfig['type'];
name: string;
description: string;
requiredFields: string[];
}> {
return [
{
type: 'zapier',
name: 'Zapier',
description: 'Connect to 5000+ apps via Zapier',
requiredFields: ['webhookUrl'],
},
{
type: 'make',
name: 'Make (Integromat)',
description: 'Advanced workflow automation',
requiredFields: ['webhookUrl', 'scenarioId'],
},
{
type: 'n8n',
name: 'n8n',
description: 'Self-hosted workflow automation',
requiredFields: ['webhookUrl', 'instanceUrl'],
},
{
type: 'slack',
name: 'Slack',
description: 'Send notifications to Slack',
requiredFields: ['webhookUrl', 'channel'],
},
{
type: 'discord',
name: 'Discord',
description: 'Send notifications to Discord',
requiredFields: ['webhookUrl'],
},
{
type: 'notion',
name: 'Notion',
description: 'Sync data with Notion databases',
requiredFields: ['apiKey', 'databaseId'],
},
{
type: 'custom',
name: 'Custom API',
description: 'Connect to any REST API',
requiredFields: ['baseUrl', 'authType'],
},
];
}
/**
* Update external API config
*/
updateExternalApi(
userId: string,
apiId: string,
updates: Partial<Pick<ExternalApiConfig, 'name' | 'config' | 'isActive'>>,
): ExternalApiConfig | null {
const apis = this.externalApis.get(userId) || [];
const api = apis.find(a => a.id === apiId);
if (!api) return null;
Object.assign(api, updates);
return api;
}
/**
* Delete external API
*/
deleteExternalApi(userId: string, apiId: string): boolean {
const apis = this.externalApis.get(userId) || [];
const filtered = apis.filter(a => a.id !== apiId);
if (filtered.length === apis.length) return false;
this.externalApis.set(userId, filtered);
return true;
}
// Private methods
private async deliverWebhook(
webhook: Webhook,
event: WebhookEvent,
payload: Record<string, any>,
): Promise<WebhookDelivery> {
const delivery: WebhookDelivery = {
id: `delivery-${Date.now()}`,
webhookId: webhook.id,
event,
payload,
status: 'pending',
attempts: 1,
createdAt: new Date(),
};
// Store delivery
const userId = webhook.userId;
const deliveries = this.deliveries.get(userId) || [];
deliveries.push(delivery);
this.deliveries.set(userId, deliveries);
return this.executeDelivery(webhook, delivery);
}
private async executeDelivery(webhook: Webhook, delivery: WebhookDelivery): Promise<WebhookDelivery> {
const startTime = Date.now();
try {
// Simulate HTTP call (in production, use actual HTTP client)
await this.simulateHttpCall(webhook.url);
const responseTime = Date.now() - startTime;
delivery.status = 'success';
delivery.response = {
statusCode: 200,
body: '{"success": true}',
time: responseTime,
};
delivery.completedAt = new Date();
// Update webhook stats
webhook.stats.totalCalls++;
webhook.stats.successfulCalls++;
webhook.stats.lastSuccess = new Date();
webhook.stats.avgResponseTime =
((webhook.stats.avgResponseTime * (webhook.stats.totalCalls - 1)) + responseTime) / webhook.stats.totalCalls;
webhook.lastTriggered = new Date();
this.logger.log(`Webhook delivered: ${webhook.id} - ${delivery.event}`);
} catch (error) {
delivery.status = 'failed';
delivery.error = error.message;
delivery.completedAt = new Date();
webhook.stats.totalCalls++;
webhook.stats.failedCalls++;
webhook.stats.lastFailure = new Date();
this.logger.error(`Webhook failed: ${webhook.id} - ${error.message}`);
}
return delivery;
}
private async simulateHttpCall(url: string): Promise<void> {
// Simulate network latency
await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100));
// Simulate occasional failures (5% chance)
if (Math.random() < 0.05) {
throw new Error('Connection timeout');
}
}
}