Merge branch 'dev'

This commit is contained in:
Emily
2024-06-14 15:42:29 +02:00
17 changed files with 337 additions and 196 deletions

Binary file not shown.

View File

@@ -10,7 +10,16 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
// @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css");
@font-face {
font-family: "Geist";
src: url("../fonts/GeistVF.ttf");
}
.geist {
font-family: "Geist";
}
.fas, .fas,
.far, .far,
@@ -59,6 +68,15 @@
} }
.hide-scrollbars {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
}
.card-shadow { .card-shadow {
box-shadow: 0 0 18px #00000033; box-shadow: 0 0 18px #00000033;

View File

@@ -20,57 +20,57 @@ type Props = {
sections: Section[] sections: Section[]
} }
const { isOpen, open, close, toggle } = useMenu()
const route = useRoute(); const route = useRoute();
const props = defineProps<Props>(); const props = defineProps<Props>();
const { isAdmin } = useUserRoles(); const { isAdmin } = useUserRoles();
const { isOpen, close } = useMenu();
</script> </script>
<template> <template>
<div class="w-0 md:w-[5rem] absolute top-0 md:relative h-full"> <div class="CVerticalNavigation h-full w-[20rem] bg-[#111111] flex shadow-[1px_0_10px_#000000] rounded-r-lg" :class="{
<div @mouseover="open()" @mouseleave="close()" 'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
class="CVerticalNavigation absolute z-[80] bg-menu h-full overflow-hidden w-0 md:w-[5rem]" 'hidden lg:flex': !isOpen
:class="{ '!w-[18rem] shadow-[0_0_20px_#000000] rounded-r-2xl': isOpen }"> }">
<div :class="{ 'w-[18rem]': isOpen }"> <div class="p-4 gap-6 flex flex-col w-full">
<div class="flex gap-4 items-center py-6 px-[.9rem] pb-8">
<div class="bg-black h-[2.8rem] aspect-[1/1] flex items-center justify-center rounded-lg"> <div class="flex items-center gap-2 ml-2">
<img class="h-[2.4rem]" :src="'/logo.png'"> <div class="bg-black h-[2.4rem] aspect-[1/1] flex items-center justify-center rounded-lg">
</div> <img class="h-[2rem]" :src="'/logo.png'">
<div v-if="isOpen" class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div> </div>
<div class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div>
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
<i @click="close()" class="fas fa-close"></i>
</div> </div>
<div class="pb-8" v-for="section of sections"> </div>
<div class="flex flex-col px-3 gap-2"> <div class="flex flex-col gap-4">
<template v-for="entry of section.entries"> <div v-for="section of sections" class="flex flex-col gap-1">
<NuxtLink @click="entry.action?.()" :target="entry.external ? '_blank' : ''" <div v-for="entry of section.entries">
v-if="entry.to && (!entry.adminOnly || (isAdmin && !isAdminHidden))" tag="div"
:to="entry.to || '/'" <div class="bg-[#111111] text-gray-300 hover:bg-[#1b1b1b] py-2 px-4 rounded-lg" :class="{
class="text-[#a3a9b6] flex w-full items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-[#363638] hover:text-[#ffffff]" 'text-gray-700 pointer-events-none': entry.disabled,
:class="{ 'bg-[#1b1b1b]': route.path == (entry.to || '#')
'brightness-[.4] pointer-events-none': entry.disabled, }">
'bg-[#363638] shadow-[0px_0px_2px_#ffffff20_inset] border-[#ffffff20] border-[1px] !text-[#ffffff]': route.path == (entry.to || '#')
}"> <NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''"
<div class="flex items-center text-[1.4rem] w-[1.8rem] justify-center"> v-if="(!entry.adminOnly || (isAdmin && !isAdminHidden))" tag="div" class="flex"
:to="entry.to || '/'">
<div class="flex items-center w-[1.8rem] justify-start">
<i :class="entry.icon"></i> <i :class="entry.icon"></i>
</div> </div>
<div v-if="isOpen" class="text-[.9rem] font-bold manrope"> {{ entry.label }} </div> <div class="manrope">
{{ entry.label }}
</div>
</NuxtLink> </NuxtLink>
<div v-if="!entry.to" @click="entry.action?.()" </div>
class="text-[#a3a9b6] flex w-full items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-[#363638] hover:text-[#ffffff]">
<div class="flex items-center text-[1.4rem] w-[1.8rem] justify-center">
<i :class="entry.icon"></i>
</div>
<div v-if="isOpen" class="text-[.9rem] font-bold manrope"> {{ entry.label }} </div>
</div>
</template>
</div> </div>
@@ -83,12 +83,9 @@ const { isAdmin } = useUserRoles();
<style lang="scss" scoped> <style lang="scss" scoped>
.CVerticalNavigation {
transition: all .25s ease-in-out;
}
.CVerticalNavigation * { .CVerticalNavigation * {
font-family: 'Inter'; font-family: 'Geist';
} }
input:focus { input:focus {

View File

@@ -15,7 +15,8 @@ type Props = {
interactive?: boolean, interactive?: boolean,
isDetailView?: boolean, isDetailView?: boolean,
rawButton?: boolean, rawButton?: boolean,
hideShowMore?: boolean hideShowMore?: boolean,
customIconStyle?: string
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emits = defineEmits<{ const emits = defineEmits<{
@@ -97,8 +98,12 @@ function showDetails(id: string) {
<div class="flex px-2 py-1 relative items-center gap-4"> <div class="flex px-2 py-1 relative items-center gap-4">
<div v-if="iconProvider && iconProvider(element._id) != undefined" <div v-if="iconProvider && iconProvider(element._id) != undefined"
class="flex items-center h-[1.3rem]"> class="flex items-center h-[1.3rem]">
<img v-if="iconProvider(element._id)?.[0] == 'img'" class="h-full"
:src="iconProvider(element._id)?.[1]"> <img v-if="iconProvider(element._id)?.[0] == 'img'"
class="h-full"
:style="customIconStyle"
:src="iconProvider(element._id)?.[1]">
<i v-else :class="iconProvider(element._id)?.[1]"></i> <i v-else :class="iconProvider(element._id)?.[1]"></i>
</div> </div>
<span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70"> <span class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">

View File

@@ -1,10 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CountriesAggregated } from '~/server/api/metrics/[project_id]/data/countries'; import type { CountriesAggregated } from '~/server/api/metrics/[project_id]/data/countries';
import type { IconProvider } from './BarsCard.vue';
const activeProject = await useActiveProject(); const activeProject = await useActiveProject();
const { data: countries, pending, refresh } = await useFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`, signHeaders()); const { data: countries, pending, refresh } = await useFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`, signHeaders());
function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link'];
return [
'img',
`https://raw.githubusercontent.com/hampusborgos/country-flags/main/png250px/${id.toLowerCase()}.png`
]
}
const customIconStyle = `width: 2rem; padding: 1px;`
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
@@ -29,7 +39,9 @@ function showMore() {
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="countries || []" :dataIcons="false" :loading="pending" <DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="countries || []" :dataIcons="false"
label="Top Countries" sub-label="Countries" desc=" Lists the countries where users access your website."></DashboardBarsCard> :loading="pending" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider"
:customIconStyle="customIconStyle" desc=" Lists the countries where users access your website.">
</DashboardBarsCard>
</div> </div>
</template> </template>

View File

@@ -86,7 +86,7 @@ onMounted(async () => {
<template> <template>
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4" v-if="metricsInfo"> <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" <DashboardCountCard :ready="visitsData.ready" icon="far fa-earth" text="Total page visits"
:value="formatNumberK(metricsInfo.visitsCount)" :avg="formatNumberK(avgVisitDay) + '/day'" :value="formatNumberK(metricsInfo.visitsCount)" :avg="formatNumberK(avgVisitDay) + '/day'"

View File

@@ -1,10 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const eventNames = ref<string[]>([]); const eventNames = ref<string[]>([]);
const selectedEventName = ref<string>(); const selectedEventName = ref<string>();
const metadataFields = ref<string[]>([]); const metadataFields = ref<string[]>([]);
@@ -50,57 +47,56 @@ const canSearch = computed(() => {
return selectedMetadataField.value != undefined; return selectedMetadataField.value != undefined;
}); });
</script> </script>
<template> <template>
<div class="w-full h-full p-8 flex flex-col">
<CardTitled title="Event metadata analyzer" sub="Filter events metadata fields to analyze them" class="w-full p-4">
<CardTitled title="Event metadata analyzer" sub="Filter events metadata fields to analyze them" class="w-full p-4"> <div class="p-2 flex flex-col">
<div class="p-2 flex flex-col"> <div class="flex flex-col gap-2">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames" v-model="selectedEventName">
</USelectMenu>
<div class="flex flex-col gap-2"> <USelectMenu v-if="metadataFields.length > 0" searchable searchable-placeholder="Search a field..."
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full" class="w-full" placeholder="Select a field" :options="metadataFields"
placeholder="Select an event" :options="eventNames" v-model="selectedEventName"> v-model="selectedMetadataField">
</USelectMenu> </USelectMenu>
</div>
<USelectMenu v-if="metadataFields.length > 0" searchable searchable-placeholder="Search a field..."
class="w-full" placeholder="Select a field" :options="metadataFields"
v-model="selectedMetadataField">
</USelectMenu>
</div>
<div v-if="canSearch" class="flex gap-4 mt-4 items-center">
<div> Filter by name: </div>
<div class="h-full flex items-center text-[1.2rem]">
<input v-model="currentSearchText"
class="bg-black/70 hover:bg-black/40 rounded-lg px-4 py-1 focus:outline-none" type="text">
</div>
<div v-if="canSearch" class="flex gap-4 mt-4 items-center">
<div> Filter by name: </div>
<div class="h-full flex items-center text-[1.2rem]">
<input v-model="currentSearchText"
class="bg-black/70 hover:bg-black/40 rounded-lg px-4 py-1 focus:outline-none" type="text">
</div> </div>
</div> </div>
</CardTitled>
<div class="mt-8 overflow-y-auto px-4 flex flex-col gap-3">
<div class="text-accent poppins font-semibold"> <div class="mt-8 overflow-y-auto px-4 flex flex-col gap-3">
Search results: {{ metadataFieldGroupedFiltered.length }}
</div>
<div class="flex flex-col"> <div class="text-accent poppins font-semibold">
<div v-for="item of metadataFieldGroupedFiltered"> Search results: {{ metadataFieldGroupedFiltered.length }}
<div class="flex gap-2"> </div>
<div> {{ item._id || 'OLD_EVENTS' }} </div>
<div> {{ item.count }} </div> <div class="flex flex-col">
<div v-for="item of metadataFieldGroupedFiltered">
<div class="flex gap-2">
<div> {{ item._id || 'OLD_EVENTS' }} </div>
<div> {{ item.count }} </div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</CardTitled>
</div>
</template> </template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
const activeProject = useActiveProject();
const eventNames = ref<string[]>([]);
const selectedEventName = ref<string>();
onMounted(async () => {
eventNames.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
});
const userFlowData = ref<any>();
const analyzing = ref<boolean>(false);
async function analyzeEvent() {
userFlowData.value = undefined;
analyzing.value = true;
userFlowData.value = await $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?name=${selectedEventName.value}`, signHeaders());
analyzing.value = false;
}
</script>
<template>
<CardTitled title="Event User Flow"
sub="Track your user's journey from external links to custom events within your platform." class="w-full p-4">
<div class="p-2 flex flex-col gap-3">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames" v-model="selectedEventName">
</USelectMenu>
<div v-if="selectedEventName && !analyzing" class="flex justify-center">
<div @click="analyzeEvent()"
class="bg-bg w-fit px-8 py-2 poppins rounded-lg hover:bg-bg/80 cursor-pointer">
Analyze
</div>
</div>
<div v-if="analyzing">
Analyzing...
</div>
<div class="flex flex-col gap-2" v-if="userFlowData">
<div class="flex gap-4 items-center bg-bg py-1 px-2 rounded-lg" v-for="(count, referrer) in userFlowData">
<div class="w-5 h-5 flex items-center justify-center">
<img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`" :alt="'referrer'">
</div>
<div> {{ referrer }} </div>
<div class="grow"></div>
<div> {{ count }} </div>
</div>
</div>
</div>
</CardTitled>
</template>

View File

@@ -19,7 +19,7 @@ const sections: Section[] = [
title: 'Project', title: 'Project',
entries: [ entries: [
{ label: 'Dashboard', to: '/', icon: 'far fa-home' }, { label: 'Dashboard', to: '/', icon: 'far fa-home' },
// { label: 'Events', to: '/events', icon: 'far fa-bolt' }, { label: 'Events', to: '/events', icon: 'far fa-bolt' },
{ label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' }, { label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' },
{ label: 'Report', to: '/report', icon: 'far fa-notes' }, { label: 'Report', to: '/report', icon: 'far fa-notes' },
// { label: 'AI', to: '/dashboard/settings', icon: 'far fa-robot brightness-[.4]' }, // { label: 'AI', to: '/dashboard/settings', icon: 'far fa-robot brightness-[.4]' },
@@ -61,8 +61,7 @@ const sections: Section[] = [
const { showDialog, closeDialog } = useBarCardDialog(); const { showDialog, closeDialog } = useBarCardDialog();
const { isOpen, close, open } = useMenu();
const { open, isOpen, close } = useMenu();
</script> </script>
@@ -73,7 +72,7 @@ const { open, isOpen, close } = useMenu();
<div <div
class="px-6 py-3 flex items-center justify-center shadow-[0_0_10px_#000000CC] z-[20] rounded-xl mx-2 my-2 md:hidden"> class="px-6 py-3 flex items-center justify-center shadow-[0_0_10px_#000000CC] z-[20] rounded-xl mx-2 my-2 lg:hidden">
<i @click="open()" class="fas fa-bars text-[1.2rem] absolute left-6"></i> <i @click="open()" class="fas fa-bars text-[1.2rem] absolute left-6"></i>
<div class="nunito font-semibold text-[1.2rem]"> <div class="nunito font-semibold text-[1.2rem]">
Litlyx Litlyx
@@ -82,13 +81,16 @@ const { open, isOpen, close } = useMenu();
<div class="flex h-full"> <div class="flex h-full">
<div v-if="isOpen" @click="close()" <div v-if="isOpen" @click="close()"
class="lg:hidden barrier bg-black/40 backdrop-blur-[2px] w-full h-full absolute left-0 top-0 z-[40]"> class="lg:hidden barrier bg-black/40 backdrop-blur-[2px] w-full h-full absolute left-0 top-0 z-[40]">
</div> </div>
<CVerticalNavigation :sections="sections"> <CVerticalNavigation :sections="sections">
</CVerticalNavigation> </CVerticalNavigation>
<div class="overflow-hidden w-full bg-bg relative h-full"> <div class="overflow-hidden w-full bg-bg relative h-full">
<div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]"> <div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]">

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const selectLabelsEvents = [
{ label: 'Day', value: 'day' },
{ label: 'Month', value: 'month' },
];
const eventsStackedSelectIndex = ref<number>(0);
</script>
<template>
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
<div class="flex gap-6 flex-col xl:flex-row">
<CardTitled class="p-4 flex-[4]" title="Events" sub="Events stacked bar chart.">
<template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
</SelectButton>
</template>
<div>
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart>
</div>
</CardTitled>
<div class="bg-menu p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
<div class="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text">
Top events
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Displays key events.
</div>
</div>
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</div>
</div>
<div class="flex">
<EventsUserFlow></EventsUserFlow>
</div>
<div class="flex">
<EventsMetadataAnalyzer></EventsMetadataAnalyzer>
</div>
</div>
</template>

View File

@@ -7,7 +7,7 @@ const activeProject = useActiveProject();
const mainChartSelectIndex = ref<number>(1); const mainChartSelectIndex = ref<number>(1);
const sessionsChartSelectIndex = ref<number>(1); const sessionsChartSelectIndex = ref<number>(1);
const eventsStackedSelectIndex = ref<number>(0);
const route = useRoute(); const route = useRoute();
@@ -23,7 +23,7 @@ const limitsInfo = ref<{
onMounted(async () => { onMounted(async () => {
if (route.query.just_logged) return location.href = '/'; if (route.query.just_logged) return location.href = '/';
limitsInfo.value = await $fetch("/api/project/limits_info", signHeaders()); limitsInfo.value = await $fetch<any>("/api/project/limits_info", signHeaders());
}); });
@@ -66,10 +66,6 @@ const selectLabels = [
{ label: 'Day', value: 'day' } { label: 'Day', value: 'day' }
]; ];
const selectLabelsEvents = [
{ label: 'Day', value: 'day' },
{ label: 'Month', value: 'month' },
];
</script> </script>
@@ -123,42 +119,6 @@ const selectLabelsEvents = [
</div> </div>
<div class="flex p-6 gap-6 flex-col xl:flex-row">
<CardTitled class="p-4 flex-[4]" title="Events" sub="Events stacked bar chart.">
<template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
</SelectButton>
</template>
<div>
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart>
</div>
</CardTitled>
<div class="bg-menu p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
<div class="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text">
Top events
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Displays key events.
</div>
</div>
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</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">

View File

@@ -116,9 +116,9 @@ const selectLabelsEvents = [
</div> </div>
<div class="p-6">
<CardTitled class="p-4 flex-1" title="Events" sub="Events stacked bar chart."> <div class="flex gap-6 flex-col xl:flex-row p-6">
<CardTitled class="p-4 flex-[4]" title="Events" sub="Events stacked bar chart.">
<template #header> <template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event" <SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents"> :currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
@@ -129,8 +129,23 @@ const selectLabelsEvents = [
</EventsStackedBarChart> </EventsStackedBarChart>
</div> </div>
</CardTitled> </CardTitled>
<div class="bg-menu p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
<div class="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text">
Top events
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Displays key events.
</div>
</div>
<DashboardEventsChart class="w-full"> </DashboardEventsChart>
</div>
</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 lg:flex-row"> <div class="flex w-full gap-6 flex-col lg:flex-row">
<div class="flex-1"> <div class="flex-1">

View File

@@ -1,43 +0,0 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const eventNames = ref<string[]>([]);
onMounted(async () => {
eventNames.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
});
function test() {
const res = $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?name=docs_clicked`)
console.log(res);
}
</script>
<template>
<div class="w-full h-full p-8 flex flex-col">
<CardTitled title="FLOW" sub="owo" class="w-full p-4">
<div class="p-2 flex flex-col">
<button @click="test">
TEST
</button>
</div>
</CardTitled>
<div class="mt-8 overflow-y-auto px-4 flex flex-col gap-3">
</div>
</div>
</template>

View File

@@ -1373,12 +1373,21 @@ 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.4.28':
resolution: {integrity: sha512-B5uvZK0ArgBMkjK8RA9l5XP+PuQ/x99oqrcHRc78wa0pWyDje5X/isGihuiuSr0nFZTA5guoy78sJ6J8XxZv1A==}
'@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.4.28':
resolution: {integrity: sha512-Corp5aAn5cm9h2cse6w5vRlnlfpy8hBRrsgCzHSoUohStlbqBXvI/uopPVkCivPCgY4fJZhXOufYYJ3DXzpN/w==}
'@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.4.28':
resolution: {integrity: sha512-y9lDMMFf2Y5GpYdE8+IuavVl95D1GY1Zp8jU1vZhQ3Z4ga3f0Ym+XxRhcFtqaQAm9u82GwB7zDpBxafWDRq4pw==}
'@vue/server-renderer@3.4.27': '@vue/server-renderer@3.4.27':
resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==}
peerDependencies: peerDependencies:
@@ -1387,6 +1396,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.4.28':
resolution: {integrity: sha512-2b+Vuv5ichZQZPmRJfniHQkBSNigmRsRkr17bkYqBFy3J88T4lB7dRbAX/rx8qr9v0cr8Adg6yP872xhxGmh0w==}
'@vueuse/components@10.10.0': '@vueuse/components@10.10.0':
resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==} resolution: {integrity: sha512-HiA10NQ9HJAGnju+8ZK4TyA8LIc0a6BnJmVWDa/k+TRhaYCVacSDU04k0BQ2otV+gghUDdwu98upf6TDRXpoeg==}
@@ -6602,7 +6614,7 @@ snapshots:
'@volar/language-core': 1.11.1 '@volar/language-core': 1.11.1
'@volar/source-map': 1.11.1 '@volar/source-map': 1.11.1
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.28
computeds: 0.0.1 computeds: 0.0.1
minimatch: 9.0.4 minimatch: 9.0.4
muggle-string: 0.3.1 muggle-string: 0.3.1
@@ -6615,17 +6627,33 @@ snapshots:
dependencies: dependencies:
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
'@vue/reactivity@3.4.28':
dependencies:
'@vue/shared': 3.4.28
'@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.4.28':
dependencies:
'@vue/reactivity': 3.4.28
'@vue/shared': 3.4.28
'@vue/runtime-dom@3.4.27': '@vue/runtime-dom@3.4.27':
dependencies: dependencies:
'@vue/runtime-core': 3.4.27 '@vue/runtime-core': 3.4.27
'@vue/shared': 3.4.27 '@vue/shared': 3.4.27
csstype: 3.1.3 csstype: 3.1.3
'@vue/runtime-dom@3.4.28':
dependencies:
'@vue/reactivity': 3.4.28
'@vue/runtime-core': 3.4.28
'@vue/shared': 3.4.28
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))':
dependencies: dependencies:
'@vue/compiler-ssr': 3.4.27 '@vue/compiler-ssr': 3.4.27
@@ -6634,6 +6662,8 @@ snapshots:
'@vue/shared@3.4.27': {} '@vue/shared@3.4.27': {}
'@vue/shared@3.4.28': {}
'@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))':
dependencies: dependencies:
'@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.2)) '@vueuse/core': 10.10.0(vue@3.4.27(typescript@5.4.2))
@@ -10310,8 +10340,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.4.27 '@vue/runtime-core': 3.4.28
'@vue/runtime-dom': 3.4.27 '@vue/runtime-dom': 3.4.28
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

View File

@@ -2,8 +2,7 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { EventModel } from "@schema/metrics/EventSchema"; import { EventModel } from "@schema/metrics/EventSchema";
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService"; import { VisitModel } from "@schema/metrics/VisitSchema";
import { PipelineStage } from "mongoose";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -19,27 +18,59 @@ export default defineEventHandler(async event => {
const { name: eventName } = getQuery(event); const { name: eventName } = getQuery(event);
if (!eventName) return []; if (!eventName) return [];
const aggregation: PipelineStage[] = [
{ $match: { project_id: project._id, name: eventName } },
{ $group: { _id: "$flowHash", count: { $sum: 1 } } }, const allEvents = await EventModel.find({ project_id: project_id, name: eventName }, { flowHash: 1 });
{ $match: { _id: { $ne: null } } }, const allFlowHashes = new Map<string, number>();
{
$lookup: { allEvents.forEach(e => {
from: "visits", if (!e.flowHash) return;
let: { flowHash: "$_id" }, if (e.flowHash.length == 0) return;
pipeline: [ if (allFlowHashes.has(e.flowHash)) {
{ $match: { $expr: { $eq: ["$flowHash", "$$flowHash"] } } }, const count = allFlowHashes.get(e.flowHash) as number;
{ $group: { _id: "referrers", list: { $addToSet: "$referrer" } } }, allFlowHashes.set(e.flowHash, count + 1);
{ $limit: 1 } } else {
], allFlowHashes.set(e.flowHash, 1);
as: "referrers"
}
} }
]; });
const flow: { _id: string, count: number, referrers: [{ list: string[] }] }[] = await EventModel.aggregate(aggregation); const flowHashIds = Array.from(allFlowHashes.keys());
return flow; const allReferrers: { referrer: string, flowHash: string }[] = [];
const promises: any[] = [];
while (flowHashIds.length > 0) {
promises.push(new Promise<void>(async resolve => {
const flowHashIdsChunk = flowHashIds.splice(0, 10);
const visits = await VisitModel.find({ project_id, flowHash: { $in: flowHashIdsChunk } }, { referrer: 1, flowHash: 1 });
allReferrers.push(...visits.map(e => { return { referrer: e.referrer, flowHash: e.flowHash } }));
resolve();
}));
}
await Promise.all(promises);
const groupedFlows: Record<string, { referrers: string[] }> = {};
flowHashIds.forEach(flowHash => {
if (!groupedFlows[flowHash]) groupedFlows[flowHash] = { referrers: [] };
const target = groupedFlows[flowHash];
if (!target) return;
const referrers = allReferrers.filter(e => e.flowHash === flowHash).map(e => e.referrer);
for (const referrer of referrers) {
if (target.referrers.includes(referrer)) continue;
target.referrers.push(referrer);
}
});
const grouped: Record<string, number> = {};
for (const referrerPlusHash of allReferrers) {
const referrer = referrerPlusHash.referrer;
if (!grouped[referrer]) grouped[referrer] = 0
grouped[referrer]++;
}
return grouped;
}); });

View File

@@ -3,6 +3,9 @@ module.exports = {
content: [], content: [],
theme: { theme: {
extend: { extend: {
screens: {
"m-cards-wrap": '1830px'
},
fontSize: { fontSize: {
}, },

View File

@@ -21,7 +21,7 @@ export type TVisit = {
} }
const VisitSchema = new Schema<TVisit>({ const VisitSchema = new Schema<TVisit>({
project_id: { type: Schema.Types.ObjectId, index: 1 }, project_id: { type: Schema.Types.ObjectId, index: true },
browser: { type: String, required: true }, browser: { type: String, required: true },
os: { type: String, required: true }, os: { type: String, required: true },