add bouncing rate + adjustments

This commit is contained in:
Emily
2024-09-30 17:01:16 +02:00
parent 1828edf98b
commit 3ba6cd171b
12 changed files with 391 additions and 57 deletions

View File

@@ -9,7 +9,8 @@ const props = defineProps<{
color: string,
data?: number[],
labels?: string[],
ready?: boolean
ready?: boolean,
slow?: boolean
}>();
const { snapshotDuration } = useSnapshot()
@@ -59,8 +60,9 @@ const uTooltipText = computed(() => {
:color="props.color">
</DashboardEmbedChartCard>
</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>
<div v-if="props.slow"> Can be very slow on large snapshots </div>
</div>
</LyxUiCard>

View File

@@ -21,12 +21,12 @@ const avgVisitDay = computed(() => {
return avg.toFixed(2);
});
const avgEventsDay = computed(() => {
if (!eventsData.data.value) return '0.00';
const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
const avg = counts / Math.max(snapshotDays.value, 1);
return avg.toFixed(2);
});
// const avgEventsDay = computed(() => {
// if (!eventsData.data.value) return '0.00';
// const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
// const avg = counts / Math.max(snapshotDays.value, 1);
// return avg.toFixed(2);
// });
const avgSessionsDay = computed(() => {
if (!sessionsData.data.value) return '0.00';
@@ -35,6 +35,16 @@ const avgSessionsDay = computed(() => {
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(() => {
if (!metricsInfo.value) return '0.00';
@@ -49,6 +59,8 @@ const avgSessionDuration = computed(() => {
});
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;
@@ -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`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
lazy: true, immediate: false
});
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
lazy: true, immediate: false
});
// const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
// method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
// lazy: true, immediate: false
// });
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
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
});
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 () => {
visitsData.execute();
eventsData.execute();
bouncingRateData.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">
</DashboardCountCard>
<DashboardCountCard :ready="!eventsData.pending.value" icon="far fa-flag" text="Total custom events"
:value="formatNumberK(eventsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
:avg="formatNumberK(avgEventsDay) + '/day'" :trend="eventsData.data.value?.trend"
:data="eventsData.data.value?.data" :labels="eventsData.data.value?.labels" color="#1e9b86">
<DashboardCountCard :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend"
:slow="true"
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
</DashboardCountCard>

View File

@@ -22,7 +22,8 @@
"chartjs-plugin-annotation": "^2.2.1",
"date-fns": "^3.6.0",
"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",
"jsonwebtoken": "^9.0.2",
"litlyx-js": "^1.0.2",

View File

@@ -47,18 +47,24 @@ onMounted(async () => {
const creatingCsv = ref<boolean>(false);
async function downloadCSV() {
async function downloadCSV(isGoogle: boolean) {
creatingCsv.value = true;
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`, signHeaders());
const blob = new Blob([result], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ReportVisits.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
const result = await $fetch(`/api/project/generate_csv?mode=visits&slice=${options.indexOf(selectedTimeFrom.value)}`,
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 a = document.createElement('a');
a.href = url;
a.download = 'ReportVisits.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} else {
alert(result);
}
creatingCsv.value = false;
}
@@ -78,7 +84,6 @@ function goToUpgrade() {
</script>
<template>
@@ -102,16 +107,21 @@ function goToUpgrade() {
<USelectMenu v-model="selectedTimeFrom" :options="options"></USelectMenu>
</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">
Download CSV
</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">
<i class="far fa-lock"></i>
Upgrade plan for CSV
</div>
</div> -->
</div>

View File

@@ -11,17 +11,35 @@ const selectLabelsEvents = [
const eventsStackedSelectIndex = ref<number>(0);
const activeProject = useActiveProject();
const { snapshot } = useSnapshot();
const { snapshot, safeSnapshotDates } = useSnapshot();
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>
<template>
<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">
<CardTitled :key="refreshKey" class="p-4 flex-[4] w-full h-full" title="Events"

View File

@@ -110,7 +110,7 @@ function goToUpgrade() {
</div>
</div>
<DashboardTopSection></DashboardTopSection>
<DashboardTopSection :key="refreshKey"></DashboardTopSection>
<DashboardTopCards :key="refreshKey"></DashboardTopCards>

View File

@@ -33,8 +33,11 @@ importers:
specifier: ^1.11.11
version: 1.11.11
google-auth-library:
specifier: ^9.9.0
specifier: ^9.10.0
version: 9.10.0(encoding@0.1.13)
googleapis:
specifier: ^144.0.0
version: 144.0.0(encoding@0.1.13)
highlight.js:
specifier: ^11.10.0
version: 11.10.0
@@ -1499,20 +1502,20 @@ packages:
'@vue/reactivity@3.4.27':
resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==}
'@vue/reactivity@3.5.8':
resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==}
'@vue/reactivity@3.5.10':
resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==}
'@vue/runtime-core@3.4.27':
resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==}
'@vue/runtime-core@3.5.8':
resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==}
'@vue/runtime-core@3.5.10':
resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==}
'@vue/runtime-dom@3.4.27':
resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==}
'@vue/runtime-dom@3.5.8':
resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==}
'@vue/runtime-dom@3.5.10':
resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==}
'@vue/server-renderer@3.4.27':
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
@@ -1522,6 +1525,9 @@ packages:
'@vue/shared@3.4.27':
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
'@vue/shared@3.5.10':
resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
'@vue/shared@3.5.8':
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
@@ -2734,6 +2740,14 @@ packages:
resolution: {integrity: sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==}
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:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
@@ -4984,6 +4998,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-template@2.0.8:
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
@@ -7241,19 +7258,19 @@ snapshots:
dependencies:
'@vue/shared': 3.4.27
'@vue/reactivity@3.5.8':
'@vue/reactivity@3.5.10':
dependencies:
'@vue/shared': 3.5.8
'@vue/shared': 3.5.10
'@vue/runtime-core@3.4.27':
dependencies:
'@vue/reactivity': 3.4.27
'@vue/shared': 3.4.27
'@vue/runtime-core@3.5.8':
'@vue/runtime-core@3.5.10':
dependencies:
'@vue/reactivity': 3.5.8
'@vue/shared': 3.5.8
'@vue/reactivity': 3.5.10
'@vue/shared': 3.5.10
'@vue/runtime-dom@3.4.27':
dependencies:
@@ -7261,11 +7278,11 @@ snapshots:
'@vue/shared': 3.4.27
csstype: 3.1.3
'@vue/runtime-dom@3.5.8':
'@vue/runtime-dom@3.5.10':
dependencies:
'@vue/reactivity': 3.5.8
'@vue/runtime-core': 3.5.8
'@vue/shared': 3.5.8
'@vue/reactivity': 3.5.10
'@vue/runtime-core': 3.5.10
'@vue/shared': 3.5.10
csstype: 3.1.3
'@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.5.10': {}
'@vue/shared@3.5.8': {}
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
@@ -8571,6 +8590,26 @@ snapshots:
- encoding
- 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:
dependencies:
get-intrinsic: 1.2.4
@@ -11143,6 +11182,8 @@ snapshots:
dependencies:
punycode: 2.3.1
url-template@2.0.8: {}
urlpattern-polyfill@8.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)):
dependencies:
'@vue/runtime-core': 3.5.8
'@vue/runtime-dom': 3.5.8
'@vue/runtime-core': 3.5.10
'@vue/runtime-dom': 3.5.10
chart.js: 3.9.1
csstype: 3.1.3
lodash-es: 4.17.21

View File

@@ -36,7 +36,14 @@ export default defineEventHandler(async event => {
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({
@@ -45,6 +52,7 @@ export default defineEventHandler(async event => {
name: payload.name,
locale: payload.locale,
picture: payload.picture,
google_tokens: tokens,
created_at: Date.now()
});

View 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;
});
});

View 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;
});
});

View File

@@ -1,9 +1,62 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
import { EventModel } from '@schema/metrics/EventSchema';
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 => {
@@ -69,6 +122,15 @@ export default defineEventHandler(async event => {
return content.join(',');
}).join('\n');
const isGoogle = getHeader(event, 'x-google-export');
if (isGoogle === 'true') {
const data = await exportToGoogle(result, userData.id);
return data;
}
return result;
} else {
return '';

View File

@@ -1,4 +1,4 @@
import { model, Schema } from 'mongoose';
import { model, Schema, Types } from 'mongoose';
export type TUser = {
email: string,
@@ -6,7 +6,15 @@ export type TUser = {
given_name: string,
locale: 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>({
@@ -15,6 +23,14 @@ const UserSchema = new Schema<TUser>({
given_name: String,
locale: 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() }
})