import pdfkit from 'pdfkit'; import { PassThrough } from 'node:stream'; import { ProjectModel } from "@schema/ProjectSchema"; import { UserSettingsModel } from "@schema/UserSettings"; import { VisitModel } from '@schema/metrics/VisitSchema'; import { EventModel } from '@schema/metrics/EventSchema'; type PDFGenerationData = { projectName: string, snapshotName: string, totalVisits: string, avgVisitsDay: string, totalEvents: string, topDomain: string, topDevice: string, topCountries: string[], topReferrers: string[], avgGrowthText: string, } function formatNumberK(value: string | number, decimals: number = 1) { const num = parseInt(value.toString()); if (num > 1_000_000) return (num / 1_000_000).toFixed(decimals) + ' M'; if (num > 1_000) return (num / 1_000).toFixed(decimals) + ' K'; return num.toFixed(); } const LINE_SPACING = 0.5; function createPdf(data: PDFGenerationData) { const pdf = new pdfkit({ size: 'A4', margins: { top: 50, bottom: 50, left: 50, right: 50 }, }); pdf.fillColor('#ffffff').rect(0, 0, pdf.page.width, pdf.page.height).fill('#000000'); pdf.font('pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor('#ffffff'); pdf.text(`Project name: ${data.projectName}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`Snapshot name: ${data.snapshotName}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.font('pdf_fonts/Poppins-Regular.ttf').fontSize(12).fillColor('#ffffff') pdf.text(`Total visits: ${data.totalVisits}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`Average visits per day: ${data.avgVisitsDay}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`Total events: ${data.totalEvents}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`Top domain: ${data.topDomain}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`Top device: ${data.topDevice}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.text('Top 3 countries:', { align: 'left' }).moveDown(LINE_SPACING); data.topCountries.forEach((country: any) => { pdf.text(`• ${country}`, { align: 'left' }).moveDown(LINE_SPACING); }); pdf.text('Top 3 best acquisition channels (referrers):', { align: 'left' }).moveDown(LINE_SPACING); data.topReferrers.forEach((channel: any) => { pdf.text(`• ${channel}`, { align: 'left' }).moveDown(LINE_SPACING); }); pdf.text('Average growth:', { align: 'left' }).moveDown(LINE_SPACING); pdf.text(`${data.avgGrowthText}`, { align: 'left' }).moveDown(LINE_SPACING); pdf.font('pdf_fonts/Poppins-Italic.ttf') .text('This gives you an idea of the average growth your website is experiencing over time.', { align: 'left' }) .moveDown(LINE_SPACING); pdf.font('pdf_fonts/Poppins-Regular.ttf') .fontSize(10) .fillColor('#ffffff') .text('Created with Litlyx.com', 50, 760, { align: 'center' }); pdf.image('pdf_images/logo.png', 460, 700, { width: 100 }); pdf.end(); return pdf; } export default defineEventHandler(async event => { const userData = getRequestUser(event); if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project'); const project_id = currentActiveProject.active_project_id; const project = await ProjectModel.findById(project_id); if (!project) return setResponseStatus(event, 400, 'Project not found'); const snapshotHeader = getHeader(event, 'x-snapshot-name'); const fromHeader = getHeader(event, 'x-from'); const toHeader = getHeader(event, 'x-to'); const from = fromHeader ? new Date(fromHeader) : new Date(2020, 0); const to = toHeader ? new Date(toHeader) : new Date(3001, 0); const eventsCount = await EventModel.countDocuments({ project_id: project._id, created_at: { $gte: from, $lte: to } }); const visitsCount = await VisitModel.countDocuments({ project_id: project._id, created_at: { $gte: from, $lte: to } }); const avgVisitDay = () => { const days = (Date.now() - (from.getTime())) / 1000 / 60 / 60 / 24; const avg = visitsCount / Math.max(days, 1); return avg; }; const topDevices = await VisitModel.aggregate([ { $match: { project_id: project._id, created_at: { $gte: from, $lte: to } } }, { $group: { _id: "$device", count: { $sum: 1 } } }, { $match: { _id: { $ne: null } } }, { $sort: { count: -1 } }, { $limit: 1 } ]); const topDevice = topDevices?.[0]?._id || 'Not enough data'; const topDomains = await VisitModel.aggregate([ { $match: { project_id: project._id, created_at: { $gte: from, $lte: to } } }, { $group: { _id: "$website", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 1 } ]); const topDomain = topDomains?.[0]?._id || 'Not enough data'; const topCountries = await VisitModel.aggregate([ { $match: { project_id: project._id, created_at: { $gte: from, $lte: to } } }, { $group: { _id: "$country", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 3 } ]); const topReferrers = await VisitModel.aggregate([ { $match: { project_id: project._id, created_at: { $gte: from, $lte: to } } }, { $group: { _id: "$referrer", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 3 } ]); const pdf = createPdf({ projectName: project.name, snapshotName: snapshotHeader || 'NO_NAME', totalVisits: formatNumberK(visitsCount), avgVisitsDay: formatNumberK(avgVisitDay()) + '/day', totalEvents: formatNumberK(eventsCount), avgGrowthText: 'Insufficient Data (Requires at least 2 months of tracking)', topDevice: topDevice, topDomain: topDomain, topCountries: topCountries.map(e => e._id), topReferrers: topReferrers.map(e => e._id) }); const passThrough = new PassThrough(); pdf.pipe(passThrough); await sendStream(event, passThrough); });