implementing snapshots

This commit is contained in:
Emily
2024-07-29 15:21:39 +02:00
parent 229c341d7a
commit 7b54c109f0
7 changed files with 133 additions and 111 deletions

View File

@@ -80,6 +80,9 @@ const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, op
onMounted(async () => { onMounted(async () => {
console.log('MOUNTED')
const c = document.createElement('canvas'); const c = document.createElement('canvas');
const ctx = c.getContext("2d"); const ctx = c.getContext("2d");
let gradient: any = `${props.color}22`; let gradient: any = `${props.color}22`;
@@ -106,5 +109,5 @@ onMounted(async () => {
<template> <template>
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart> <LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
</template> </template>

View File

@@ -1,46 +1,43 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService'; import DateService, { type Slice } from '@services/DateService';
const data = ref<number[]>([]); const data = ref<number[]>([]);
const labels = ref<string[]>([]); const labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>(); const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot(); const slice = computed(() => props.slice);
const snapshotFrom = computed(() => { const res = useTimeline('sessions', slice);
return new Date(snapshot.value?.from || '0').toISOString();
});
const snapshotTo = computed(() => {
return new Date(snapshot.value?.to || Date.now()).toISOString();
});
async function loadData() {
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));
ready.value = true;
}
onMounted(async () => { onMounted(async () => {
await loadData();
watch(props, async () => { await loadData(); }); res.onResponse(resData => {
watch(snapshot, async () => { await loadData(); }); if (!resData.value) return;
data.value = resData.value.map(e => e.count);
labels.value = resData.value.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
});
await res.refresh();
watch(props, () => res.refresh());
}) })
const chartVisible = computed(() => {
if (res.pending.value) return false;
if (!res.data.value) return false;
return true;
})
</script> </script>
<template> <template>
<div> <div>
<AdvancedLineChart v-if="ready" :data="data" :labels="labels" color="#f56523"></AdvancedLineChart> <div v-if="!chartVisible" class="flex justify-center py-40">
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
<AdvancedLineChart v-if="chartVisible" :data="data" :labels="labels" color="#f56523"></AdvancedLineChart>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import DateService from '@services/DateService'; import DateService from '@services/DateService';
import type { Slice } from '@services/DateService';
const { data: metricsInfo } = useMetricsData(); const { data: metricsInfo } = useMetricsData();
@@ -75,31 +76,41 @@ const avgSessionDuration = computed(() => {
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s` return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
}); });
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;
return 'month' as Slice;
});
async function loadData(timelineEndpointName: string, target: Data) { async function loadData(timelineEndpointName: string, target: Data) {
target.ready = false; target.ready = false;
const response = await useTimeline(timelineEndpointName as any, 'day', const response = useTimeline(timelineEndpointName as any, chartSlice);
snapshot.value?.from.toString() || "0",
snapshot.value?.to.toString() || Date.now().toString()
);
console.log(timelineEndpointName,response); response.onResponse(data => {
if (!response) return; if (!data.value) return;
target.data = response.map(e => e.count); target.data = data.value.map(e => e.count);
target.labels = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, 'day')); target.labels = data.value.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
const pool = [...response.map(e => e.count)]; const pool = [...data.value.map(e => e.count)];
pool.pop(); pool.pop();
const avg = pool.reduce((a, e) => a + e, 0) / pool.length; const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
const diffPercent: number = (100 / avg * (response.at(-1)?.count || 0)) - 100; const diffPercent: number = (100 / avg * (data.value.at(-1)?.count || 0)) - 100;
target.trend = Math.max(Math.min(diffPercent, 99), -99); target.trend = Math.max(Math.min(diffPercent, 99), -99);
target.ready = true;
});
response.execute();
target.ready = true;
} }
@@ -113,6 +124,7 @@ async function loadAllData() {
]) ])
} }
onMounted(async () => { onMounted(async () => {
await loadAllData(); await loadAllData();
@@ -137,13 +149,15 @@ onMounted(async () => {
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :ready="eventsData.ready" icon="far fa-flag" text="Total custom events" <DashboardCountCard :ready="eventsData.ready" icon="far fa-flag" text="Total custom events"
:value="formatNumberK(eventsData.data.reduce((a, e) => a + e, 0))" :avg="formatNumberK(avgEventsDay) + '/day'" :value="formatNumberK(eventsData.data.reduce((a, e) => a + e, 0))"
:trend="eventsData.trend" :data="eventsData.data" :labels="eventsData.labels" color="#1e9b86"> :avg="formatNumberK(avgEventsDay) + '/day'" :trend="eventsData.trend" :data="eventsData.data"
:labels="eventsData.labels" color="#1e9b86">
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :ready="sessionsData.ready" icon="far fa-user" text="Unique visits sessions" <DashboardCountCard :ready="sessionsData.ready" icon="far fa-user" text="Unique visits sessions"
:value="formatNumberK(sessionsData.data.reduce((a, e) => a + e, 0))" :avg="formatNumberK(avgSessionsDay) + '/day'" :value="formatNumberK(sessionsData.data.reduce((a, e) => a + e, 0))"
:trend="sessionsData.trend" :data="sessionsData.data" :labels="sessionsData.labels" color="#4abde8"> :avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.trend" :data="sessionsData.data"
:labels="sessionsData.labels" color="#4abde8">
</DashboardCountCard> </DashboardCountCard>
<DashboardCountCard :ready="sessionsDurationData.ready" icon="far fa-timer" text="Avg session time" <DashboardCountCard :ready="sessionsDurationData.ready" icon="far fa-timer" text="Avg session time"

View File

@@ -4,43 +4,41 @@ import DateService, { type Slice } from '@services/DateService';
const data = ref<number[]>([]); const data = ref<number[]>([]);
const labels = ref<string[]>([]); const labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>(); const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot(); const slice = computed(() => props.slice);
const snapshotFrom = computed(() => { const res = useTimeline('visits', slice);
return new Date(snapshot.value?.from || '0').toISOString();
});
const snapshotTo = computed(() => {
return new Date(snapshot.value?.to || Date.now()).toISOString();
});
async function loadData() {
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));
ready.value = true;
}
onMounted(async () => { onMounted(async () => {
await loadData();
watch(props, async () => { await loadData(); }); res.onResponse(resData => {
watch(snapshot, async () => { await loadData(); }); if (!resData.value) return;
data.value = resData.value.map(e => e.count);
labels.value = resData.value.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
});
await res.refresh();
watch(props, () => res.refresh());
})
const chartVisible = computed(() => {
if (res.pending.value) return false;
if (!res.data.value) return false;
return true;
}) })
</script> </script>
<template> <template>
<div> <div>
<AdvancedLineChart v-if="ready" :data="data" :labels="labels" color="#5655d7"></AdvancedLineChart> <div v-if="!chartVisible" class="flex justify-center py-40">
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
<AdvancedLineChart v-if="chartVisible" :data="data" :labels="labels" color="#5655d7">
</AdvancedLineChart>
</div> </div>
</template> </template>

View File

@@ -6,7 +6,9 @@ type NitroFetchRequest = Exclude<keyof InternalApi, `/_${string}` | `/api/_${str
export type CustomFetchOptions = { export type CustomFetchOptions = {
watchProps?: WatchSource[], watchProps?: WatchSource[],
lazy?: boolean lazy?: boolean,
method?: string,
getBody?: () => Record<string, any>
} }
type OnResponseCallback<TData> = (data: Ref<TData | undefined>) => any type OnResponseCallback<TData> = (data: Ref<TData | undefined>) => any
@@ -27,7 +29,13 @@ export function useCustomFetch<T>(url: NitroFetchRequest, getHeaders: () => Reco
pending.value = true; pending.value = true;
error.value = undefined; error.value = undefined;
try { try {
data.value = await $fetch<T>(url, { headers: getHeaders() });
data.value = await $fetch<T>(url, {
headers: getHeaders(),
method: (options?.method || 'GET') as any,
body: options?.getBody ? JSON.stringify(options.getBody()) : undefined
});
onResponseCallback(data); onResponseCallback(data);
} catch (err) { } catch (err) {
error.value = err as Error; error.value = err as Error;

View File

@@ -19,6 +19,26 @@ export function useMetricsData() {
return metricsInfo; return metricsInfo;
} }
const { safeSnapshotDates, snapshot } = useSnapshot()
const activeProject = useActiveProject();
const createFromToHeaders = (headers: Record<string, string> = {}) => ({
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
...headers
});
const createFromToBody = (body: Record<string, any> = {}) => ({
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
...body
});
export function useFirstInteractionData() { export function useFirstInteractionData() {
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const metricsInfo = useFetch<boolean>(`/api/metrics/${activeProject.value?._id}/first_interaction`, signHeaders()); const metricsInfo = useFetch<boolean>(`/api/metrics/${activeProject.value?._id}/first_interaction`, signHeaders());
@@ -26,33 +46,25 @@ export function useFirstInteractionData() {
} }
export async function useTimelineAdvanced(endpoint: string, slice: Slice, fromDate?: string, toDate?: string, customBody: Object = {}) { export function useTimelineAdvanced(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) {
const response = useCustomFetch<{ _id: string, count: number }[]>(
const { from, to } = DateService.prepareDateRange( `/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
fromDate || DateService.getDefaultRange(slice).from, () => signHeaders({ 'Content-Type': 'application/json' }).headers, {
toDate || DateService.getDefaultRange(slice).to,
slice
);
const activeProject = useActiveProject();
const response = await $fetch(
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`, {
method: 'POST', method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }), getBody: () => createFromToBody({ slice: slice.value, ...customBody }),
body: JSON.stringify({ slice, from, to, ...customBody }) lazy: true,
watchProps: [snapshot, slice]
}); });
return response;
return response as { _id: string, count: number }[];
} }
export async function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Slice, fromDate?: string, toDate?: string) { export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Ref<Slice>) {
return await useTimelineAdvanced(endpoint, slice, fromDate, toDate, {}); return useTimelineAdvanced(endpoint, slice);
} }
export async function useReferrersTimeline(referrer: string, slice: Slice, fromDate?: string, toDate?: string) { export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) {
return await useTimelineAdvanced('referrers', slice, fromDate, toDate, { referrer }); return await useTimelineAdvanced('referrers', slice, { referrer });
} }
@@ -93,21 +105,12 @@ export function usePagesData(website: string, limit: number = 10) {
} }
const { safeSnapshotDates, snapshot } = useSnapshot()
const activeProject = useActiveProject();
const getFromToHeaders = (headers: Record<string, string> = {}) => ({
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
...headers
});
export function useWebsitesData(limit: number = 10) { export function useWebsitesData(limit: number = 10) {
const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/websites`, const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/websites`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -115,7 +118,7 @@ export function useWebsitesData(limit: number = 10) {
export function useEventsData(limit: number = 10) { export function useEventsData(limit: number = 10) {
const res = useCustomFetch<CustomEventsAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/events`, const res = useCustomFetch<CustomEventsAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/events`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -123,7 +126,7 @@ export function useEventsData(limit: number = 10) {
export function useReferrersData(limit: number = 10) { export function useReferrersData(limit: number = 10) {
const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`, const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -131,7 +134,7 @@ export function useReferrersData(limit: number = 10) {
export function useBrowsersData(limit: number = 10) { export function useBrowsersData(limit: number = 10) {
const res = useCustomFetch<BrowsersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`, const res = useCustomFetch<BrowsersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -139,7 +142,7 @@ export function useBrowsersData(limit: number = 10) {
export function useOssData(limit: number = 10) { export function useOssData(limit: number = 10) {
const res = useCustomFetch<OssAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/oss`, const res = useCustomFetch<OssAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/oss`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -147,7 +150,7 @@ export function useOssData(limit: number = 10) {
export function useGeolocationData(limit: number = 10) { export function useGeolocationData(limit: number = 10) {
const res = useCustomFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`, const res = useCustomFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;
@@ -155,7 +158,7 @@ export function useGeolocationData(limit: number = 10) {
export function useDevicesData(limit: number = 10) { export function useDevicesData(limit: number = 10) {
const res = useCustomFetch<DevicesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/devices`, const res = useCustomFetch<DevicesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/devices`,
() => signHeaders(getFromToHeaders({ 'x-query-limit': limit.toString() })).headers, () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
{ lazy: false, watchProps: [snapshot] } { lazy: false, watchProps: [snapshot] }
); );
return res; return res;

View File

@@ -119,7 +119,6 @@ const selectLabels = [
</div> </div>
<div class="flex w-full justify-center mt-6 px-6"> <div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row"> <div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1"> <div class="flex-1">