new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

View File

@@ -0,0 +1,128 @@
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { prepareTimelineAggregation } from "../services/TimelineService";
import { Types } from "mongoose";
import { StandardController, TimelineOptions } from "./UtilsController";
class BouncingController extends StandardController {
constructor() { super('bouncing', e => e.count); }
async getTimeline(options: TimelineOptions): Promise<any[]> {
const { project_id, slice, from, to, domain } = options;
const info = prepareTimelineAggregation({
model: VisitModel,
from, to, slice, domain,
projectId: new Types.ObjectId(project_id),
forced: options.ignoreSliceSize
});
const aggregation = [
{
$match: {
project_id: new Types.ObjectId(project_id),
created_at: {
$gte: new Date(from),
$lte: new Date(to)
},
...info.domainMatch,
}
},
{
$project: {
created_at: 1, session: 1
}
},
{
$addFields: {
date: {
$dateTrunc: {
date: "$created_at",
unit: info.granularity,
timezone: "UTC"
}
}
}
},
{
$group: {
_id: {
date: "$date",
session: "$session"
},
pageViews: {
$sum: 1
}
}
},
{
$group: {
_id: "$_id.date",
totalSessions: {
$sum: 1
},
bouncedSessions: {
$sum: { $cond: [{ $eq: ["$pageViews", 1] }, 1, 0] }
}
}
},
{
$project: {
_id: 1,
totalSessions: 1,
bouncedSessions: 1,
bounceRate: {
$cond: [{ $eq: ["$totalSessions", 0] }, 0,
{
$multiply: [
{
$divide: [
"$bouncedSessions",
"$totalSessions"
]
},
100
]
}
]
}
}
},
{
$densify: {
field: "_id",
range: {
step: 1,
unit: info.granularity,
bounds: [
info.from,
info.to
]
}
}
},
{
$set: {
count: {
$ifNull: ["$bounceRate", 0]
}
}
},
{
$sort: {
"_id": 1
}
},
{
$project: { _id: 1, count: 1, timestamp: 1 }
}
] as any[];
const result = await VisitModel.aggregate(aggregation, { allowDiskUse: true });
return result;
}
}
export const bouncingController = new BouncingController();

View File

@@ -0,0 +1,22 @@
import { endOfDay, startOfDay } from "date-fns";
import { Types } from "mongoose";
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
type DomainsListOptions = {
project_id: string,
date: Date
}
export async function getAllDomains(options: DomainsListOptions) {
const domains = await VisitModel.aggregate([
{
$match: {
project_id: new Types.ObjectId(options.project_id),
created_at: { $gte: startOfDay(options.date), $lte: endOfDay(options.date) }
},
},
{ $group: { _id: "$website" } }
]);
return domains.map(e => e._id) as string[];
}

View File

@@ -0,0 +1,198 @@
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { prepareTimelineAggregation } from "../services/TimelineService";
import { Types } from "mongoose";
import { StandardController, TimelineOptions } from "./UtilsController";
class DurationController extends StandardController {
constructor() { super('duration', e => e.count); }
async getTimeline(options: TimelineOptions): Promise<any[]> {
const { project_id, from, to, slice, domain } = options;
const info = prepareTimelineAggregation({
model: VisitModel,
from, to, slice, domain,
projectId: new Types.ObjectId(project_id),
forced: options.ignoreSliceSize
});
const aggregation = [
{
$match: {
project_id: new Types.ObjectId(project_id),
created_at: {
$gte: new Date(from),
$lte: new Date(to)
},
...info.domainMatch,
}
},
{
$project: {
_id: 0,
session: 1,
created_at: 1
}
},
{
$setWindowFields: {
partitionBy: "$session",
sortBy: {
created_at: 1
},
output: {
prevTime: {
$shift: {
output: "$created_at",
by: -1
}
}
}
}
},
{
$addFields: {
timeDiff: {
$cond: [
{ $eq: ["$prevTime", null] },
0,
{ $divide: [{ $subtract: ["$created_at", "$prevTime"] }, 1000] }
]
},
isNewSegment: {
$cond: [
{
$gt: [{ $divide: [{ $subtract: ["$created_at", "$prevTime"] }, 1000] },
300 // 5 minutes = 300 seconds
]
}, 1, 0]
}
}
},
{
$setWindowFields: {
partitionBy: "$session",
sortBy: {
created_at: 1
},
output: {
segmentIndex: {
$sum: "$isNewSegment",
window: {
documents: ["unbounded", "current"]
}
}
}
}
},
{
$addFields: {
segmentId: {
$concat: [
"$session",
"_",
{
$toString: "$segmentIndex"
}
]
}
}
},
{
$group: {
_id: "$segmentId",
session: {
$first: "$session"
},
startTime: {
$min: "$created_at"
},
endTime: {
$max: "$created_at"
},
pageViews: {
$sum: 1
}
}
},
{
$addFields: {
durationSeconds: {
$divide: [
{
$subtract: ["$endTime", "$startTime"]
},
1000
]
}
}
},
{
$project: {
_id: 0,
segmentId: "$_id",
session: 1,
startTime: 1,
endTime: 1,
pageViews: 1,
durationSeconds: 1
}
},
{
$addFields: {
date: {
$dateTrunc: {
date: "$startTime",
unit: info.granularity,
timezone: "UTC"
}
}
}
},
{
$group: {
_id: {
date: "$date"
},
averageDuration: {
$avg: "$durationSeconds"
},
sessionCount: {
$sum: 1
}
}
},
{
$densify: {
field: "_id.date",
range: {
step: 1,
unit: info.granularity,
bounds: [info.from, info.to]
}
}
},
{ $set: { averageDuration: { $ifNull: ["$averageDuration", 0] } } },
{
$sort: {
"_id.date": 1
}
},
{
$project: {
_id: "$_id.date",
count: {
$round: ["$averageDuration", 2]
},
sessions: "$sessionCount",
}
}
]
const result = await VisitModel.aggregate(aggregation as any, { allowDiskUse: true });
return result;
}
}
export const durationController = new DurationController();

