mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add snapshots and fix top cards following it
This commit is contained in:
@@ -208,8 +208,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
<div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
|
||||
<div class="flex gap-1 items-center justify-center text-lyx-text-dark">
|
||||
<div class="poppins">
|
||||
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim()
|
||||
}}
|
||||
{{ new Date(snapshot.from).toLocaleString().split(',')[0].trim() }}
|
||||
</div>
|
||||
<div class="poppins"> to </div>
|
||||
<div class="poppins">
|
||||
@@ -217,7 +216,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2" v-if="snapshot._id.toString().startsWith('default') === false">
|
||||
<div class="mt-2" v-if="('default' in snapshot == false)">
|
||||
<UPopover placement="bottom">
|
||||
<LyxUiButton type="danger" class="w-full text-center">
|
||||
Delete current snapshot
|
||||
|
||||
@@ -3,19 +3,13 @@
|
||||
import DateService from '@services/DateService';
|
||||
import type { Slice } from '@services/DateService';
|
||||
|
||||
const { snapshot, safeSnapshotDates } = useSnapshot()
|
||||
const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
||||
|
||||
const snapshotDays = computed(() => {
|
||||
const to = new Date(safeSnapshotDates.value.to).getTime();
|
||||
const from = new Date(safeSnapshotDates.value.from).getTime();
|
||||
return (to - from) / 1000 / 60 / 60 / 24;
|
||||
});
|
||||
|
||||
const chartSlice = computed(() => {
|
||||
const snapshotSizeMs = new Date(snapshot.value.to).getTime() - new Date(snapshot.value.from).getTime();
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 6) return 'hour' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 30) return 'day' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 90) return 'day' as Slice;
|
||||
if (snapshotDuration.value <= 3) return 'hour' as Slice;
|
||||
if (snapshotDuration.value <= 3 * 7) return 'day' as Slice;
|
||||
if (snapshotDuration.value <= 3 * 30) return 'week' as Slice;
|
||||
return 'month' as Slice;
|
||||
});
|
||||
|
||||
@@ -42,29 +36,30 @@ function transformResponse(input: { _id: string, count: number }[]) {
|
||||
}
|
||||
|
||||
const visitsData = useFetch('/api/timeline/visits', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
headers: useComputedHeaders({ slice: chartSlice }), lazy: true, transform: transformResponse
|
||||
});
|
||||
|
||||
const sessionsData = useFetch('/api/timeline/sessions', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
headers: useComputedHeaders({ slice: chartSlice }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const sessionsDurationData = useFetch('/api/timeline/sessions_duration', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
headers: useComputedHeaders({ slice: chartSlice }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const bouncingRateData = useFetch('/api/timeline/bouncing_rate', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
headers: useComputedHeaders({ slice: chartSlice }), lazy: true, transform: transformResponse
|
||||
});
|
||||
|
||||
const avgVisitDay = computed(() => {
|
||||
if (!visitsData.data.value) return '0.00';
|
||||
const counts = visitsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
const avg = counts / Math.max(snapshotDuration.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
const avgSessionsDay = computed(() => {
|
||||
if (!sessionsData.data.value) return '0.00';
|
||||
const counts = sessionsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
const avg = counts / Math.max(snapshotDuration.value, 1);
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
@@ -123,8 +118,8 @@ const avgSessionDuration = computed(() => {
|
||||
</DashboardCountCard>
|
||||
|
||||
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
|
||||
text="Visit duration" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer" text="Visit duration"
|
||||
:value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
||||
color="#f56523">
|
||||
</DashboardCountCard>
|
||||
|
||||
79
dashboard/composables/snapshots/BaseSnapshots.ts
Normal file
79
dashboard/composables/snapshots/BaseSnapshots.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
import type { TProjectSnapshot } from "@schema/project/ProjectSnapshot";
|
||||
|
||||
import * as fns from 'date-fns';
|
||||
|
||||
export type DefaultSnapshot = TProjectSnapshot & { default: true }
|
||||
export type GenericSnapshot = TProjectSnapshot | DefaultSnapshot;
|
||||
|
||||
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id']) {
|
||||
|
||||
const today: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___today' as any,
|
||||
name: 'Today',
|
||||
from: fns.startOfDay(Date.now()),
|
||||
to: fns.endOfDay(Date.now()),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
const lastDay: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___lastDay' as any,
|
||||
name: 'Last Day',
|
||||
from: fns.startOfDay(fns.subDays(Date.now(), 1)),
|
||||
to: fns.endOfDay(fns.subDays(Date.now(), 1)),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
const currentWeek: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___currentWeek' as any,
|
||||
name: 'Current Week',
|
||||
from: fns.startOfWeek(Date.now()),
|
||||
to: fns.endOfWeek(Date.now()),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
const lastWeek: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___lastWeek' as any,
|
||||
name: 'Last Week',
|
||||
from: fns.startOfWeek(fns.subWeeks(Date.now(), 1)),
|
||||
to: fns.endOfWeek(fns.subWeeks(Date.now(), 1)),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
|
||||
const currentMonth: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___currentMonth' as any,
|
||||
name: 'Current Month',
|
||||
from: fns.startOfMonth(Date.now()),
|
||||
to: fns.endOfMonth(Date.now()),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
const lastMonth: DefaultSnapshot = {
|
||||
project_id,
|
||||
_id: '___lastMonth' as any,
|
||||
name: 'Last Month',
|
||||
from: fns.startOfMonth(fns.subMonths(Date.now(), 1)),
|
||||
to: fns.endOfMonth(fns.subMonths(Date.now(), 1)),
|
||||
color: '#CC11CC',
|
||||
default: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek]
|
||||
|
||||
return snapshotList;
|
||||
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
||||
const useActivePid = customOptions?.useActivePid || true;
|
||||
|
||||
const headers = computed<Record<string, string>>(() => {
|
||||
|
||||
console.trace('Computed recalculated');
|
||||
const parsedCustom: Record<string, string> = {}
|
||||
const customKeys = Object.keys(customOptions?.custom || {});
|
||||
for (const key of customKeys) {
|
||||
@@ -43,5 +43,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
return headers;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TProjectSnapshot } from "@schema/project/ProjectSnapshot";
|
||||
|
||||
import fns from 'date-fns';
|
||||
import { getDefaultSnapshots, type GenericSnapshot } from "./snapshots/BaseSnapshots";
|
||||
import * as fns from 'date-fns';
|
||||
|
||||
const { projectId, project } = useProject();
|
||||
|
||||
@@ -10,59 +10,20 @@ const headers = computed(() => {
|
||||
'x-pid': projectId.value ?? ''
|
||||
}
|
||||
});
|
||||
const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
|
||||
headers
|
||||
});
|
||||
|
||||
const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', { headers });
|
||||
|
||||
watch(project, async () => {
|
||||
await remoteSnapshots.refresh();
|
||||
snapshot.value = isLiveDemo.value ? snapshots.value[0] : snapshots.value[1];
|
||||
});
|
||||
|
||||
const snapshots = computed(() => {
|
||||
|
||||
const getDefaultSnapshots: () => TProjectSnapshot[] = () => [
|
||||
{
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default0' as any,
|
||||
name: 'All',
|
||||
from: new Date(project.value?.created_at || 0),
|
||||
to: new Date(Date.now()),
|
||||
color: '#CCCCCC'
|
||||
},
|
||||
{
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'current_month' as any,
|
||||
name: 'Current month',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
|
||||
to: new Date(Date.now()),
|
||||
color: '#00CC00'
|
||||
},
|
||||
{
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default2' as any,
|
||||
name: 'Last week',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
|
||||
to: new Date(Date.now()),
|
||||
color: '#0F02D2'
|
||||
},
|
||||
{
|
||||
project_id: project.value?._id as any,
|
||||
_id: 'default3' as any,
|
||||
name: 'Last day',
|
||||
from: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
to: new Date(Date.now()),
|
||||
color: '#CC11CC'
|
||||
}
|
||||
]
|
||||
|
||||
return [
|
||||
...getDefaultSnapshots(),
|
||||
...(remoteSnapshots.data.value || [])
|
||||
];
|
||||
const snapshots = computed<GenericSnapshot[]>(() => {
|
||||
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any) : [];
|
||||
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
||||
})
|
||||
|
||||
const snapshot = ref<TProjectSnapshot>(isLiveDemo.value ? snapshots.value[0] : snapshots.value[1]);
|
||||
const snapshot = ref<GenericSnapshot>(snapshots.value[1]);
|
||||
|
||||
const safeSnapshotDates = computed(() => {
|
||||
const from = new Date(snapshot.value?.from || 0).toISOString();
|
||||
@@ -77,7 +38,7 @@ async function updateSnapshots() {
|
||||
const snapshotDuration = computed(() => {
|
||||
const from = new Date(snapshot.value?.from || 0).getTime();
|
||||
const to = new Date(snapshot.value?.to || 0).getTime();
|
||||
return (to - from) / (1000 * 60 * 60 * 24);
|
||||
return fns.differenceInDays(to, from);
|
||||
});
|
||||
|
||||
export function useSnapshot() {
|
||||
|
||||
@@ -16,7 +16,7 @@ export default defineEventHandler(async event => {
|
||||
const timelineData = await executeTimelineAggregation({
|
||||
projectId: project_id,
|
||||
model: VisitModel,
|
||||
from, to, slice,
|
||||
from, to, slice
|
||||
});
|
||||
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||
return timelineFilledMerged;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Slice } from "@services/DateService";
|
||||
import DateService from "@services/DateService";
|
||||
import type mongoose from "mongoose";
|
||||
|
||||
import * as fns from 'date-fns'
|
||||
|
||||
export type TimelineAggregationOptions = {
|
||||
projectId: mongoose.Schema.Types.ObjectId | mongoose.Types.ObjectId,
|
||||
@@ -27,17 +27,20 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
||||
options.customProjection = options.customProjection || {};
|
||||
options.customIdGroup = options.customIdGroup || {};
|
||||
|
||||
const { group, sort, fromParts } = DateService.getQueryDateRange(options.slice);
|
||||
const { group, sort } = DateService.getQueryDateRange(options.slice);
|
||||
|
||||
if (!sort) throw Error('Slice is probably not correct');
|
||||
|
||||
const dateDistDays = (new Date(options.to).getTime() - new Date(options.from).getTime()) / (1000 * 60 * 60 * 24)
|
||||
// 15 Days
|
||||
if (options.slice === 'hour' && (dateDistDays > 15)) throw Error('Date gap too big for this slice');
|
||||
// 1 Year
|
||||
if (options.slice === 'day' && (dateDistDays > 365)) throw Error('Date gap too big for this slice');
|
||||
const daysDiff = fns.differenceInDays(new Date(options.to), new Date(options.from));
|
||||
|
||||
// 3 Days
|
||||
if (options.slice === 'hour' && (daysDiff > 3)) throw Error('Date gap too big for this slice');
|
||||
// 3 Weeks
|
||||
if (options.slice === 'day' && (daysDiff > 7 * 3)) throw Error('Date gap too big for this slice');
|
||||
// 3 Months
|
||||
if (options.slice === 'week' && (daysDiff > 30 * 3)) throw Error('Date gap too big for this slice');
|
||||
// 3 Years
|
||||
if (options.slice === 'month' && (dateDistDays > 365 * 3)) throw Error('Date gap too big for this slice');
|
||||
if (options.slice === 'month' && (daysDiff > 365 * 3)) throw Error('Date gap too big for this slice');
|
||||
|
||||
|
||||
const aggregation = [
|
||||
@@ -48,9 +51,22 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
||||
...options.customMatch
|
||||
}
|
||||
},
|
||||
{ $group: { _id: { ...group, ...options.customIdGroup }, count: { $sum: 1 }, ...options.customGroup } },
|
||||
{
|
||||
$group: {
|
||||
_id: { ...group, ...options.customIdGroup },
|
||||
count: { $sum: 1 },
|
||||
firstDate: { $first: '$created_at' },
|
||||
...options.customGroup
|
||||
}
|
||||
},
|
||||
{ $sort: sort },
|
||||
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count", ...options.customProjection } }
|
||||
{
|
||||
$project: {
|
||||
_id: "$firstDate",
|
||||
count: "$count",
|
||||
...options.customProjection
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (options.debug === true) {
|
||||
@@ -75,7 +91,53 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count:
|
||||
}
|
||||
|
||||
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
|
||||
const filledDates = DateService.createBetweenDates(from, to, slice);
|
||||
const merged = DateService.mergeFilledDates(filledDates.dates, timeline, '_id', slice, { count: 0 });
|
||||
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
|
||||
const merged = mergeDates(timeline, allDates, slice);
|
||||
return merged;
|
||||
}
|
||||
|
||||
function generateDateSlices(slice: Slice, fromDate: Date, toDate: Date) {
|
||||
const slices: Date[] = [];
|
||||
let currentDate = fromDate;
|
||||
const addFunctions: { [key in Slice]: any } = { hour: fns.addHours, day: fns.addDays, week: fns.addWeeks, month: fns.addMonths, year: fns.addYears };
|
||||
const addFunction = addFunctions[slice];
|
||||
if (!addFunction) { throw new Error(`Invalid slice: ${slice}`); }
|
||||
while (fns.isBefore(currentDate, toDate) || currentDate.getTime() === toDate.getTime()) {
|
||||
slices.push(currentDate);
|
||||
currentDate = addFunction(currentDate, 1);
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
function mergeDates(timeline: { _id: string, count: number }[], dates: Date[], slice: Slice) {
|
||||
|
||||
const result: { _id: string, count: number }[] = [];
|
||||
|
||||
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: fns.isSameDay, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
|
||||
|
||||
const isSame = isSames[slice];
|
||||
|
||||
if (!isSame) {
|
||||
throw new Error(`Invalid slice: ${slice}`);
|
||||
}
|
||||
|
||||
for (const element of timeline) {
|
||||
const elementDate = new Date(element._id);
|
||||
for (const date of dates) {
|
||||
if (isSame(elementDate, date)) {
|
||||
const existingEntry = result.find(item => isSame(new Date(item._id), date));
|
||||
|
||||
if (existingEntry) {
|
||||
existingEntry.count += element.count;
|
||||
} else {
|
||||
result.push({
|
||||
_id: date.toISOString(),
|
||||
count: element.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,11 @@ import dayjs from 'dayjs';
|
||||
export type Slice = keyof typeof slicesData;
|
||||
|
||||
const slicesData = {
|
||||
hour: {
|
||||
fromOffset: 1000 * 60 * 60 * 24
|
||||
},
|
||||
day: {
|
||||
fromOffset: 1000 * 60 * 60 * 24 * 7
|
||||
},
|
||||
month: {
|
||||
fromOffset: 1000 * 60 * 60 * 24 * 30 * 12
|
||||
},
|
||||
year: {
|
||||
fromOffset: 1000 * 60 * 60 * 24 * 30 * 12 * 10
|
||||
}
|
||||
hour: {},
|
||||
day: {},
|
||||
week: {},
|
||||
month: {},
|
||||
year: {}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,32 +25,23 @@ class DateService {
|
||||
return date.format();
|
||||
}
|
||||
|
||||
getDefaultRange(slice: Slice) {
|
||||
return {
|
||||
from: new Date(Date.now() - slicesData[slice].fromOffset).toISOString(),
|
||||
to: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
getQueryDateRange(slice: Slice) {
|
||||
|
||||
const group: Record<string, any> = {}
|
||||
const sort: Record<string, any> = {}
|
||||
const fromParts: Record<string, any> = {}
|
||||
|
||||
switch (slice) {
|
||||
case 'hour':
|
||||
group.hour = { $hour: '$created_at' }
|
||||
fromParts.hour = "$_id.hour";
|
||||
case 'day':
|
||||
group.day = { $dayOfMonth: '$created_at' }
|
||||
fromParts.day = "$_id.day";
|
||||
case 'week':
|
||||
group.week = { $isoWeek: '$created_at' }
|
||||
case 'month':
|
||||
group.month = { $month: '$created_at' }
|
||||
fromParts.month = "$_id.month";
|
||||
case 'year':
|
||||
group.year = { $year: '$created_at' }
|
||||
fromParts.year = "$_id.year";
|
||||
}
|
||||
|
||||
switch (slice) {
|
||||
@@ -68,6 +52,7 @@ class DateService {
|
||||
sort['_id.year'] = 1;
|
||||
sort['_id.month'] = 1;
|
||||
break;
|
||||
case 'week':
|
||||
case 'day':
|
||||
sort['_id.year'] = 1;
|
||||
sort['_id.month'] = 1;
|
||||
@@ -81,7 +66,7 @@ class DateService {
|
||||
break;
|
||||
}
|
||||
|
||||
return { group, sort, fromParts }
|
||||
return { group, sort }
|
||||
}
|
||||
|
||||
prepareDateRange(from: string, to: string, slice: Slice) {
|
||||
@@ -115,7 +100,6 @@ class DateService {
|
||||
return { dates: filledDates, from, to };
|
||||
}
|
||||
|
||||
|
||||
fillDates(dates: string[], slice: Slice) {
|
||||
const allDates: dayjs.Dayjs[] = [];
|
||||
const firstDate = dayjs(dates.at(0));
|
||||
|
||||
Reference in New Issue
Block a user