Files
Nas-Notification/server/api/notifications/logs/analytics.get.js

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`
}