implementing snapshots

This commit is contained in:
Emily
2024-07-26 01:29:58 +02:00
parent 2c9f5c45f8
commit e9505e24a0
12 changed files with 238 additions and 45 deletions

View File

@@ -29,6 +29,13 @@ const debugMode = process.dev;
const { isOpen, close } = useMenu();
const { snapshots, snapshot } = useSnapshot();
const snapshotsItems = computed(() => {
if (!snapshots.data.value) return []
return snapshots.data.value as any[];
})
</script>
<template>
@@ -50,6 +57,31 @@ const { isOpen, close } = useMenu();
</div>
<div class="px-4 w-full flex-col">
<USelectMenu class="w-full" v-model="snapshot" :options="snapshotsItems">
<template #label>
<div class="flex items-center gap-2">
<div :style="'background-color:' + snapshot?.color" class="w-2 h-2 rounded-full">
</div>
<div> {{ snapshot?.name }} </div>
</div>
</template>
<template #option="{ option }">
<div class="flex items-center gap-2">
<div :style="'background-color:' + option.color" class="w-2 h-2 rounded-full">
</div>
<div> {{ option.name }} </div>
</div>
</template>
</USelectMenu>
<div v-if="snapshot">
<div> {{ new Date(snapshot.from).toLocaleString('it-IT') }} </div>
<div> {{ new Date(snapshot.to).toLocaleString('it-IT') }}</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div v-for="section of sections" class="flex flex-col gap-1">

View File

