mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
167 lines
6.1 KiB
TypeScript
167 lines
6.1 KiB
TypeScript
|
|
import pdfkit from 'pdfkit';
|
|
|
|
import { PassThrough } from 'node:stream';
|
|
|
|
import { ProjectModel } from "@schema/project/ProjectSchema";
|
|
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;
|
|
|
|
const resourcePath = process.env.MODE === 'TEST' ? './public/pdf/' : '../public/pdf/';
|
|
|
|
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(resourcePath + 'pdf_fonts/Poppins-Bold.ttf').fontSize(16).fillColor('#ffffff');
|
|
|
|
pdf.text(`Project name: ${data.projectName}`, { align: 'left' }).moveDown(LINE_SPACING);
|
|
pdf.text(`Timeframe name: ${data.snapshotName}`, { align: 'left' }).moveDown(LINE_SPACING);
|
|
|
|
pdf.font(resourcePath + '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(resourcePath + '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(resourcePath + 'pdf_fonts/Poppins-Regular.ttf')
|
|
.fontSize(10)
|
|
.fillColor('#ffffff')
|
|
.text('Created with Litlyx.com', 50, 760, { align: 'center' });
|
|
|
|
pdf.image(resourcePath + 'pdf_images/logo.png', 460, 700, { width: 100 });
|
|
|
|
pdf.end();
|
|
return pdf;
|
|
}
|
|
|
|
export default defineEventHandler(async event => {
|
|
|
|
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: true, requireRange: false });
|
|
if (!data) return;
|
|
|
|
const userData = getRequestUser(event);
|
|
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
|
|
|
|
const project = await ProjectModel.findById(data.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);
|
|
}); |