Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
492
server/api/notifications/logs/analytics.get.js
Normal file
492
server/api/notifications/logs/analytics.get.js
Normal file
@@ -0,0 +1,492 @@
|
||||
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`
|
||||
}
|
||||
Reference in New Issue
Block a user