mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
new selfhosted version
This commit is contained in:
128
dashboard/server/controllers/BouncingController.ts
Normal file
128
dashboard/server/controllers/BouncingController.ts
Normal 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();
|
||||
|
||||
22
dashboard/server/controllers/DomainController.ts
Normal file
22
dashboard/server/controllers/DomainController.ts
Normal 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[];
|
||||
}
|
||||
198
dashboard/server/controllers/DurationController.ts
Normal file
198
dashboard/server/controllers/DurationController.ts
Normal 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();
|
||||
37
dashboard/server/controllers/SessionController.ts
Normal file
37
dashboard/server/controllers/SessionController.ts
Normal 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();
|
||||
132
dashboard/server/controllers/UtilsController.ts
Normal file
132
dashboard/server/controllers/UtilsController.ts
Normal 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 });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
23
dashboard/server/controllers/VisitController.ts
Normal file
23
dashboard/server/controllers/VisitController.ts
Normal 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();
|
||||
Reference in New Issue
Block a user