@@ -17,7 +17,7 @@ const props = defineProps<{
<template>
<Card class="flex flex-col overflow-hidden relative max-h-[12rem] aspect-[2/1] w-full">
<div class="flex p-4 items-start">
<div v-if="ready" class="flex p-4 items-start">
<div class="flex items-center mt-2 mr-4">
<i :style="`color: ${props.color}`" :class="icon" class="text-[1.6rem] 2xl:text-[2rem]"></i>
</div>
@@ -40,7 +40,7 @@ const props = defineProps<{
</div>
</div>
<div class="absolute bottom-0 left-0 w-full h-[50%] flex items-end" v-if="(props.data?.length || 0) > 0">
<div class="absolute bottom-0 left-0 w-full h-[50%] flex items-end" v-if="((props.data?.length || 0) > 0) && ready">
<DashboardEmbedChartCard v-if="ready" :data="props.data || []" :labels="props.labels || []"
:color="props.color">
</DashboardEmbedChartCard>

View File

@@ -9,8 +9,22 @@ const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot();
const snapshotFrom = computed(() => {
return new Date(snapshot.value?.from || '0').toISOString();
});
const snapshotTo = computed(() => {
return new Date(snapshot.value?.to || Date.now()).toISOString();
});
async function loadData() {
const response = await useTimeline('sessions', props.slice);
ready.value = false;
const response = await useTimeline('sessions', props.slice,
snapshotFrom.value.toString(),
snapshotTo.value.toString()
);
if (!response) return;
data.value = response.map(e => e.count);
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
@@ -20,6 +34,7 @@ async function loadData() {
onMounted(async () => {
await loadData();
watch(props, async () => { await loadData(); });
watch(snapshot, async () => { await loadData(); });
})
</script>

View File

@@ -4,24 +4,50 @@ import DateService from '@services/DateService';
const { data: metricsInfo } = useMetricsData();
type Data = {
data: number[],
labels: string[],
trend: number,
ready: boolean
}
const { snapshot } = useSnapshot()
const visitsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const eventsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const sessionsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const sessionsDurationData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const snapshotFrom = computed(() => {
return new Date(snapshot.value?.from || '0').getTime();
});
const snapshotTo = computed(() => {
return new Date(snapshot.value?.to || Date.now()).getTime();
});
const avgVisitDay = computed(() => {
if (!metricsInfo.value) return '0.00';
const days = (Date.now() - (metricsInfo.value?.firstViewDate || 0)) / 1000 / 60 / 60 / 24;
const avg = metricsInfo.value.visitsCount / Math.max(days, 1);
const days = (snapshotTo.value - snapshotFrom.value) / 1000 / 60 / 60 / 24;
const counts = visitsData.data.reduce((a, e) => e + a, 0);
const avg = counts / Math.max(days, 1);
return avg.toFixed(2);
});
const avgEventsDay = computed(() => {
if (!metricsInfo.value) return '0.00';
const days = (Date.now() - (metricsInfo.value?.firstEventDate || 0)) / 1000 / 60 / 60 / 24;
const avg = metricsInfo.value.eventsCount / Math.max(days, 1);
const days = (snapshotTo.value - snapshotFrom.value) / 1000 / 60 / 60 / 24;
const counts = eventsData.data.reduce((a, e) => e + a, 0);
const avg = counts / Math.max(days, 1);
return avg.toFixed(2);
});
const avgSessionsDay = computed(() => {
if (!metricsInfo.value) return '0.00';
const days = (Date.now() - (metricsInfo.value?.firstViewDate || 0)) / 1000 / 60 / 60 / 24;
const avg = metricsInfo.value.sessionsVisitsCount / Math.max(days, 1);
const days = (snapshotTo.value - snapshotFrom.value) / 1000 / 60 / 60 / 24;
const counts = sessionsData.data.reduce((a, e) => e + a, 0);
const avg = counts / Math.max(days, 1);
return avg.toFixed(2);
});
@@ -49,23 +75,19 @@ const avgSessionDuration = computed(() => {
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
});
type Data = {
data: number[],
labels: string[],
trend: number,
ready: boolean
}
const visitsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const eventsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const sessionsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
const sessionsDurationData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
async function loadData(timelineEndpointName: string, target: Data) {
const response = await useTimeline(timelineEndpointName as any, 'day');
target.ready = false;
const response = await useTimeline(timelineEndpointName as any, 'day',
snapshot.value?.from.toString() || "0",
snapshot.value?.to.toString() || Date.now().toString()
);
console.log(timelineEndpointName,response);
if (!response) return;
target.data = response.map(e => e.count);
target.labels = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, 'day'));
@@ -80,12 +102,23 @@ async function loadData(timelineEndpointName: string, target: Data) {
target.ready = true;
}
async function loadAllData() {
console.log('LOAD ALL DATA')
await Promise.all([
loadData('visits', visitsData),
loadData('events', eventsData),
loadData('sessions', sessionsData),
loadData('sessions_duration', sessionsDurationData),
])
}
onMounted(async () => {
await loadData('visits', visitsData);
await loadData('events', eventsData);
await loadData('sessions', sessionsData);
await loadData('sessions_duration', sessionsDurationData);
await loadAllData();
watch(snapshot, async () => {
await loadAllData();
})
});
@@ -98,17 +131,18 @@ onMounted(async () => {
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4" v-if="metricsInfo">
<DashboardCountCard :ready="visitsData.ready" icon="far fa-earth" text="Total page visits"
:value="formatNumberK(metricsInfo.visitsCount)" :avg="formatNumberK(avgVisitDay) + '/day'"
:trend="visitsData.trend" :data="visitsData.data" :labels="visitsData.labels" color="#5655d7">
:value="formatNumberK(visitsData.data.reduce((a, e) => a + e, 0))"
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.trend" :data="visitsData.data"
:labels="visitsData.labels" color="#5655d7">
</DashboardCountCard>
<DashboardCountCard :ready="eventsData.ready" icon="far fa-flag" text="Total custom events"
:value="formatNumberK(metricsInfo.eventsCount)" :avg="formatNumberK(avgEventsDay) + '/day'"
:value="formatNumberK(eventsData.data.reduce((a, e) => a + e, 0))" :avg="formatNumberK(avgEventsDay) + '/day'"
:trend="eventsData.trend" :data="eventsData.data" :labels="eventsData.labels" color="#1e9b86">
</DashboardCountCard>
<DashboardCountCard :ready="sessionsData.ready" icon="far fa-user" text="Unique visits sessions"
:value="formatNumberK(metricsInfo.sessionsVisitsCount)" :avg="formatNumberK(avgSessionsDay) + '/day'"
:value="formatNumberK(sessionsData.data.reduce((a, e) => a + e, 0))" :avg="formatNumberK(avgSessionsDay) + '/day'"
:trend="sessionsData.trend" :data="sessionsData.data" :labels="sessionsData.labels" color="#4abde8">
</DashboardCountCard>

View File

@@ -8,8 +8,23 @@ const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot();
const snapshotFrom = computed(() => {
return new Date(snapshot.value?.from || '0').toISOString();
});
const snapshotTo = computed(() => {
return new Date(snapshot.value?.to || Date.now()).toISOString();
});
async function loadData() {
const response = await useTimeline('visits', props.slice);
ready.value = false;
const response = await useTimeline('visits', props.slice,
snapshotFrom.value.toString(),
snapshotTo.value.toString()
);
if (!response) return;
data.value = response.map(e => e.count);
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
@@ -19,6 +34,7 @@ async function loadData() {
onMounted(async () => {
await loadData();
watch(props, async () => { await loadData(); });
watch(snapshot, async () => { await loadData(); });
})
</script>

View File

@@ -6,13 +6,24 @@ const { data: websites, pending, refresh } = useWebsitesData();
const currentViewData = ref<(VisitsWebsiteAggregated[] | null)>(websites.value);
watch(pending, () => {
currentViewData.value = websites.value;
})
const isPagesView = ref<boolean>(false);
const isLoading = ref<boolean>(false);
const { snapshot } = useSnapshot()
watch(pending, () => {
isLoading.value = true;
currentViewData.value = websites.value;
isLoading.value = false;
});
watch(snapshot, () => {
refresh();
});
async function showDetails(website: string) {
if (isPagesView.value == true) return;
isLoading.value = true;
@@ -21,6 +32,7 @@ async function showDetails(website: string) {
const { data: pagesData, pending } = usePagesData(website, 10);
watch(pending, () => {
isLoading.value = true;
currentViewData.value = pagesData.value;
isLoading.value = false;
})

View File

@@ -87,12 +87,18 @@ export function usePagesData(website: string, limit: number = 10) {
}
const { safeSnapshotDates } = useSnapshot()
export function useWebsitesData(limit: number = 10) {
const activeProject = useActiveProject();
const res = useFetch<VisitsWebsiteAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/websites`, {
...signHeaders({ 'x-query-limit': limit.toString() }),
key: `websites_data:${limit}`,
lazy: true
...signHeaders({
'x-query-limit': limit.toString(),
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to
}),
key: `websites_data:${limit}:${safeSnapshotDates.value.from}:${safeSnapshotDates.value.to}`,
lazy: true,
});
return res;
}

View File

@@ -0,0 +1,26 @@
import type { TProjectSnapshot } from "@schema/ProjectSnapshot";
const snapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
...signHeaders(),
immediate: false
});
const snapshot = ref<TProjectSnapshot>();
watch(snapshots.data, () => {
if (!snapshots.data.value) return;
snapshot.value = snapshots.data.value[0];
});
const safeSnapshotDates = computed(() => {
const from = new Date(snapshot.value?.from || 0).toISOString();
const to = new Date(snapshot.value?.to || Date.now()).toISOString();
return { from, to }
})
export function useSnapshot() {
if (snapshots.status.value === 'idle') {
snapshots.execute();
}
return { snapshot, snapshots, safeSnapshotDates }
}

View File

@@ -91,7 +91,7 @@ const selectLabels = [
<DashboardTopCards></DashboardTopCards>
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<!-- <div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<CardTitled class="p-4 flex-1" title="Visits trends" sub="Shows trends in page visits.">
<template #header>
@@ -117,7 +117,7 @@ const selectLabels = [
</div>
</CardTitled>
</div>
</div> -->
<div class="flex w-full justify-center mt-6 px-6">

View File

@@ -22,12 +22,25 @@ export default defineEventHandler(async event => {
const limit = getRequestHeader(event, 'x-query-limit');
const numLimit = parseInt(limit || '10');
const from = getRequestHeader(event, 'x-from');
const to = getRequestHeader(event, 'x-to');
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to headers missing');
return await Redis.useCache({
key: `websites:${project_id}:${numLimit}`,
exp: DATA_EXPIRE_TIME
}, async () => {
const websites: VisitsWebsiteAggregated[] = await VisitModel.aggregate([
{ $match: { project_id: project._id }, },
{
$match: {
project_id: project._id,
created_at: {
$gte: new Date(from),
$lte: new Date(to)
}
},
},
{ $group: { _id: "$website", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: numLimit }

View File

@@ -0,0 +1,19 @@
import { ProjectSnapshotModel, TProjectSnapshot } from "@schema/ProjectSnapshot";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const snapshots = await ProjectSnapshotModel.find({ project_id });
return snapshots.map(e => e.toJSON()) as TProjectSnapshot[];
});

View File

@@ -0,0 +1,20 @@
import { model, Schema, Types } from 'mongoose';
export type TProjectSnapshot = {
_id: Schema.Types.ObjectId,
project_id: Schema.Types.ObjectId,
name: string,
from: Date,
to: Date,
color: string
}
const ProjectSnapshotSchema = new Schema<TProjectSnapshot>({
project_id: { type: Types.ObjectId, index: true },
name: { type: String, required: true },
from: { type: Date, required: true },
to: { type: Date, required: true },
color: { type: String, required: true },
});
export const ProjectSnapshotModel = model<TProjectSnapshot>('project_snapshots', ProjectSnapshotSchema);