mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
865 lines
32 KiB
TypeScript
865 lines
32 KiB
TypeScript
import { ProjectModel, TProject } from "~/shared/schema/project/ProjectSchema";
|
|
import puppeteer from 'puppeteer'
|
|
import { PDFDocument } from 'pdf-lib';
|
|
import fs from 'fs';
|
|
import path from "path";
|
|
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
|
|
import { formatNumberK, formatTime } from "~/utils/numberFormatter";
|
|
import { visitController } from "~/server/controllers/VisitController";
|
|
import { durationController } from "~/server/controllers/DurationController";
|
|
import { bouncingController } from "~/server/controllers/BouncingController";
|
|
import { AiService } from "~/server/services/ai/AiService";
|
|
import referrers from "../data/referrers";
|
|
|
|
|
|
type BaseOptions = {
|
|
target: TProject,
|
|
resourcesPath: string,
|
|
domain: string,
|
|
from: Date,
|
|
to: Date,
|
|
customLogo: string
|
|
}
|
|
|
|
function getDistribution(array: any[], key: string, current: number) {
|
|
const count = array.reduce((a, e) => a + e[key], 0);
|
|
return (100 / count * array[current][key]).toFixed(2);
|
|
}
|
|
|
|
export async function getPage1(options: BaseOptions) {
|
|
|
|
const page1_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page1.html'), 'utf-8');
|
|
const page1 = page1_raw
|
|
.replace('%PROJECT_NAME%', options.target.name)
|
|
.replace('%DOMAIN%', options.domain)
|
|
.replace('%FROM%', options.from.toLocaleString())
|
|
.replace('%TO%', options.to.toLocaleString())
|
|
.replace('%LOGO%', options.customLogo)
|
|
return page1;
|
|
}
|
|
|
|
export async function getPage2(options: BaseOptions) {
|
|
const page2_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page2.html'), 'utf-8');
|
|
|
|
const [visit_count, session_count, pages, countries, devices, referrers, session_duration, bouncing_rate] = await Promise.all([
|
|
VisitModel.countDocuments({
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: null,
|
|
uniqueSessions: { $addToSet: "$session" }
|
|
}
|
|
},
|
|
{
|
|
$project: {
|
|
_id: 0,
|
|
uniqueSessionCount: { $size: "$uniqueSessions" }
|
|
}
|
|
}
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
durationController.executeDynamic({
|
|
project_id: options.target._id.toString(),
|
|
from: options.from.getTime(), to: options.to.getTime(),
|
|
slice: 'day', domain: options.domain
|
|
}),
|
|
await bouncingController.executeDynamic({
|
|
project_id: options.target._id.toString(),
|
|
from: options.from.getTime(), to: options.to.getTime(),
|
|
slice: 'day', domain: options.domain
|
|
})
|
|
]);
|
|
|
|
|
|
|
|
const ai = AiService.init();
|
|
|
|
|
|
const data: any[] = [
|
|
`- Visits: ${visit_count}`,
|
|
`- Sessions: ${session_count[0].uniqueSessionCount}`,
|
|
`- Most viewed pages: ${JSON.stringify(pages)}`,
|
|
`- Most viewed from countries: ${JSON.stringify(countries)}`,
|
|
`- Most viewed from devices: ${JSON.stringify(devices)}`,
|
|
`- Date of analytics: ${options.from.toISOString()} to ${options.to.toISOString()}`,
|
|
`- Domain of analytics: ${options.domain}`,
|
|
]
|
|
|
|
const aiResponse = await ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 600 characters):\n${data.join('\n')}`
|
|
})
|
|
|
|
|
|
|
|
const page2 = page2_raw
|
|
.replace('%VISITS%', formatNumberK(visit_count ?? 0, 1))
|
|
.replace('%SESSIONS%', formatNumberK(session_count[0].uniqueSessionCount ?? 0, 1))
|
|
.replace('%BOUNCING_RATE%', (bouncing_rate.data.reduce((a, e) => a + e.count, 0) / bouncing_rate.data.length).toFixed(2) + '%')
|
|
.replace('%SESSION_DURATION%', formatTime(session_duration.data.reduce((a, e) => a + e.count, 0) * 1000))
|
|
|
|
.replace('%PAGE_0%', pages[0]?._id)
|
|
.replace('%PAGE_1%', pages[1]?._id)
|
|
.replace('%PAGE_2%', pages[2]?._id)
|
|
|
|
.replace('%COUNTRY_0%', countries[0]?._id)
|
|
.replace('%COUNTRY_1%', countries[1]?._id)
|
|
.replace('%COUNTRY_2%', countries[2]?._id)
|
|
|
|
.replace('%DEVICE_0%', devices[0]?._id)
|
|
.replace('%DEVICE_1%', devices[1]?._id)
|
|
.replace('%DEVICE_2%', devices[2]?._id)
|
|
|
|
.replace('%REFERRER_0%', referrers[0]?._id)
|
|
.replace('%REFERRER_1%', referrers[1]?._id)
|
|
.replace('%REFERRER_2%', referrers[2]?._id)
|
|
|
|
.replace('%AI_0%', aiResponse.output_text)
|
|
|
|
.replace('%LOGO%', options.customLogo)
|
|
|
|
return page2;
|
|
}
|
|
|
|
|
|
export async function getPage3(options: BaseOptions) {
|
|
|
|
const page3_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page3.html'), 'utf-8');
|
|
|
|
const [visits, sessions, devices, visits_timeline] = await Promise.all([
|
|
VisitModel.countDocuments({
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$session" } },
|
|
{ $count: "count" }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: {
|
|
$gte: options.from,
|
|
$lte: options.to
|
|
},
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$device", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
visitController.executeDynamic({
|
|
project_id: options.target._id.toString(),
|
|
from: options.from.getTime(), to: options.to.getTime(),
|
|
slice: 'day', domain: options.domain
|
|
})
|
|
]);
|
|
|
|
|
|
const ai = AiService.init();
|
|
|
|
const [ai0, ai1] = await Promise.all([
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- Visits: ${JSON.stringify(visits)}\n- Sessions: ${JSON.stringify(sessions)}`
|
|
}),
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 500 characters):\n- Visits: ${JSON.stringify(visits_timeline)}`
|
|
})
|
|
])
|
|
|
|
const page3 = page3_raw
|
|
.replace('%VISITS%', visits.toString())
|
|
.replace('%SESSIONS%', sessions[0].count)
|
|
.replace('%DEVICE_0%', `${devices[0]._id} - ${devices[0].count}`)
|
|
.replace('%DEVICE_1%', `${devices[1]._id} - ${devices[1].count}`)
|
|
.replace('%DEVICE_2%', `${devices[2]._id} - ${devices[2].count}`)
|
|
|
|
.replace('VAR_LABELS_TRAFFIC', `[${visits_timeline.data.map(e => `"${e.count}"`).join(',')}]`)
|
|
.replace('VAR_DATA_TRAFFIC', `[${visits_timeline.data.map(e => e.count).join(',')}]`)
|
|
|
|
.replace('VAR_LABELS_PIE', `[${devices.map(e => `"${e._id}"`).join(',')}]`)
|
|
.replace('VAR_DATA_PIE', `[${devices.map(e => e.count).join(',')}]`)
|
|
|
|
.replace('%AI_0%', ai0.output_text)
|
|
.replace('%AI_1%', ai1.output_text)
|
|
|
|
.replace('%LOGO%', options.customLogo)
|
|
|
|
return page3;
|
|
}
|
|
|
|
export async function getPage4(options: BaseOptions) {
|
|
|
|
const page4_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page4.html'), 'utf-8');
|
|
|
|
const [referrers, utm_sources, utm_campaign, utm_medium] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$referrer", count: { $sum: 1 } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain,
|
|
utm_source: { $ne: null }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$utm_source',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain,
|
|
utm_campaign: { $ne: null }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$utm_campaign',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain,
|
|
utm_medium: { $ne: null }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$utm_medium',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
])
|
|
]);
|
|
|
|
const ai = AiService.init();
|
|
|
|
const [ai0, ai1, ai2, ai3] = await Promise.all([
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- Referrers: ${JSON.stringify(referrers)}`
|
|
}),
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- utm_sources: ${JSON.stringify(utm_sources)}`
|
|
}),
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- utm_campaign: ${JSON.stringify(utm_campaign)}`
|
|
}),
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- utm_medium: ${JSON.stringify(utm_medium)}`
|
|
})
|
|
])
|
|
|
|
|
|
const page4 = page4_raw
|
|
.replace('%REFERRER_0%', `${referrers[0]._id} - ${referrers[0].count}`)
|
|
.replace('%REFERRER_1%', `${referrers[1]._id} - ${referrers[1].count}`)
|
|
.replace('%REFERRER_2%', `${referrers[2]._id} - ${referrers[2].count}`)
|
|
.replace('%REFERRER_3%', `${referrers[3]._id} - ${referrers[3].count}`)
|
|
.replace('%REFERRER_4%', `${referrers[4]._id} - ${referrers[4].count}`)
|
|
|
|
.replace('%UTMSOURCE_0%', `${utm_sources[0]?._id} -> ${utm_sources[0]?.count}`)
|
|
.replace('%UTMSOURCE_1%', `${utm_sources[1]?._id} -> ${utm_sources[1]?.count}`)
|
|
.replace('%UTMSOURCE_2%', `${utm_sources[2]?._id} -> ${utm_sources[2]?.count}`)
|
|
|
|
.replace('%UTMCAMPAIGN_0%', `${utm_campaign[0]?._id} -> ${utm_campaign[0]?.count}`)
|
|
.replace('%UTMCAMPAIGN_1%', `${utm_campaign[1]?._id} -> ${utm_campaign[1]?.count}`)
|
|
.replace('%UTMCAMPAIGN_2%', `${utm_campaign[2]?._id} -> ${utm_campaign[2]?.count}`)
|
|
|
|
.replace('%UTMMEDIUM_0%', `${utm_medium[0]?._id} -> ${utm_medium[0]?.count}`)
|
|
.replace('%UTMMEDIUM_1%', `${utm_medium[1]?._id} -> ${utm_medium[1]?.count}`)
|
|
.replace('%UTMMEDIUM_2%', `${utm_medium[2]?._id} -> ${utm_medium[2]?.count}`)
|
|
|
|
.replace('%AI_0%', ai0.output_text)
|
|
.replace('%AI_1%', ai1.output_text)
|
|
.replace('%AI_2%', ai2.output_text)
|
|
.replace('%AI_3%', ai3.output_text)
|
|
|
|
|
|
.replace('%LOGO%', options.customLogo)
|
|
|
|
return page4;
|
|
}
|
|
|
|
export async function getPage5(options: BaseOptions) {
|
|
|
|
const page5_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page5.html'), 'utf-8');
|
|
|
|
const [referrers, utm_term, utm_content] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$referrer", count: { $sum: 1 } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain,
|
|
utm_term: { $ne: null }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$utm_term',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain,
|
|
utm_content: { $ne: null }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: '$utm_content',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 3 }
|
|
]),
|
|
]);
|
|
|
|
const ai = AiService.init();
|
|
|
|
const [ai0, ai1] = await Promise.all([
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- utm_term: ${JSON.stringify(utm_term)}`
|
|
}),
|
|
ai.responses.create({
|
|
model: 'gpt-5-nano',
|
|
input: `Generate an insight of the current website analytics data (max 220 characters):\n- utm_content: ${JSON.stringify(utm_content)}`
|
|
}),
|
|
])
|
|
|
|
|
|
let page5 = page5_raw
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
page5 = page5.replace(`%REFERRER_${i}_NAME%`, `${referrers[i]?._id}`)
|
|
.replace(`%REFERRER_${i}_COUNT%`, `${referrers[i]?.count}`)
|
|
.replace(`%REFERRER_${i}_PERCENT%`, `${getDistribution(referrers, 'count', i)} %`)
|
|
}
|
|
|
|
page5 = page5.replace(`%UTMTERM_0%`, `${utm_term[0]?._id} → ${utm_term[0]?.count}`)
|
|
page5 = page5.replace(`%UTMTERM_1%`, `${utm_term[1]?._id} → ${utm_term[1]?.count}`)
|
|
page5 = page5.replace(`%UTMTERM_2%`, `${utm_term[2]?._id} → ${utm_term[2]?.count}`)
|
|
page5 = page5.replace(`%UTMCONTENT_0%`, `${utm_content[0]?._id} → ${utm_content[0]?.count}`)
|
|
page5 = page5.replace(`%UTMCONTENT_1%`, `${utm_content[1]?._id} → ${utm_content[1]?.count}`)
|
|
page5 = page5.replace(`%UTMCONTENT_2%`, `${utm_content[2]?._id} → ${utm_content[2]?.count}`)
|
|
|
|
page5 = page5.replace(`%AI_0%`, ai0.output_text)
|
|
page5 = page5.replace(`%AI_1%`, ai1.output_text)
|
|
|
|
return page5;
|
|
}
|
|
|
|
export async function getPage6(options: BaseOptions) {
|
|
const page6_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page6.html'), 'utf-8');
|
|
|
|
const [pages, entry_pages, exit_pages] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$page", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $sort: { session: 1, created_at: 1 } },
|
|
{ $group: { _id: "$session", entryPage: { $first: "$page" } } },
|
|
{ $group: { _id: "$entryPage", count: { $sum: 1 } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $sort: { session: 1, created_at: 1 } },
|
|
{ $group: { _id: "$session", exitPage: { $last: "$page" } } },
|
|
{ $group: { _id: "$exitPage", count: { $sum: 1 } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
])
|
|
])
|
|
|
|
const page6 = page6_raw
|
|
.replace('%PAGE_0%', `${pages[0]?._id} → ${pages[0]?.count}`)
|
|
.replace('%PAGE_1%', `${pages[1]?._id} → ${pages[1]?.count}`)
|
|
.replace('%PAGE_2%', `${pages[2]?._id} → ${pages[2]?.count}`)
|
|
.replace('%PAGE_3%', `${pages[3]?._id} → ${pages[3]?.count}`)
|
|
.replace('%PAGE_4%', `${pages[4]?._id} → ${pages[4]?.count}`)
|
|
|
|
.replace('%ENTRY_PAGE_0%', `${entry_pages[0]?._id} → ${entry_pages[0]?.count}`)
|
|
.replace('%ENTRY_PAGE_1%', `${entry_pages[1]?._id} → ${entry_pages[1]?.count}`)
|
|
.replace('%ENTRY_PAGE_2%', `${entry_pages[2]?._id} → ${entry_pages[2]?.count}`)
|
|
.replace('%ENTRY_PAGE_3%', `${entry_pages[3]?._id} → ${entry_pages[3]?.count}`)
|
|
.replace('%ENTRY_PAGE_4%', `${entry_pages[4]?._id} → ${entry_pages[4]?.count}`)
|
|
|
|
.replace('%EXIT_PAGE_0%', `${exit_pages[0]?._id} → ${exit_pages[0]?.count}`)
|
|
.replace('%EXIT_PAGE_1%', `${exit_pages[1]?._id} → ${exit_pages[1]?.count}`)
|
|
.replace('%EXIT_PAGE_2%', `${exit_pages[2]?._id} → ${exit_pages[2]?.count}`)
|
|
.replace('%EXIT_PAGE_3%', `${exit_pages[3]?._id} → ${exit_pages[3]?.count}`)
|
|
.replace('%EXIT_PAGE_4%', `${exit_pages[4]?._id} → ${exit_pages[4]?.count}`)
|
|
|
|
return page6;
|
|
}
|
|
|
|
export async function getPage7(options: BaseOptions) {
|
|
const page7_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page7.html'), 'utf-8');
|
|
|
|
|
|
const [pages, sessions] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{
|
|
$setWindowFields: {
|
|
partitionBy: "$session",
|
|
sortBy: { created_at: 1 },
|
|
output: {
|
|
nextCreatedAt: { $shift: { output: "$created_at", by: 1 } },
|
|
nextPage: { $shift: { output: "$page", by: 1 } }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
$project: {
|
|
page: 1,
|
|
created_at: 1,
|
|
nextCreatedAt: 1,
|
|
durationMs: {
|
|
$cond: [
|
|
{ $ne: ["$nextCreatedAt", null] },
|
|
{ $subtract: ["$nextCreatedAt", "$created_at"] },
|
|
null
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
$match: {
|
|
durationMs: { $ne: null, $gt: 0, $lte: 1000 * 60 * 60 * 1 }
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: "$page",
|
|
count: { $sum: 1 },
|
|
avgMs: { $avg: "$durationMs" }
|
|
}
|
|
},
|
|
{
|
|
$project: {
|
|
_id: 1,
|
|
count: 1,
|
|
avgSeconds: { $round: [{ $divide: ["$avgMs", 1000] }, 0] },
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 8 },
|
|
]),
|
|
durationController.executeDynamic({
|
|
project_id: options.target._id.toString(),
|
|
from: options.from.getTime(), to: options.to.getTime(),
|
|
slice: 'day', domain: options.domain
|
|
})
|
|
])
|
|
|
|
let page7 = page7_raw
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
page7 = page7
|
|
.replace(`%ITEM_${i}_A%`, `${pages[i]?._id}`)
|
|
.replace(`%ITEM_${i}_B%`, `${pages[i]?.count}`)
|
|
.replace(`%ITEM_${i}_C%`, `${formatTime(pages[i]?.avgSeconds * 1000)}`)
|
|
.replace(`%ITEM_${i}_D%`, `${getDistribution(pages, 'count', i)} %`)
|
|
|
|
.replace(`%AVG_PAGE_TIME%`, `${formatTime((pages.reduce((a, e) => a + e.avgSeconds, 0) / pages.length) * 1000)}`)
|
|
.replace(`%AVG_SESSION_TIME%`, `${formatTime((sessions.data.reduce((a, e) => a + e.count, 0) / sessions.data.length) * 1000)}`)
|
|
|
|
.replace('%LOGO%', options.customLogo)
|
|
}
|
|
|
|
return page7;
|
|
}
|
|
|
|
export async function getPage8(options: BaseOptions) {
|
|
const page8_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page8.html'), 'utf-8');
|
|
|
|
|
|
const [continents, countries] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$continent", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 4 }
|
|
]),
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$country", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
])
|
|
])
|
|
|
|
let page8 = page8_raw
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
page8 = page8
|
|
.replace(`%CONTINENT_${i}_A%`, `${continents[i]?._id}`)
|
|
.replace(`%CONTINENT_${i}_B%`, `${continents[i]?.count}`)
|
|
.replace(`%CONTINENT_${i}_C%`, `${getDistribution(continents, 'count', i)}%`)
|
|
}
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
page8 = page8
|
|
.replace(`%COUNTRY_${i}_A%`, `${countries[i]?._id}`)
|
|
.replace(`%COUNTRY_${i}_B%`, `${countries[i]?.count}`)
|
|
.replace(`%COUNTRY_${i}_C%`, `${getDistribution(countries, 'count', i)}%`)
|
|
}
|
|
|
|
page8 = page8.replace('%LOGO%', options.customLogo)
|
|
|
|
return page8;
|
|
}
|
|
|
|
export async function getPage9(options: BaseOptions) {
|
|
const page9_raw = fs.readFileSync(path.join(options.resourcesPath, 'pages/page9.html'), 'utf-8');
|
|
|
|
|
|
const [cities] = await Promise.all([
|
|
VisitModel.aggregate([
|
|
{
|
|
$match: {
|
|
project_id: options.target._id,
|
|
created_at: { $gte: options.from, $lte: options.to },
|
|
website: options.domain
|
|
}
|
|
},
|
|
{ $group: { _id: "$city", count: { $sum: 1, } } },
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 5 }
|
|
]),
|
|
])
|
|
|
|
let page9 = page9_raw
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
page9 = page9
|
|
.replace(`%CITY_${i}_A%`, `${cities[i]?._id ?? 'Unknown'}`)
|
|
.replace(`%CITY_${i}_B%`, `${cities[i]?.count}`)
|
|
.replace(`%CITY_${i}_C%`, `${getDistribution(cities, 'count', i)}%`)
|
|
}
|
|
|
|
page9 = page9.replace('%LOGO%', options.customLogo)
|
|
|
|
return page9;
|
|
}
|
|
|
|
|
|
|
|
function getResourcePath() {
|
|
if (isSelfhosted()) return '/home/app/public/pdf/';
|
|
return process.dev ? './public/pdf/' : './.output/public/pdf/';
|
|
}
|
|
|
|
export default defineEventHandler(async event => {
|
|
|
|
const ctx = await getRequestContext(event, 'pid', 'range');
|
|
|
|
const project = await ProjectModel.findById(ctx.project_id);
|
|
if (!project) return setResponseStatus(event, 400, 'Project not found');
|
|
|
|
const { customLogo } = await readBody(event);
|
|
|
|
const target = project;
|
|
const from = new Date(ctx.from);
|
|
const to = new Date(ctx.to);
|
|
const query = getQuery(event);
|
|
const domain = query.domain as string;
|
|
|
|
const resourcesPath = getResourcePath();
|
|
|
|
|
|
const [page1, page2, page3, page4, page5, page6, page7, page8, page9] = await Promise.all([
|
|
getPage1({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage2({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage3({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage4({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage5({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage6({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage7({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage8({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
getPage9({ resourcesPath, domain, from, to, target, customLogo: customLogo ?? 'https://dashboard.litlyx.com/pdf/pdf_images/logo-black.png' }),
|
|
])
|
|
|
|
|
|
const CUSTOM_CHROMIUM = fs.existsSync('/usr/bin/chromium-browser');
|
|
|
|
const browser = await puppeteer.launch({
|
|
executablePath: CUSTOM_CHROMIUM ? '/usr/bin/chromium-browser' : undefined,
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-features=site-per-process',
|
|
'--aggressive-cache-discard',
|
|
'--disable-cache',
|
|
'--disable-application-cache',
|
|
'--disable-offline-load-stale-cache',
|
|
'--disable-gpu-shader-disk-cache',
|
|
'--media-cache-size=0',
|
|
'--disk-cache-size=0',
|
|
],
|
|
});
|
|
const page = await browser.newPage();
|
|
|
|
await page.setContent(page1, { waitUntil: "load" });
|
|
const page1buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page2, { waitUntil: "load" });
|
|
const page2buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page3, { waitUntil: "load" });
|
|
await new Promise(e => setTimeout(e, 1000));
|
|
const page3buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page4, { waitUntil: "load" });
|
|
const page4buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page5, { waitUntil: "load" });
|
|
const page5buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page6, { waitUntil: "load" });
|
|
const page6buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page7, { waitUntil: "load" });
|
|
const page7buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page8, { waitUntil: "load" });
|
|
const page8buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await page.setContent(page9, { waitUntil: "load" });
|
|
const page9buffer = await page.pdf({ format: "A4", printBackground: true });
|
|
|
|
await browser.close();
|
|
|
|
|
|
|
|
const mergedPdf = await PDFDocument.create();
|
|
|
|
const pdf1 = await PDFDocument.load(page1buffer);
|
|
const copiedPages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
|
|
copiedPages1.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf2 = await PDFDocument.load(page2buffer);
|
|
const copiedPages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
|
|
copiedPages2.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf3 = await PDFDocument.load(page3buffer);
|
|
const copiedPages3 = await mergedPdf.copyPages(pdf3, pdf3.getPageIndices());
|
|
copiedPages3.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf4 = await PDFDocument.load(page4buffer);
|
|
const copiedPages4 = await mergedPdf.copyPages(pdf4, pdf4.getPageIndices());
|
|
copiedPages4.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf5 = await PDFDocument.load(page5buffer);
|
|
const copiedPages5 = await mergedPdf.copyPages(pdf5, pdf5.getPageIndices());
|
|
copiedPages5.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf6 = await PDFDocument.load(page6buffer);
|
|
const copiedPages6 = await mergedPdf.copyPages(pdf6, pdf6.getPageIndices());
|
|
copiedPages6.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf7 = await PDFDocument.load(page7buffer);
|
|
const copiedPages7 = await mergedPdf.copyPages(pdf7, pdf7.getPageIndices());
|
|
copiedPages7.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf8 = await PDFDocument.load(page8buffer);
|
|
const copiedPages8 = await mergedPdf.copyPages(pdf8, pdf8.getPageIndices());
|
|
copiedPages8.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const pdf9 = await PDFDocument.load(page9buffer);
|
|
const copiedPages9 = await mergedPdf.copyPages(pdf9, pdf9.getPageIndices());
|
|
copiedPages9.forEach((p) => mergedPdf.addPage(p));
|
|
|
|
const finalPdfBytes = await mergedPdf.save();
|
|
|
|
setResponseHeaders(event, {
|
|
'Content-Type': 'application/pdf',
|
|
'Content-Disposition': 'attachment; filename="AdvancedReport.pdf"',
|
|
});
|
|
|
|
return finalPdfBytes;
|
|
|
|
}); |