+
Delete current snapshot
diff --git a/dashboard/components/dashboard/TopCards.vue b/dashboard/components/dashboard/TopCards.vue
index ba7d63e..5414d2b 100644
--- a/dashboard/components/dashboard/TopCards.vue
+++ b/dashboard/components/dashboard/TopCards.vue
@@ -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(() => {
-
diff --git a/dashboard/composables/snapshots/BaseSnapshots.ts b/dashboard/composables/snapshots/BaseSnapshots.ts
new file mode 100644
index 0000000..534fe8c
--- /dev/null
+++ b/dashboard/composables/snapshots/BaseSnapshots.ts
@@ -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;
+
+}
diff --git a/dashboard/composables/useCustomFetch.ts b/dashboard/composables/useCustomFetch.ts
index 04b9932..207d811 100644
--- a/dashboard/composables/useCustomFetch.ts
+++ b/dashboard/composables/useCustomFetch.ts
@@ -25,7 +25,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
const useActivePid = customOptions?.useActivePid || true;
const headers = computed>(() => {
-
+ console.trace('Computed recalculated');
const parsedCustom: Record = {}
const customKeys = Object.keys(customOptions?.custom || {});
for (const key of customKeys) {
@@ -43,5 +43,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
}
})
+
+
return headers;
}
\ No newline at end of file
diff --git a/dashboard/composables/useSnapshot.ts b/dashboard/composables/useSnapshot.ts
index b92c193..e0b4d35 100644
--- a/dashboard/composables/useSnapshot.ts
+++ b/dashboard/composables/useSnapshot.ts
@@ -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('/api/project/snapshots', {
- headers
-});
+
+const remoteSnapshots = useFetch('/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(() => {
+ const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any) : [];
+ return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
})
-const snapshot = ref(isLiveDemo.value ? snapshots.value[0] : snapshots.value[1]);
+const snapshot = ref(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() {
diff --git a/dashboard/server/api/timeline/visits.ts b/dashboard/server/api/timeline/visits.ts
index fea47e2..f531b2b 100644
--- a/dashboard/server/api/timeline/visits.ts
+++ b/dashboard/server/api/timeline/visits.ts
@@ -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;
diff --git a/dashboard/server/services/TimelineService.ts b/dashboard/server/services/TimelineService.ts
index 477b234..da1ff06 100644
--- a/dashboard/server/services/TimelineService.ts
+++ b/dashboard/server/services/TimelineService.ts
@@ -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(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(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;
-}
\ No newline at end of file
+}
+
+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;
+}
diff --git a/shared/services/DateService.ts b/shared/services/DateService.ts
index c186617..6cfe5da 100644
--- a/shared/services/DateService.ts
+++ b/shared/services/DateService.ts
@@ -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 = {}
const sort: Record = {}
- const fromParts: Record = {}
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));