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 () => {
console.log('MOUNTED')
const c = document.createElement('canvas');
const ctx = c.getContext("2d");
let gradient: any = `${props.color}22`;

View File

@@ -1,46 +1,43 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
const data = ref<number[]>([]);
const labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot();
const slice = computed(() => props.slice);
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() {
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;
}
const res = useTimeline('sessions', slice);
onMounted(async () => {
await loadData();
watch(props, async () => { await loadData(); });
watch(snapshot, async () => { await loadData(); });
res.onResponse(resData => {
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>
<template>
<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>
</template>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import DateService from '@services/DateService';
import type { Slice } from '@services/DateService';
const { data: metricsInfo } = useMetricsData();
@@ -75,31 +76,41 @@ const avgSessionDuration = computed(() => {
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) {
target.ready = false;
const response = await useTimeline(timelineEndpointName as any, 'day',
snapshot.value?.from.toString() || "0",
snapshot.value?.to.toString() || Date.now().toString()
);
const response = useTimeline(timelineEndpointName as any, chartSlice);
console.log(timelineEndpointName,response);
response.onResponse(data => {
if (!response) return;
if (!data.value) return;
target.data = response.map(e => e.count);
target.labels = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, 'day'));
target.data = data.value.map(e => e.count);
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();
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.ready = true;
});
response.execute();
}
@@ -113,6 +124,7 @@ async function loadAllData() {
])
}
onMounted(async () => {
await loadAllData();
@@ -137,13 +149,15 @@ onMounted(async () => {
</DashboardCountCard>
<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'"
:trend="eventsData.trend" :data="eventsData.data" :labels="eventsData.labels" color="#1e9b86">
: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(sessionsData.data.reduce((a, e) => a + e, 0))" :avg="formatNumberK(avgSessionsDay) + '/day'"
:trend="sessionsData.trend" :data="sessionsData.data" :labels="sessionsData.labels" color="#4abde8">
: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>
<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 labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>();
const { snapshot } = useSnapshot();
const slice = computed(() => props.slice);
const snapshotFrom = computed(() => {
return new Date(snapshot.value?.from || '0').toISOString();
});
const res = useTimeline('visits', slice);
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 () => {
await loadData();
watch(props, async () => { await loadData(); });
watch(snapshot, async () => { await loadData(); });
res.onResponse(resData => {
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>
<template>
<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>
</template>

View File

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

View File

@@ -19,6 +19,26 @@ export function useMetricsData() {
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() {
const activeProject = useActiveProject();
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 = {}) {
const { from, to } = DateService.prepareDateRange(
fromDate || DateService.getDefaultRange(slice).from,
toDate || DateService.getDefaultRange(slice).to,
slice
);
const activeProject = useActiveProject();
const response = await $fetch(
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`, {
export function useTimelineAdvanced(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) {
const response = useCustomFetch<{ _id: string, count: number }[]>(
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
() => signHeaders({ 'Content-Type': 'application/json' }).headers, {
method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ slice, from, to, ...customBody })
getBody: () => createFromToBody({ slice: slice.value, ...customBody }),
lazy: true,
watchProps: [snapshot, slice]
});
return response as { _id: string, count: number }[];
return response;
}
export async function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Slice, fromDate?: string, toDate?: string) {
return await useTimelineAdvanced(endpoint, slice, fromDate, toDate, {});
export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Ref<Slice>) {
return useTimelineAdvanced(endpoint, slice);
}
export async function useReferrersTimeline(referrer: string, slice: Slice, fromDate?: string, toDate?: string) {
return await useTimelineAdvanced('referrers', slice, fromDate, toDate, { referrer });
export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) {
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) {
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] }
);
return res;
@@ -115,7 +118,7 @@ export function useWebsitesData(limit: number = 10) {
export function useEventsData(limit: number = 10) {
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] }
);
return res;
@@ -123,7 +126,7 @@ export function useEventsData(limit: number = 10) {
export function useReferrersData(limit: number = 10) {
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] }
);
return res;
@@ -131,7 +134,7 @@ export function useReferrersData(limit: number = 10) {
export function useBrowsersData(limit: number = 10) {
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] }
);
return res;
@@ -139,7 +142,7 @@ export function useBrowsersData(limit: number = 10) {
export function useOssData(limit: number = 10) {
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] }
);
return res;
@@ -147,7 +150,7 @@ export function useOssData(limit: number = 10) {
export function useGeolocationData(limit: number = 10) {
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] }
);
return res;
@@ -155,7 +158,7 @@ export function useGeolocationData(limit: number = 10) {
export function useDevicesData(limit: number = 10) {
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] }
);
return res;

View File

@@ -119,7 +119,6 @@ const selectLabels = [
</div>
<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-1">