View File

@@ -0,0 +1,37 @@
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { executeAdvancedTimelineAggregation } from "../services/TimelineService";
import { Types } from "mongoose";
import { StandardController, TimelineOptions } from "./UtilsController";
class SessionController extends StandardController {
constructor() { super('session', e => e.count); }
async getTimeline(options: TimelineOptions): Promise<any[]> {
const { project_id, from, to, slice, domain } = options;
const timelineData = await executeAdvancedTimelineAggregation({
projectId: new Types.ObjectId(project_id),
model: VisitModel,
from, to, slice, domain,
allowDisk: true,
customIdGroup: { session: '$session' },
customQueries: [
{
index: 2,
query: {
$group: { _id: { date: '$_id.date' }, count: { $sum: 1 } }
}
}
],
forced: options.ignoreSliceSize,
});
return timelineData;
}
}
export const sessionController = new SessionController();

View File

@@ -0,0 +1,132 @@
import mongoose from "mongoose";
import { endOfDay, endOfMonth, endOfWeek, startOfDay, startOfMonth, startOfWeek } from "date-fns";
import { UTCDate } from "@date-fns/utc";
import { Slice } from "~/shared/services/DateService";
import { AggModel } from "~/shared/schema/aggregation/AggSchema";
type BaseOptions = {
project_id: string,
from: number,
to: number,
domain?: string,
slice: Slice
}
export type TimelineOptions = BaseOptions & {
ignoreSliceSize?: boolean
}
export type AggregateOptions = {
project_id: string,
date: Date,
override: boolean,
domains: string[]
}
export class StandardController {
constructor(private data_type: string, private parse: (e: any) => number) { }
async getTimeline(options: TimelineOptions): Promise<any[]> {
console.error('Must implement executeTimeline');
return [];
}
async getAggregated(options: BaseOptions) {
const { project_id, domain } = options;
const from = startOfDay(new UTCDate(options.from));
const to = endOfDay(new UTCDate(options.to));
const aggregation: any[] = [
{
$match: {
project_id: new mongoose.Types.ObjectId(project_id),
data_type: this.data_type,
date: { $gte: from, $lte: to },
domain: domain ?? { $exists: true }
}
},
{
$group: {
_id: {
$dateTrunc: {
date: "$date",
unit: "day"
}
},
count: {
[this.data_type === 'bouncing' ? '$avg' : '$sum']: "$data"
}
}
},
{
$project: {
_id: 0,
date: "$_id",
count: 1
}
},
{
$densify: {
field: "date",
range: {
step: 1,
unit: "day",
bounds: [from, to]
}
}
},
{
$project: {
_id: "$date",
count: {
$ifNull: ["$count", 0]
}
}
}
];
const data = await AggModel.aggregate(aggregation);
return data;
}
async executeDynamic(options: BaseOptions) {
const start = performance.now();
if (options.slice === 'day') {
const aggregated = await this.getAggregated(options);
if (aggregated.findIndex(e => e.count != 0) != -1) {
const end = performance.now();
return { time: end - start, data: aggregated }
}
}
const data = await this.getTimeline(options);
const end = performance.now();
return { time: end - start, data }
}
async aggregate(options: AggregateOptions) {
const { project_id } = options;
const date = startOfDay(new UTCDate(options.date))
for (const domain of options.domains) {
if (options.override === false) {
const exists = await AggModel.exists({ project_id, date, domain, data_type: this.data_type });
if (exists) continue;
}
const data = await this.getTimeline({ project_id, from: startOfDay(date).getTime(), to: endOfDay(date).getTime(), slice: 'day', domain, ignoreSliceSize: true });
if (data.length == 0) continue;
await AggModel.updateOne({ project_id, date, domain, data_type: this.data_type }, {
data: this.parse(data[0])
}, { upsert: true });
}
}
}

View File

@@ -0,0 +1,23 @@
import { VisitModel } from "~/shared/schema/metrics/VisitSchema";
import { executeAdvancedTimelineAggregation } from "../services/TimelineService";
import { Types } from "mongoose";
import { StandardController, TimelineOptions } from "./UtilsController";
class VisitController extends StandardController {
constructor() { super('visit', e => e.count); }
async getTimeline(options: TimelineOptions): Promise<any[]> {
const { project_id, from, to, slice, domain } = options;
const timelineData = await executeAdvancedTimelineAggregation({
projectId: new Types.ObjectId(project_id),
model: VisitModel,
from, to, slice, domain,
forced: options.ignoreSliceSize
});
return timelineData;
}
}
export const visitController = new VisitController();