mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add bouncing rate + adjustments
This commit is contained in:
@@ -9,7 +9,8 @@ const props = defineProps<{
|
|||||||
color: string,
|
color: string,
|
||||||
data?: number[],
|
data?: number[],
|
||||||
labels?: string[],
|
labels?: string[],
|
||||||
ready?: boolean
|
ready?: boolean,
|
||||||
|
slow?: boolean
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { snapshotDuration } = useSnapshot()
|
const { snapshotDuration } = useSnapshot()
|
||||||
@@ -59,8 +60,9 @@ const uTooltipText = computed(() => {
|
|||||||
:color="props.color">
|
:color="props.color">
|
||||||
</DashboardEmbedChartCard>
|
</DashboardEmbedChartCard>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!ready" class="flex justify-center items-center w-full h-full">
|
<div v-if="!ready" class="flex justify-center items-center w-full h-full flex-col gap-2">
|
||||||
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
|
<div v-if="props.slow"> Can be very slow on large snapshots </div>
|
||||||
</div>
|
</div>
|
||||||
</LyxUiCard>
|
</LyxUiCard>
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ const avgVisitDay = computed(() => {
|
|||||||
return avg.toFixed(2);
|
return avg.toFixed(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
const avgEventsDay = computed(() => {
|
// const avgEventsDay = computed(() => {
|
||||||
if (!eventsData.data.value) return '0.00';
|
// if (!eventsData.data.value) return '0.00';
|
||||||
const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
|
// const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||||
const avg = counts / Math.max(snapshotDays.value, 1);
|
// const avg = counts / Math.max(snapshotDays.value, 1);
|
||||||
return avg.toFixed(2);
|
// return avg.toFixed(2);
|
||||||
});
|
// });
|
||||||
|
|
||||||
const avgSessionsDay = computed(() => {
|
const avgSessionsDay = computed(() => {
|
||||||
if (!sessionsData.data.value) return '0.00';
|
if (!sessionsData.data.value) return '0.00';
|
||||||
@@ -35,6 +35,16 @@ const avgSessionsDay = computed(() => {
|
|||||||
return avg.toFixed(2);
|
return avg.toFixed(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const avgBouncingRate = computed(() => {
|
||||||
|
if (!bouncingRateData.data.value) return '0.00 %'
|
||||||
|
|
||||||
|
const counts = bouncingRateData.data.value.data
|
||||||
|
.filter(e => e > 0)
|
||||||
|
.reduce((a, e) => e + a, 0);
|
||||||
|
|
||||||
|
const avg = counts / Math.max(bouncingRateData.data.value.data.filter(e => e > 0).length, 1);
|
||||||
|
return avg.toFixed(2) + ' %';
|
||||||
|
})
|
||||||
|
|
||||||
const avgSessionDuration = computed(() => {
|
const avgSessionDuration = computed(() => {
|
||||||
if (!metricsInfo.value) return '0.00';
|
if (!metricsInfo.value) return '0.00';
|
||||||
@@ -49,6 +59,8 @@ const avgSessionDuration = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const chartSlice = computed(() => {
|
const chartSlice = computed(() => {
|
||||||
const snapshotSizeMs = new Date(snapshot.value.to).getTime() - new Date(snapshot.value.from).getTime();
|
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 * 6) return 'hour' as Slice;
|
||||||
@@ -79,15 +91,25 @@ function getBody() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const computedHeaders = computed(() => {
|
||||||
|
return {
|
||||||
|
...signHeaders().headers,
|
||||||
|
'x-pid': activeProject.value?._id.toString() || '',
|
||||||
|
'x-from': safeSnapshotDates.value.from,
|
||||||
|
'x-to': safeSnapshotDates.value.to,
|
||||||
|
'x-slice': chartSlice.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
||||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||||
lazy: true, immediate: false
|
lazy: true, immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
// const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
||||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
// method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||||
lazy: true, immediate: false
|
// lazy: true, immediate: false
|
||||||
});
|
// });
|
||||||
|
|
||||||
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
||||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||||
@@ -99,12 +121,26 @@ const sessionsDurationData = useFetch(`/api/metrics/${activeProject.value?._id}/
|
|||||||
lazy: true, immediate: false
|
lazy: true, immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bouncingRateData = useFetch(`/api/data/bouncing_rate`, {
|
||||||
|
headers: computedHeaders, lazy: true, immediate: false,
|
||||||
|
transform: (input: { data: string, value: number | null }[]) => {
|
||||||
|
const data = input.map(e => e.value || 0);
|
||||||
|
const labels = input.map(e => DateService.getChartLabelFromISO(e.data, navigator.language, chartSlice.value));
|
||||||
|
const pool = [...input.map(e => e.value || 0)];
|
||||||
|
pool.pop();
|
||||||
|
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||||
|
const diffPercent: number = (100 / avg * (input.at(-1)?.value || 0)) - 100;
|
||||||
|
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||||
|
return { data, labels, trend }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
visitsData.execute();
|
visitsData.execute();
|
||||||
eventsData.execute();
|
bouncingRateData.execute();
|
||||||
sessionsData.execute();
|
sessionsData.execute();
|
||||||
sessionsDurationData.execute();
|
sessionsDurationData.execute()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -120,10 +156,10 @@ onMounted(async () => {
|
|||||||
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
<DashboardCountCard :ready="!eventsData.pending.value" icon="far fa-flag" text="Total custom events"
|
<DashboardCountCard :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
|
||||||
:value="formatNumberK(eventsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend"
|
||||||
:avg="formatNumberK(avgEventsDay) + '/day'" :trend="eventsData.data.value?.trend"
|
:slow="true"
|
||||||
:data="eventsData.data.value?.data" :labels="eventsData.data.value?.labels" color="#1e9b86">
|
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"chartjs-plugin-annotation": "^2.2.1",
|
"chartjs-plugin-annotation": "^2.2.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"google-auth-library": "^9.9.0",
|
"google-auth-library": "^9.10.0",
|
||||||
|
"googleapis": "^144.0.0",
|
||||||
"highlight.js": "^11.10.0",
|
"highlight.js": "^11.10.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"litlyx-js": "^1.0.2",
|
"litlyx-js": "^1.0.2",
|
||||||
|
|||||||
@@ -47,10 +47,13 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const creatingCsv = ref<boolean>(false);
|
const creatingCsv = ref<boolean>(false);
|
||||||
|
|
||||||
async function downloadCSV() {
|
async function downloadCSV(isGoogle: boolean) {
|
||||||
creatingCsv.value = true;
|
creatingCsv.value = true;
|
||||||
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`, signHeaders());
|
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`,
|
||||||
const blob = new Blob([result], { type: 'text/csv' });
|
signHeaders({ 'x-google-export': isGoogle ? 'true' : 'false' })
|
||||||
|
);
|
||||||
|
if (!isGoogle) {
|
||||||
|
const blob = new Blob([result as any], { type: 'text/csv' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
@@ -59,6 +62,9 @@ async function downloadCSV() {
|
|||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
|
} else {
|
||||||
|
alert(result);
|
||||||
|
}
|
||||||
creatingCsv.value = false;
|
creatingCsv.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +84,6 @@ function goToUpgrade() {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
|
||||||
@@ -102,16 +107,21 @@ function goToUpgrade() {
|
|||||||
<USelectMenu v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
<USelectMenu v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isPremium" @click="downloadCSV()"
|
<div v-if="isPremium" @click="downloadCSV(false)"
|
||||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||||
Download CSV
|
Download CSV
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isPremium" @click="goToUpgrade()"
|
<div v-if="isPremium" @click="downloadCSV(true)"
|
||||||
|
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||||
|
Export CSV to Google Sheets
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div v-if="!isPremium" @click="goToUpgrade()"
|
||||||
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||||
<i class="far fa-lock"></i>
|
<i class="far fa-lock"></i>
|
||||||
Upgrade plan for CSV
|
Upgrade plan for CSV
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,35 @@ const selectLabelsEvents = [
|
|||||||
const eventsStackedSelectIndex = ref<number>(0);
|
const eventsStackedSelectIndex = ref<number>(0);
|
||||||
|
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
const { snapshot } = useSnapshot();
|
const { snapshot, safeSnapshotDates } = useSnapshot();
|
||||||
|
|
||||||
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const headers = computed(() => {
|
||||||
|
return {
|
||||||
|
'x-from': safeSnapshotDates.value.from,
|
||||||
|
'x-to': safeSnapshotDates.value.to,
|
||||||
|
'Authorization': authorizationHeaderComputed.value,
|
||||||
|
'x-schema': 'events',
|
||||||
|
'x-pid': activeProject.value?._id.toString() || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers, lazy: true });
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
|
||||||
|
|
||||||
|
|
||||||
|
<LyxUiCard class="w-full">
|
||||||
|
Total events: {{ eventsData.data.value?.[0].total || '???' }}
|
||||||
|
</LyxUiCard>
|
||||||
|
|
||||||
<div class="flex gap-6 flex-col xl:flex-row h-full">
|
<div class="flex gap-6 flex-col xl:flex-row h-full">
|
||||||
|
|
||||||
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"
|
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function goToUpgrade() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DashboardTopSection></DashboardTopSection>
|
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
|
||||||
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
<DashboardTopCards :key="refreshKey"></DashboardTopCards>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
77
dashboard/pnpm-lock.yaml
generated
77
dashboard/pnpm-lock.yaml
generated
@@ -33,8 +33,11 @@ importers:
|
|||||||
specifier: ^1.11.11
|
specifier: ^1.11.11
|
||||||
version: 1.11.11
|
version: 1.11.11
|
||||||
google-auth-library:
|
google-auth-library:
|
||||||
specifier: ^9.9.0
|
specifier: ^9.10.0
|
||||||
version: 9.10.0(encoding@0.1.13)
|
version: 9.10.0(encoding@0.1.13)
|
||||||
|
googleapis:
|
||||||
|
specifier: ^144.0.0
|
||||||
|
version: 144.0.0(encoding@0.1.13)
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.10.0
|
specifier: ^11.10.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
@@ -1499,20 +1502,20 @@ packages:
|
|||||||
'@vue/reactivity@3.4.27':
|
'@vue/reactivity@3.4.27':
|
||||||
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
|
||||||
|
|
||||||
'@vue/reactivity@3.5.8':
|
'@vue/reactivity@3.5.10':
|
||||||
resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==}
|
resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
|
||||||
|
|
||||||
'@vue/runtime-core@3.5.8':
|
'@vue/runtime-core@3.5.10':
|
||||||
resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==}
|
resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
|
||||||
|
|
||||||
'@vue/runtime-dom@3.5.8':
|
'@vue/runtime-dom@3.5.10':
|
||||||
resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==}
|
resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==}
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27':
|
'@vue/server-renderer@3.4.27':
|
||||||
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
|
||||||
@@ -1522,6 +1525,9 @@ packages:
|
|||||||
'@vue/shared@3.4.27':
|
'@vue/shared@3.4.27':
|
||||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||||
|
|
||||||
|
'@vue/shared@3.5.10':
|
||||||
|
resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
|
||||||
|
|
||||||
'@vue/shared@3.5.8':
|
'@vue/shared@3.5.8':
|
||||||
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
||||||
|
|
||||||
@@ -2734,6 +2740,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==}
|
resolution: {integrity: sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
googleapis-common@7.2.0:
|
||||||
|
resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
googleapis@144.0.0:
|
||||||
|
resolution: {integrity: sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
gopd@1.0.1:
|
gopd@1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
|
|
||||||
@@ -4984,6 +4998,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
url-template@2.0.8:
|
||||||
|
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||||
|
|
||||||
urlpattern-polyfill@8.0.2:
|
urlpattern-polyfill@8.0.2:
|
||||||
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
|
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
|
||||||
|
|
||||||
@@ -7241,19 +7258,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/reactivity@3.5.8':
|
'@vue/reactivity@3.5.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/shared': 3.5.8
|
'@vue/shared': 3.5.10
|
||||||
|
|
||||||
'@vue/runtime-core@3.4.27':
|
'@vue/runtime-core@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.4.27
|
'@vue/reactivity': 3.4.27
|
||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
|
|
||||||
'@vue/runtime-core@3.5.8':
|
'@vue/runtime-core@3.5.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.5.8
|
'@vue/reactivity': 3.5.10
|
||||||
'@vue/shared': 3.5.8
|
'@vue/shared': 3.5.10
|
||||||
|
|
||||||
'@vue/runtime-dom@3.4.27':
|
'@vue/runtime-dom@3.4.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7261,11 +7278,11 @@ snapshots:
|
|||||||
'@vue/shared': 3.4.27
|
'@vue/shared': 3.4.27
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/runtime-dom@3.5.8':
|
'@vue/runtime-dom@3.5.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/reactivity': 3.5.8
|
'@vue/reactivity': 3.5.10
|
||||||
'@vue/runtime-core': 3.5.8
|
'@vue/runtime-core': 3.5.10
|
||||||
'@vue/shared': 3.5.8
|
'@vue/shared': 3.5.10
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
'@vue/server-renderer@3.4.27(vue@3.4.27(typescript@5.4.2))':
|
||||||
@@ -7276,6 +7293,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.4.27': {}
|
'@vue/shared@3.4.27': {}
|
||||||
|
|
||||||
|
'@vue/shared@3.5.10': {}
|
||||||
|
|
||||||
'@vue/shared@3.5.8': {}
|
'@vue/shared@3.5.8': {}
|
||||||
|
|
||||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||||
@@ -8571,6 +8590,26 @@ snapshots:
|
|||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
googleapis-common@7.2.0(encoding@0.1.13):
|
||||||
|
dependencies:
|
||||||
|
extend: 3.0.2
|
||||||
|
gaxios: 6.6.0(encoding@0.1.13)
|
||||||
|
google-auth-library: 9.10.0(encoding@0.1.13)
|
||||||
|
qs: 6.12.1
|
||||||
|
url-template: 2.0.8
|
||||||
|
uuid: 9.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
googleapis@144.0.0(encoding@0.1.13):
|
||||||
|
dependencies:
|
||||||
|
google-auth-library: 9.10.0(encoding@0.1.13)
|
||||||
|
googleapis-common: 7.2.0(encoding@0.1.13)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
gopd@1.0.1:
|
gopd@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
@@ -11143,6 +11182,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
url-template@2.0.8: {}
|
||||||
|
|
||||||
urlpattern-polyfill@8.0.2: {}
|
urlpattern-polyfill@8.0.2: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
@@ -11347,8 +11388,8 @@ snapshots:
|
|||||||
|
|
||||||
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
vue-chart-3@3.1.8(chart.js@3.9.1)(vue@3.4.27(typescript@5.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/runtime-core': 3.5.8
|
'@vue/runtime-core': 3.5.10
|
||||||
'@vue/runtime-dom': 3.5.8
|
'@vue/runtime-dom': 3.5.10
|
||||||
chart.js: 3.9.1
|
chart.js: 3.9.1
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const user = await UserModel.findOne({ email: payload.email });
|
const user = await UserModel.findOne({ email: payload.email });
|
||||||
|
|
||||||
if (user) return { error: false, access_token: createUserJwt({ email: user.email, name: user.name }) }
|
if (user) {
|
||||||
|
user.google_tokens = tokens as any;
|
||||||
|
await user.save();
|
||||||
|
return {
|
||||||
|
error: false,
|
||||||
|
access_token: createUserJwt({ email: user.email, name: user.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const newUser = new UserModel({
|
const newUser = new UserModel({
|
||||||
@@ -45,6 +52,7 @@ export default defineEventHandler(async event => {
|
|||||||
name: payload.name,
|
name: payload.name,
|
||||||
locale: payload.locale,
|
locale: payload.locale,
|
||||||
picture: payload.picture,
|
picture: payload.picture,
|
||||||
|
google_tokens: tokens,
|
||||||
created_at: Date.now()
|
created_at: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
79
dashboard/server/api/data/bouncing_rate.ts
Normal file
79
dashboard/server/api/data/bouncing_rate.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
import { Redis } from "~/server/services/CacheService";
|
||||||
|
import DateService from "@services/DateService";
|
||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const project_id = getHeader(event, 'x-pid');
|
||||||
|
if (!project_id) return;
|
||||||
|
|
||||||
|
const user = getRequestUser(event);
|
||||||
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const from = getRequestHeader(event, 'x-from');
|
||||||
|
const to = getRequestHeader(event, 'x-to');
|
||||||
|
|
||||||
|
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to are required');
|
||||||
|
|
||||||
|
const slice = getRequestHeader(event, 'x-slice');
|
||||||
|
|
||||||
|
const cacheKey = `bouncing_rate:${project_id}:${from}:${to}`;
|
||||||
|
const cacheExp = 60 * 60; //1 hour
|
||||||
|
|
||||||
|
return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => {
|
||||||
|
|
||||||
|
const dateDistDays = (new Date(to).getTime() - new Date(from).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
// 15 Days
|
||||||
|
if (slice === 'hour' && (dateDistDays > 15)) throw Error('Date gap too big for this slice');
|
||||||
|
// 1 Year
|
||||||
|
if (slice === 'day' && (dateDistDays > 365)) throw Error('Date gap too big for this slice');
|
||||||
|
// 3 Years
|
||||||
|
if (slice === 'month' && (dateDistDays > 365 * 3)) throw Error('Date gap too big for this slice');
|
||||||
|
|
||||||
|
|
||||||
|
const allDates = DateService.createBetweenDates(from, to, slice as any);
|
||||||
|
|
||||||
|
const result: { date: string, value: number }[] = [];
|
||||||
|
|
||||||
|
for (const date of allDates.dates) {
|
||||||
|
|
||||||
|
const visits = await VisitModel.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id: project._id,
|
||||||
|
created_at: {
|
||||||
|
$gte: date.startOf(slice as any).toDate(),
|
||||||
|
$lte: date.endOf(slice as any).toDate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $group: { _id: "$session", count: { $sum: 1, } } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sessions = await SessionModel.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id: project._id,
|
||||||
|
created_at: {
|
||||||
|
$gte: date.startOf(slice as any).toDate(),
|
||||||
|
$lte: date.endOf(slice as any).toDate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $group: { _id: "$session", count: { $sum: 1, }, duration: { $sum: '$duration' } } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const total = visits.length;
|
||||||
|
const bounced = sessions.filter(e => (e.duration / e.count) < 1).length;
|
||||||
|
const bouncing_rate = 100 / total * bounced;
|
||||||
|
result.push({ date: date.toISOString(), value: bouncing_rate });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
61
dashboard/server/api/data/count.ts
Normal file
61
dashboard/server/api/data/count.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
|
import { Redis } from "~/server/services/CacheService";
|
||||||
|
|
||||||
|
import type { Model } from "mongoose";
|
||||||
|
|
||||||
|
|
||||||
|
const allowedModels: Record<string, { model: Model<any>, field: string }> = {
|
||||||
|
'events': {
|
||||||
|
model: EventModel,
|
||||||
|
field: 'name'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TModelName = keyof typeof allowedModels;
|
||||||
|
|
||||||
|
export default defineEventHandler(async event => {
|
||||||
|
const project_id = getHeader(event, 'x-pid');
|
||||||
|
if (!project_id) return;
|
||||||
|
|
||||||
|
const user = getRequestUser(event);
|
||||||
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const from = getRequestHeader(event, 'x-from');
|
||||||
|
const to = getRequestHeader(event, 'x-to');
|
||||||
|
|
||||||
|
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to are required');
|
||||||
|
|
||||||
|
const schemaName = getRequestHeader(event, 'x-schema');
|
||||||
|
if (!schemaName) return setResponseStatus(event, 400, 'x-schema is required');
|
||||||
|
|
||||||
|
if (!Object.keys(allowedModels).includes(schemaName)) return setResponseStatus(event, 400, 'x-schema value is not valid');
|
||||||
|
|
||||||
|
const cacheKey = `count:${schemaName}:${project_id}:${from}:${to}`;
|
||||||
|
const cacheExp = 60;
|
||||||
|
|
||||||
|
return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => {
|
||||||
|
|
||||||
|
const { model } = allowedModels[schemaName as TModelName];
|
||||||
|
|
||||||
|
const result = await model.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id: project._id,
|
||||||
|
created_at: {
|
||||||
|
$gte: new Date(from),
|
||||||
|
$lte: new Date(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$count: 'total'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,9 +1,62 @@
|
|||||||
|
|
||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { ProjectModel } from "@schema/ProjectSchema";
|
||||||
|
import { UserModel } from "@schema/UserSchema";
|
||||||
import { UserSettingsModel } from "@schema/UserSettings";
|
import { UserSettingsModel } from "@schema/UserSettings";
|
||||||
import { EventModel } from '@schema/metrics/EventSchema';
|
import { EventModel } from '@schema/metrics/EventSchema';
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
|
|
||||||
|
import { google } from 'googleapis';
|
||||||
|
|
||||||
|
const { GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_ID } = useRuntimeConfig()
|
||||||
|
|
||||||
|
async function exportToGoogle(data: string, user_id: string) {
|
||||||
|
|
||||||
|
const user = await UserModel.findOne({ _id: user_id }, { google_tokens: true });
|
||||||
|
|
||||||
|
const authClient = new google.auth.OAuth2({
|
||||||
|
clientId: GOOGLE_AUTH_CLIENT_ID,
|
||||||
|
clientSecret: GOOGLE_AUTH_CLIENT_SECRET
|
||||||
|
})
|
||||||
|
|
||||||
|
authClient.setCredentials({ access_token: user?.google_tokens?.access_token, refresh_token: user?.google_tokens?.refresh_token });
|
||||||
|
|
||||||
|
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createSheetResponse = await sheets.spreadsheets.create({
|
||||||
|
requestBody: {
|
||||||
|
properties: {
|
||||||
|
title: 'Text export'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const spreadsheetId = createSheetResponse.data.spreadsheetId;
|
||||||
|
|
||||||
|
await sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId: spreadsheetId as string,
|
||||||
|
range: 'Sheet1!A1',
|
||||||
|
requestBody: {
|
||||||
|
values: data.split('\n').map(e => {
|
||||||
|
return e.split(',')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
|
||||||
|
console.error('Error creating Google Sheet from CSV:', error);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
return { error: 'Auth error, try to logout and login again' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: error.message.toString() }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
@@ -69,6 +122,15 @@ export default defineEventHandler(async event => {
|
|||||||
return content.join(',');
|
return content.join(',');
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const isGoogle = getHeader(event, 'x-google-export');
|
||||||
|
|
||||||
|
if (isGoogle === 'true') {
|
||||||
|
const data = await exportToGoogle(result, userData.id);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { model, Schema } from 'mongoose';
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
export type TUser = {
|
export type TUser = {
|
||||||
email: string,
|
email: string,
|
||||||
@@ -6,7 +6,15 @@ export type TUser = {
|
|||||||
given_name: string,
|
given_name: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
picture: string,
|
picture: string,
|
||||||
created_at: Date
|
created_at: Date,
|
||||||
|
google_tokens?: {
|
||||||
|
refresh_token?: string;
|
||||||
|
expiry_date?: number;
|
||||||
|
access_token?: string;
|
||||||
|
token_type?: string;
|
||||||
|
id_token?: string;
|
||||||
|
scope?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSchema = new Schema<TUser>({
|
const UserSchema = new Schema<TUser>({
|
||||||
@@ -15,6 +23,14 @@ const UserSchema = new Schema<TUser>({
|
|||||||
given_name: String,
|
given_name: String,
|
||||||
locale: String,
|
locale: String,
|
||||||
picture: String,
|
picture: String,
|
||||||
|
google_tokens: {
|
||||||
|
refresh_token: String,
|
||||||
|
expiry_date: Number,
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
id_token: String,
|
||||||
|
scope: String
|
||||||
|
},
|
||||||
created_at: { type: Date, default: () => Date.now() }
|
created_at: { type: Date, default: () => Date.now() }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user