492 lines
13 KiB
JavaScript
492 lines
13 KiB
JavaScript
import prisma from '~/server/utils/prisma'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
// Check authentication
|
|
const user = event.context.user;
|
|
if (!user || !user.userID) {
|
|
return {
|
|
statusCode: 401,
|
|
body: { success: false, message: 'Unauthorized' }
|
|
}
|
|
}
|
|
|
|
const query = getQuery(event)
|
|
const period = query.period || '7d' // Default to last 7 days
|
|
const channel = query.channel || 'all'
|
|
|
|
// Calculate date range based on selected period
|
|
const endDate = new Date()
|
|
let startDate = new Date()
|
|
|
|
switch(period) {
|
|
case '1d':
|
|
startDate.setDate(startDate.getDate() - 1)
|
|
break
|
|
case '7d':
|
|
startDate.setDate(startDate.getDate() - 7)
|
|
break
|
|
case '30d':
|
|
startDate.setDate(startDate.getDate() - 30)
|
|
break
|
|
case '90d':
|
|
startDate.setDate(startDate.getDate() - 90)
|
|
break
|
|
case '12m':
|
|
startDate.setMonth(startDate.getMonth() - 12)
|
|
break
|
|
default:
|
|
startDate.setDate(startDate.getDate() - 7)
|
|
}
|
|
|
|
// Channel filter
|
|
const channelFilter = channel !== 'all' ? { channel_type: channel } : {}
|
|
|
|
// Get key metrics
|
|
const totalSent = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const previousPeriodEnd = new Date(startDate)
|
|
const previousPeriodStart = new Date(startDate)
|
|
|
|
// Calculate the same duration for previous period
|
|
switch(period) {
|
|
case '1d':
|
|
previousPeriodStart.setDate(previousPeriodStart.getDate() - 1)
|
|
break
|
|
case '7d':
|
|
previousPeriodStart.setDate(previousPeriodStart.getDate() - 7)
|
|
break
|
|
case '30d':
|
|
previousPeriodStart.setDate(previousPeriodStart.getDate() - 30)
|
|
break
|
|
case '90d':
|
|
previousPeriodStart.setDate(previousPeriodStart.getDate() - 90)
|
|
break
|
|
case '12m':
|
|
previousPeriodStart.setMonth(previousPeriodStart.getMonth() - 12)
|
|
break
|
|
}
|
|
|
|
const previousTotalSent = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: previousPeriodStart,
|
|
lt: startDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const sentChangePercent = previousTotalSent > 0
|
|
? ((totalSent - previousTotalSent) / previousTotalSent * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
// Get success rate
|
|
const totalAttempted = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
action: {
|
|
in: ['Notification Sent', 'Delivery Attempted']
|
|
},
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const successfulDeliveries = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const successRate = totalAttempted > 0
|
|
? ((successfulDeliveries / totalAttempted) * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
const previousSuccessRate = await calculateSuccessRate(
|
|
prisma,
|
|
previousPeriodStart,
|
|
startDate,
|
|
channelFilter
|
|
)
|
|
|
|
const successRateChangePercent = previousSuccessRate > 0
|
|
? ((parseFloat(successRate) - previousSuccessRate) / previousSuccessRate * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
// Get open rate (if tracking available)
|
|
const totalOpened = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Opened',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const openRate = totalSent > 0
|
|
? ((totalOpened / totalSent) * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
const previousOpenRate = await calculateOpenRate(
|
|
prisma,
|
|
previousPeriodStart,
|
|
startDate,
|
|
channelFilter
|
|
)
|
|
|
|
const openRateChangePercent = previousOpenRate > 0
|
|
? ((parseFloat(openRate) - previousOpenRate) / previousOpenRate * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
// Get click rate (if tracking available)
|
|
const totalClicked = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Clicked',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const clickRate = totalSent > 0
|
|
? ((totalClicked / totalSent) * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
const previousClickRate = await calculateClickRate(
|
|
prisma,
|
|
previousPeriodStart,
|
|
startDate,
|
|
channelFilter
|
|
)
|
|
|
|
const clickRateChangePercent = previousClickRate > 0
|
|
? ((parseFloat(clickRate) - previousClickRate) / previousClickRate * 100).toFixed(1)
|
|
: '0.0'
|
|
|
|
// Get channel performance data
|
|
const channelPerformance = await getChannelPerformance(prisma, startDate, endDate)
|
|
|
|
// Get recent notable events
|
|
const recentEvents = await getRecentEvents(prisma)
|
|
|
|
return {
|
|
statusCode: 200,
|
|
body: {
|
|
success: true,
|
|
data: {
|
|
keyMetrics: [
|
|
{
|
|
title: "Total Sent",
|
|
value: totalSent.toLocaleString(),
|
|
icon: "ic:outline-send",
|
|
trend: parseFloat(sentChangePercent) >= 0 ? "up" : "down",
|
|
change: `${parseFloat(sentChangePercent) >= 0 ? '+' : ''}${sentChangePercent}%`
|
|
},
|
|
{
|
|
title: "Success Rate",
|
|
value: `${successRate}%`,
|
|
icon: "ic:outline-check-circle",
|
|
trend: parseFloat(successRateChangePercent) >= 0 ? "up" : "down",
|
|
change: `${parseFloat(successRateChangePercent) >= 0 ? '+' : ''}${successRateChangePercent}%`
|
|
},
|
|
{
|
|
title: "Open Rate",
|
|
value: `${openRate}%`,
|
|
icon: "ic:outline-open-in-new",
|
|
trend: parseFloat(openRateChangePercent) >= 0 ? "up" : "down",
|
|
change: `${parseFloat(openRateChangePercent) >= 0 ? '+' : ''}${openRateChangePercent}%`
|
|
},
|
|
{
|
|
title: "Click Rate",
|
|
value: `${clickRate}%`,
|
|
icon: "ic:outline-touch-app",
|
|
trend: parseFloat(clickRateChangePercent) >= 0 ? "up" : "down",
|
|
change: `${parseFloat(clickRateChangePercent) >= 0 ? '+' : ''}${clickRateChangePercent}%`
|
|
}
|
|
],
|
|
channelPerformance,
|
|
recentEvents
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching analytics data:', error)
|
|
return {
|
|
statusCode: 500,
|
|
body: { success: false, message: 'Failed to fetch analytics data', error: error.message }
|
|
}
|
|
}
|
|
})
|
|
|
|
// Helper functions
|
|
async function calculateSuccessRate(prisma, startDate, endDate, channelFilter) {
|
|
const totalAttempted = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
action: {
|
|
in: ['Notification Sent', 'Delivery Attempted']
|
|
},
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const successfulDeliveries = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
return totalAttempted > 0
|
|
? ((successfulDeliveries / totalAttempted) * 100)
|
|
: 0
|
|
}
|
|
|
|
async function calculateOpenRate(prisma, startDate, endDate, channelFilter) {
|
|
const totalSent = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const totalOpened = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
status: 'Opened',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
return totalSent > 0
|
|
? ((totalOpened / totalSent) * 100)
|
|
: 0
|
|
}
|
|
|
|
async function calculateClickRate(prisma, startDate, endDate, channelFilter) {
|
|
const totalSent = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
status: 'Sent',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
const totalClicked = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lt: endDate
|
|
},
|
|
status: 'Clicked',
|
|
...channelFilter
|
|
}
|
|
})
|
|
|
|
return totalSent > 0
|
|
? ((totalClicked / totalSent) * 100)
|
|
: 0
|
|
}
|
|
|
|
async function getChannelPerformance(prisma, startDate, endDate) {
|
|
// Define the channels we want to analyze
|
|
const channels = ['Email', 'SMS', 'Push Notification', 'Webhook']
|
|
const channelIcons = {
|
|
'Email': 'ic:outline-email',
|
|
'SMS': 'ic:outline-sms',
|
|
'Push Notification': 'ic:outline-notifications',
|
|
'Webhook': 'ic:outline-webhook'
|
|
}
|
|
|
|
const result = []
|
|
|
|
for (const channel of channels) {
|
|
// Get total sent for this channel
|
|
const sent = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Sent',
|
|
channel_type: channel
|
|
}
|
|
})
|
|
|
|
// Get total failed for this channel
|
|
const failed = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Failed',
|
|
channel_type: channel
|
|
}
|
|
})
|
|
|
|
// Get total bounced for this channel
|
|
const bounced = await prisma.notification_logs.count({
|
|
where: {
|
|
created_at: {
|
|
gte: startDate,
|
|
lte: endDate
|
|
},
|
|
status: 'Bounced',
|
|
channel_type: channel
|
|
}
|
|
})
|
|
|
|
// Calculate total attempted (sent + failed + bounced)
|
|
const total = sent + failed + bounced
|
|
|
|
// Calculate success rate
|
|
const successRate = total > 0 ? ((sent / total) * 100).toFixed(1) : '0.0'
|
|
|
|
// Calculate bounce rate
|
|
const bounceRate = total > 0 ? ((bounced / total) * 100).toFixed(1) : '0.0'
|
|
|
|
// Calculate failure rate
|
|
const failureRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0'
|
|
|
|
result.push({
|
|
name: channel,
|
|
icon: channelIcons[channel] || 'ic:outline-message',
|
|
sent: sent.toString(),
|
|
failed: failed.toString(),
|
|
bounced: bounced.toString(),
|
|
total: total.toString(),
|
|
successRate,
|
|
bounceRate,
|
|
failureRate
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Function to get recent notable events
|
|
async function getRecentEvents(prisma) {
|
|
// Get last 24 hours
|
|
const now = new Date()
|
|
const oneDayAgo = new Date(now)
|
|
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
|
|
|
// Find recent logs with interesting events
|
|
const recentLogs = await prisma.notification_logs.findMany({
|
|
where: {
|
|
created_at: {
|
|
gte: oneDayAgo,
|
|
lte: now
|
|
},
|
|
OR: [
|
|
{ status: 'Failed' },
|
|
{ status: 'Bounced' },
|
|
{
|
|
AND: [
|
|
{ status: 'Sent' },
|
|
{
|
|
details: {
|
|
contains: 'batch'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{ status: 'Opened' }
|
|
]
|
|
},
|
|
orderBy: {
|
|
created_at: 'desc'
|
|
},
|
|
take: 5
|
|
})
|
|
|
|
// Transform logs into notable events
|
|
return recentLogs.map(log => {
|
|
let type = 'info'
|
|
let title = log.action
|
|
let description = log.details || ''
|
|
let value = ''
|
|
let time = formatTimeAgo(log.created_at)
|
|
|
|
if (log.status === 'Failed') {
|
|
type = 'error'
|
|
title = 'Delivery Failure'
|
|
description = log.error_message || log.details || ''
|
|
value = log.channel_type || ''
|
|
} else if (log.status === 'Bounced') {
|
|
type = 'warning'
|
|
title = 'Delivery Bounced'
|
|
value = log.channel_type || ''
|
|
} else if (log.status === 'Opened') {
|
|
type = 'success'
|
|
title = 'Notification Opened'
|
|
value = 'User Engagement'
|
|
} else if (log.status === 'Sent' && log.details?.includes('batch')) {
|
|
type = 'info'
|
|
title = 'Batch Delivery'
|
|
// Try to extract batch size from details
|
|
const match = log.details?.match(/(\d+)\s*notifications?/i)
|
|
value = match ? `${match[1]} sent` : ''
|
|
}
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
value,
|
|
time,
|
|
type
|
|
}
|
|
})
|
|
}
|
|
|
|
// Format time ago
|
|
function formatTimeAgo(timestamp) {
|
|
const now = new Date()
|
|
const time = new Date(timestamp)
|
|
const diffInMinutes = Math.floor((now - time) / (1000 * 60))
|
|
|
|
if (diffInMinutes < 1) return "Just now"
|
|
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`
|
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`
|
|
return `${Math.floor(diffInMinutes / 1440)} days ago`
|
|
}
|