This commit is contained in:
Emily
2024-10-07 15:26:57 +02:00
parent c2846ca595
commit b27cacf4e6
50 changed files with 512 additions and 583 deletions

2
TODO
View File

@@ -4,4 +4,4 @@
- Show more on Dashboard cards - Show more on Dashboard cards
- Fix devices Dashboard card, replace empty with "unkwnwn" - Fix devices Dashboard card, replace empty with "unkwnwn"
- Component first interaction must make the request inside - Component first interaction must make the request inside
- Reactivity on project delete - Reactivity on project delete (update dropdown)

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TProject } from '@schema/ProjectSchema';
import CreateSnapshot from './dialog/CreateSnapshot.vue'; import CreateSnapshot from './dialog/CreateSnapshot.vue';
export type Entry = { export type Entry = {
@@ -27,7 +26,8 @@ type Props = {
const route = useRoute(); const route = useRoute();
const props = defineProps<Props>(); const props = defineProps<Props>();
const { user, userRoles, setLoggedUser } = useLoggedUser() const { userRoles, setLoggedUser } = useLoggedUser();
const { projectList } = useProject();
const debugMode = process.dev; const debugMode = process.dev;
@@ -100,9 +100,6 @@ function onLogout() {
router.push('/login'); router.push('/login');
} }
const { projects } = useProjectsList();
const activeProject = useActiveProject();
const { data: maxProjects } = useFetch("/api/user/max_projects", { const { data: maxProjects } = useFetch("/api/user/max_projects", {
headers: computed(() => { headers: computed(() => {
return { return {
@@ -111,9 +108,6 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
}) })
}); });
const isPremium = computed(() => {
return activeProject.value?.premium;
})
const pricingDrawer = usePricingDrawer(); const pricingDrawer = usePricingDrawer();
@@ -147,7 +141,7 @@ const pricingDrawer = usePricingDrawer();
</div> </div>
<NuxtLink to="/project_creation" v-if="projects && (projects.length < (maxProjects || 1))" <NuxtLink to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer"> class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer">
<div><i class="fas fa-plus"></i></div> <div><i class="fas fa-plus"></i></div>
<div> Create new project </div> <div> Create new project </div>
@@ -256,7 +250,7 @@ const pricingDrawer = usePricingDrawer();
<div class="manrope grow"> <div class="manrope grow">
{{ entry.label }} {{ entry.label }}
</div> </div>
<div v-if="entry.premiumOnly && !isPremium" class="flex items-center"> <div v-if="entry.premiumOnly && !userRoles.isPremium" class="flex items-center">
<i class="fal fa-lock"></i> <i class="fal fa-lock"></i>
</div> </div>
</NuxtLink> </NuxtLink>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
const activeProject = useActiveProject(); const { project } = useProject();
const { createAlert } = useAlert(); const { createAlert } = useAlert();
import 'highlight.js/styles/stackoverflow-dark.css'; import 'highlight.js/styles/stackoverflow-dark.css';
@@ -18,7 +18,7 @@ onMounted(() => {
function copyProjectId() { function copyProjectId() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP'); if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText(activeProject.value?._id?.toString() || ''); navigator.clipboard.writeText(project.value?._id?.toString() || '');
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000); createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
} }
@@ -30,7 +30,7 @@ function copyScript() {
const createScriptText = () => { const createScriptText = () => {
return [ return [
'<script defer ', '<script defer ',
`data-project="${activeProject.value?._id}" `, `data-project="${project.value?._id}" `,
'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></', 'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></',
'script>' 'script>'
].join('') ].join('')
@@ -43,7 +43,7 @@ function copyScript() {
const scriptText = computed(() => { const scriptText = computed(() => {
return [ return [
`<script defer data-project="${activeProject.value?._id.toString()}"`, `<script defer data-project="${project.value?._id.toString()}"`,
`\nsrc="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js">\n<`, `\nsrc="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js">\n<`,
`/script>` `/script>`
].join(''); ].join('');
@@ -57,7 +57,7 @@ function reloadPage() {
<template> <template>
<div v-if="!firstInteraction && activeProject" class="mt-[5vh] flex flex-col"> <div v-if="!firstInteraction && project" class="mt-[5vh] flex flex-col">
<div class="flex gap-4 items-center justify-center"> <div class="flex gap-4 items-center justify-center">
<div class="animate-pulse w-[1.5rem] h-[1.5rem] bg-accent rounded-full"> </div> <div class="animate-pulse w-[1.5rem] h-[1.5rem] bg-accent rounded-full"> </div>
@@ -109,7 +109,7 @@ function reloadPage() {
<CardTitled class="h-full w-full" title="Project id" <CardTitled class="h-full w-full" title="Project id"
sub="This is the identifier for this project, used to forward data"> sub="This is the identifier for this project, used to forward data">
<div class="flex flex-col items-end"> <div class="flex flex-col items-end">
<div class="w-full text-[.9rem] text-[#acacac]"> {{ activeProject?._id }} </div> <div class="w-full text-[.9rem] text-[#acacac]"> {{ project?._id }} </div>
<LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton> <LyxUiButton type="secondary" @click="copyProjectId()"> Copy </LyxUiButton>
</div> </div>
</CardTitled> </CardTitled>

View File

@@ -4,13 +4,12 @@ import type { TProject } from '@schema/ProjectSchema';
const { user } = useLoggedUser() const { user } = useLoggedUser()
const { projectList, actions, project } = useProject(); const { projectList, guestProjectList, actions, project } = useProject();
const { data: guestProjects } = useGuestProjectsList()
const selectorProjects = computed(() => { const selectorProjects = computed(() => {
const result: TProject[] = []; const result: TProject[] = [];
if (projectList.value) result.push(...projectList.value); if (projectList.value) result.push(...projectList.value);
if (guestProjects.value) result.push(...guestProjects.value); if (guestProjectList.value) result.push(...guestProjectList.value);
return result; return result;
}); });

View File

@@ -77,7 +77,7 @@ const chartData = ref<ChartData<'doughnut'>>({
const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions }); const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions });
const activeProjectId = useActiveProjectId(); const { projectId } = useProject();
const { safeSnapshotDates } = useSnapshot(); const { safeSnapshotDates } = useSnapshot();
@@ -105,7 +105,7 @@ const headers = computed(() => {
'Authorization': authorizationHeaderComputed.value, 'Authorization': authorizationHeaderComputed.value,
'x-schema': 'events', 'x-schema': 'events',
'x-limit': "6", 'x-limit': "6",
'x-pid': activeProjectId.data.value || '' 'x-pid': projectId.value || ''
} }
}); });

View File

@@ -38,7 +38,7 @@ function showAnomalyInfoAlert() {
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start"> <div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div> <div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
<div class="poppins font-medium text-[1.2rem]"> {{ onlineUsers }} Online users</div> <div class="poppins font-medium text-[1.2rem]"> {{ onlineUsers.data }} Online users</div>
</div> </div>
<div class="grow"></div> <div class="grow"></div>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ChartData, ChartOptions } from 'chart.js'; import type { ChartData, ChartOptions } from 'chart.js';
import { defineChartComponent } from 'vue-chart-3'; import { defineChartComponent } from 'vue-chart-3';
registerChartComponents();
const FunnelChart = defineChartComponent('funnel', 'funnel'); const FunnelChart = defineChartComponent('funnel', 'funnel');
@@ -90,17 +89,13 @@ onMounted(async () => {
}); });
const activeProjectId = useActiveProjectId();
const { safeSnapshotDates } = useSnapshot();
const eventsCount = await useFetch<{ _id: string, count: number }[]>(`/api/data/query`, { const eventsCount = await useFetch<{ _id: string, count: number }[]>(`/api/data/query`, {
...signHeaders({ lazy: true, headers: useComputedHeaders({
'x-pid': activeProjectId.data.value || '', limit: 1000,
'x-schema': 'events', custom: {
'x-from': safeSnapshotDates.value.from, 'schema': 'events'
'x-to': safeSnapshotDates.value.to, }
'x-query-limit': '1000' })
}), lazy: true
}); });

View File

@@ -1,16 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
const activeProject = useActiveProject();
const eventNames = ref<string[]>([]); const eventNames = await useFetch<string[]>(`/api/data/events_data/names`, {
headers: useComputedHeaders()
});
const selectedEventName = ref<string>(); const selectedEventName = ref<string>();
const metadataFields = ref<string[]>([]); const metadataFields = ref<string[]>([]);
const selectedMetadataField = ref<string>(); const selectedMetadataField = ref<string>();
const metadataFieldGrouped = ref<any[]>([]); const metadataFieldGrouped = ref<any[]>([]);
onMounted(async () => {
eventNames.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/names`, signHeaders());
});
watch(selectedEventName, () => { watch(selectedEventName, () => {
getMetadataFields(); getMetadataFields();
@@ -21,7 +20,9 @@ watch(selectedMetadataField, () => {
}); });
async function getMetadataFields() { async function getMetadataFields() {
metadataFields.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_fields?name=${selectedEventName.value}`, signHeaders()); metadataFields.value = await $fetch<string[]>(`/api/metrics/events_data/metadata_fields?name=${selectedEventName.value}`, {
headers: useComputedHeaders().value
});
selectedMetadataField.value = undefined; selectedMetadataField.value = undefined;
currentSearchText.value = ""; currentSearchText.value = "";
} }
@@ -41,7 +42,9 @@ async function getMetadataFieldGrouped() {
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&'); const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
metadataFieldGrouped.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_field_group?${queryParamsString}`, signHeaders()); metadataFieldGrouped.value = await $fetch<string[]>(`/api/metrics/events_data/metadata_field_group?${queryParamsString}`, {
headers: useComputedHeaders().value
});
} }
@@ -75,7 +78,7 @@ const canSearch = computed(() => {
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full" <USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames" v-model="selectedEventName"> placeholder="Select an event" :options="eventNames.data.value || []" v-model="selectedEventName">
</USelectMenu> </USelectMenu>
<USelectMenu v-if="metadataFields.length > 0" searchable searchable-placeholder="Search a field..." <USelectMenu v-if="metadataFields.length > 0" searchable searchable-placeholder="Search a field..."

View File

@@ -6,7 +6,6 @@ import DateService, { type Slice } from '@services/DateService';
const props = defineProps<{ slice: Slice }>(); const props = defineProps<{ slice: Slice }>();
const slice = computed(() => props.slice); const slice = computed(() => props.slice);
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot() const { safeSnapshotDates } = useSnapshot()
const body = computed(() => { const body = computed(() => {
@@ -90,8 +89,10 @@ function onResponse(e: any) {
if (e.response.status != 500) errorData.value = { errored: false, text: '' } if (e.response.status != 500) errorData.value = { errored: false, text: '' }
} }
const eventsStackedData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events_stacked`, { const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
method: 'POST', body, lazy: true, immediate: false, transform: transformResponse, ...signHeaders(), method: 'POST', body, lazy: true, immediate: false,
transform: transformResponse,
headers: useComputedHeaders(),
onResponseError, onResponseError,
onResponse onResponse
}); });

View File

@@ -1,13 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
const activeProject = useActiveProject(); const eventNames = await useFetch<string[]>(`/api/data/events_data/names`, {
headers: useComputedHeaders()
});
const eventNames = ref<string[]>([]);
const selectedEventName = 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 userFlowData = ref<any>();
const analyzing = ref<boolean>(false); const analyzing = ref<boolean>(false);
@@ -26,7 +24,10 @@ async function getUserFlowData() {
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&'); const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
userFlowData.value = await $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?${queryParamsString}`, signHeaders()); userFlowData.value = await $fetch(`/api/data/events_data/flow_from_name?${queryParamsString}`, {
headers: useComputedHeaders().value
});
analyzing.value = false; analyzing.value = false;
} }
@@ -38,11 +39,12 @@ async function analyzeEvent() {
<template> <template>
<CardTitled title="Event User Flow" <CardTitled title="Event User Flow"
sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement." class="w-full p-4"> sub="Track your user's journey from external links to in-app events, maintaining a complete view of their path from entry to engagement."
class="w-full p-4">
<div class="p-2 flex flex-col gap-3"> <div class="p-2 flex flex-col gap-3">
<USelectMenu searchable searchable-placeholder="Search an event..." class="w-full" <USelectMenu searchable searchable-placeholder="Search an event..." class="w-full"
placeholder="Select an event" :options="eventNames" v-model="selectedEventName"> placeholder="Select an event" :options="eventNames.data.value || []" v-model="selectedEventName">
</USelectMenu> </USelectMenu>
<div v-if="selectedEventName && !analyzing" class="flex justify-center"> <div v-if="selectedEventName && !analyzing" class="flex justify-center">
<div @click="analyzeEvent()" <div @click="analyzeEvent()"

View File

@@ -4,17 +4,9 @@ import type { PricingCardProp } from './PricingCardGeneric.vue';
const { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', { const { data: planData, refresh: refreshPlanData } = useFetch('/api/project/plan', {
...signHeaders(), lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
lazy: true
}); });
const activeProject = useActiveProject();
watch(activeProject, () => {
refreshPlanData();
});
function getPricingsData() { function getPricingsData() {
const freePricing: PricingCardProp[] = [ const freePricing: PricingCardProp[] = [
@@ -190,15 +182,18 @@ function getPricingsData() {
return { freePricing, customPricing, slidePricings } return { freePricing, customPricing, slidePricings }
} }
const { projectId } = useProject();
const emits = defineEmits<{ const emits = defineEmits<{
(evt: 'onCloseClick'): void (evt: 'onCloseClick'): void
}>(); }>();
async function onLifetimeUpgradeClick() { async function onLifetimeUpgradeClick() {
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, { const res = await $fetch<string>(`/api/pay/create-onetime`, {
...signHeaders({ 'content-type': 'application/json' }), ...signHeaders({
'content-type': 'application/json',
'x-pid': projectId.value ?? ''
}),
method: 'POST', method: 'POST',
body: JSON.stringify({ planId: 2001 }) body: JSON.stringify({ planId: 2001 })
}) })
@@ -218,7 +213,8 @@ async function onLifetimeUpgradeClick() {
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col"> <div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
<PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().freePricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().slidePricings" :default-index="2">
</PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="getPricingsData().customPricing"></PricingCardGeneric>
</div> </div>

View File

@@ -2,7 +2,7 @@
import type { TApiSettings } from '@schema/ApiSettingsSchema'; import type { TApiSettings } from '@schema/ApiSettingsSchema';
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const { project } = useProject(); const { project, actions, projectList } = useProject();
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'pname', title: 'Name', text: 'Project name' }, { id: 'pname', title: 'Name', text: 'Project name' },
@@ -77,7 +77,10 @@ const canChange = computed(() => {
async function changeProjectName() { async function changeProjectName() {
await $fetch("/api/project/change_name", { await $fetch("/api/project/change_name", {
method: 'POST', method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }), ...signHeaders({
'Content-Type': 'application/json',
'x-pid': project.value?._id.toString() ?? ''
}),
body: JSON.stringify({ name: projectNameInputVal.value }) body: JSON.stringify({ name: projectNameInputVal.value })
}); });
location.reload(); location.reload();
@@ -92,14 +95,17 @@ async function deleteProject() {
await $fetch('/api/project/delete', { await $fetch('/api/project/delete', {
method: 'DELETE', method: 'DELETE',
...signHeaders({ 'Content-Type': 'application/json' }), ...signHeaders({
'Content-Type': 'application/json',
'x-pid': project.value?._id.toString() ?? ''
}),
body: JSON.stringify({ project_id: project.value._id.toString() }) body: JSON.stringify({ project_id: project.value._id.toString() })
}); });
const projectsList = useProjectsList()
await projectsList.refresh();
const firstProjectId = projectsList.data.value?.[0]?._id.toString(); await actions.refreshProjectsList()
const firstProjectId = projectList.value?.[0]?._id.toString();
if (firstProjectId) { if (firstProjectId) {
await setActiveProject(firstProjectId); await setActiveProject(firstProjectId);
} }

View File

@@ -3,16 +3,16 @@ import dayjs from 'dayjs';
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM'; import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM';
const activeProject = useActiveProject(); const { projectId, isGuest } = useProject();
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const { data: planData, refresh: planRefresh, pending: planPending } = useFetch('/api/project/plan', { const { data: planData, pending: planPending } = useFetch('/api/project/plan', {
...signHeaders(), lazy: true lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
}); });
const { data: customerAddress, refresh: refreshCustomerAddress } = useFetch(`/api/pay/${activeProject.value?._id.toString()}/customer_info`, { const { data: customerAddress } = useFetch(`/api/pay/customer_info`, {
...signHeaders(), lazy: true lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
}); });
const percent = computed(() => { const percent = computed(() => {
@@ -45,8 +45,8 @@ const prettyExpireDate = computed(() => {
}); });
const { data: invoices, refresh: invoicesRefresh, pending: invoicesPending } = useFetch(`/api/pay/${activeProject.value?._id.toString()}/invoices`, { const { data: invoices, pending: invoicesPending } = useFetch(`/api/pay/invoices`, {
...signHeaders(), lazy: true lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
}) })
function openInvoice(link: string) { function openInvoice(link: string) {
@@ -75,7 +75,7 @@ const entries: SettingsTemplateEntry[] = [
] ]
watch(customerAddress, () => { watch(customerAddress, () => {
console.log('UPDATE',customerAddress.value) console.log('UPDATE', customerAddress.value)
if (!customerAddress.value) return; if (!customerAddress.value) return;
currentBillingInfo.value = customerAddress.value; currentBillingInfo.value = customerAddress.value;
}); });
@@ -94,19 +94,20 @@ const { createAlert } = useAlert()
async function saveBillingInfo() { async function saveBillingInfo() {
try { try {
const res = await $fetch(`/api/pay/${activeProject.value?._id.toString()}/update_customer`, { const res = await $fetch(`/api/pay/update_customer`, {
method: 'POST', method: 'POST',
...signHeaders({ ...signHeaders({
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'x-pid': projectId.value ?? ''
}), }),
body: JSON.stringify(currentBillingInfo.value) body: JSON.stringify(currentBillingInfo.value)
}); });
createAlert('Customer updated','Customer updated successfully', 'far fa-check', 5000); createAlert('Customer updated', 'Customer updated successfully', 'far fa-check', 5000);
} catch(ex) { } catch (ex) {
createAlert('Error updating customer','An error occurred while updating the customer', 'far fa-error', 8000); createAlert('Error updating customer', 'An error occurred while updating the customer', 'far fa-error', 8000);
} }
} }

View File

@@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const activeProject = useActiveProject(); const { projectId } = useProject();
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const columns = [ const columns = [
{ key: 'me', label: '' }, { key: 'me', label: '' },
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
@@ -15,7 +14,9 @@ const columns = [
// { key: 'pending', label: 'Pending' }, // { key: 'pending', label: 'Pending' },
] ]
const { data: members, refresh: refreshMembers, pending: pendingMembers } = useFetch('/api/project/members/list', signHeaders()); const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', {
headers: useComputedHeaders({ useSnapshotDates: false })
});
const showAddMember = ref<boolean>(false); const showAddMember = ref<boolean>(false);
@@ -30,7 +31,10 @@ async function kickMember(email: string) {
await $fetch('/api/project/members/kick', { await $fetch('/api/project/members/kick', {
method: 'POST', method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }), ...signHeaders({
'Content-Type': 'application/json',
'x-pid': projectId.value ?? ''
}),
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
onResponseError({ request, response, options }) { onResponseError({ request, response, options }) {
alert(response.statusText); alert(response.statusText);
@@ -55,7 +59,10 @@ async function addMember() {
await $fetch('/api/project/members/add', { await $fetch('/api/project/members/add', {
method: 'POST', method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }), ...signHeaders({
'Content-Type': 'application/json',
'x-pid': projectId.value ?? ''
}),
body: JSON.stringify({ email: addMemberEmail.value }), body: JSON.stringify({ email: addMemberEmail.value }),
onResponseError({ request, response, options }) { onResponseError({ request, response, options }) {
alert(response.statusText); alert(response.statusText);
@@ -71,10 +78,6 @@ async function addMember() {
} }
watch(activeProject, () => {
refreshMembers();
})
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'add', title: 'Add member', text: 'Add new member to project' }, { id: 'add', title: 'Add member', text: 'Add new member to project' },
{ id: 'members', title: 'Members', text: 'Manage members of current project' }, { id: 'members', title: 'Members', text: 'Manage members of current project' },

View File

@@ -1,59 +0,0 @@
import type { TProject } from "@schema/ProjectSchema";
const projects = useFetch<TProject[]>('/api/project/list', {
key: 'projectslist', ...signHeaders()
});
export function useProjectsList() {
return { ...projects, projects: projects.data }
}
const guestProjects = useFetch<TProject[]>('/api/project/list_guest', {
key: 'guestProjectslist', ...signHeaders()
});
export function useGuestProjectsList() {
return { ...guestProjects, guestProjects: guestProjects.data }
}
const activeProjectId = useFetch<string>(`/api/user/active_project`, {
key: 'activeProjectId', ...signHeaders(),
});
export const isGuest = computed(() => {
if (!guestProjects.data.value) return false;
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
if (guestTarget) return true;
return false;
});
export function useActiveProjectId() {
return { ...activeProjectId, pid: activeProjectId.data }
}
export function useActiveProject() {
if (isLiveDemo()) {
const { data: liveDemoProject } = useLiveDemo();
return liveDemoProject;
}
return computed(() => {
if (!projects.data.value) return;
if (!activeProjectId.data.value) return;
const target = projects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
if (target) return target;
if (!guestProjects.data.value) return;
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
return guestTarget;
});
}
export async function setActiveProject(project_id: string) {
changingProject.value = true;
await new Promise(e => setTimeout(e, 500));
await $fetch<string>(`/api/user/set_active_project?project_id=${project_id}`, signHeaders());
await activeProjectId.refresh();
changingProject.value = false;
}
export const changingProject = ref<boolean>(false);

View File

@@ -1,19 +0,0 @@
export function createRequestOptions(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', sign: boolean, body?: Record<string, any>, headers: Record<string, string> = {}) {
const requestHeaders = sign ? signHeaders(headers) : headers;
let requestBody;
if (method === 'POST' || method == 'PUT' || method == 'PATCH') {
requestBody = body ? JSON.stringify(body) : undefined;
}
return {
method,
headers: requestHeaders,
body: requestBody
}
}

View File

@@ -1,169 +0,0 @@
import type { Slice } from "@services/DateService";
import DateService from "@services/DateService";
import type { MetricsCounts } from "~/server/api/metrics/[project_id]/counts";
import type { BrowsersAggregated } from "~/server/api/metrics/[project_id]/data/browsers";
import type { CountriesAggregated } from "~/server/api/metrics/[project_id]/data/countries";
import type { DevicesAggregated } from "~/server/api/metrics/[project_id]/data/devices";
import type { CustomEventsAggregated } from "~/server/api/metrics/[project_id]/data/events";
import type { OssAggregated } from "~/server/api/metrics/[project_id]/data/oss";
import type { ReferrersAggregated } from "~/server/api/metrics/[project_id]/data/referrers";
import type { VisitsWebsiteAggregated } from "~/server/api/metrics/[project_id]/data/websites";
import type { MetricsTimeline } from "~/server/api/metrics/[project_id]/timeline/generic";
export function useMetricsData() {
const activeProject = useActiveProject();
const metricsInfo = useFetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, {
...signHeaders(),
lazy: true
});
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());
// return metricsInfo;
// }
// export function useTimelineAdvanced<T>(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) {
// const response = useCustomFetch<T>(
// `/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
// () => signHeaders({ 'Content-Type': 'application/json' }).headers, {
// method: 'POST',
// getBody: () => createFromToBody({ slice: slice.value, ...customBody }),
// lazy: true,
// watchProps: [snapshot, slice]
// });
// return response;
// }
// export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers' | 'events_stacked', slice: Ref<Slice>) {
// return useTimelineAdvanced<{ _id: string, count: number }[]>(endpoint, slice);
// }
// export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) {
// return await useTimelineAdvanced<{ _id: string, count: number }[]>('referrers', slice, { referrer });
// }
// export function useEventsStackedTimeline(slice: Ref<Slice>) {
// return useTimelineAdvanced<{ _id: string, name: string, count: number }[]>('events_stacked', slice);
// }
// export async function useTimelineDataRaw(timelineEndpointName: string, slice: SliceName) {
// const activeProject = useActiveProject();
// const response = await $fetch<{ data: MetricsTimeline[], from: string, to: string }>(
// `/api/metrics/${activeProject.value?._id}/timeline/${timelineEndpointName}`, {
// method: 'POST',
// ...signHeaders({ 'Content-Type': 'application/json' }),
// body: JSON.stringify({ slice }),
// });
// return response;
// }
// export async function useTimelineData(timelineEndpointName: string, slice: SliceName) {
// const response = await useTimelineDataRaw(timelineEndpointName, slice);
// if (!response) return;
// const fixed = fixMetrics(response, slice);
// return fixed;
// }
// export function usePagesData(website: string, limit: number = 10) {
// const activeProject = useActiveProject();
// const res = useFetch<VisitsWebsiteAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/pages`, {
// ...signHeaders({
// 'x-query-limit': limit.toString(),
// 'x-website-name': website
// }),
// key: `pages_data:${website}:${limit}`,
// lazy: true
// });
// return res;
// }
// export function useWebsitesData(limit: number = 10) {
// const res = useCustomFetch<VisitsWebsiteAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/websites`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useEventsData(limit: number = 10) {
// const res = useCustomFetch<CustomEventsAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/events`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useReferrersData(limit: number = 10) {
// const res = useCustomFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useBrowsersData(limit: number = 10) {
// const res = useCustomFetch<BrowsersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useOssData(limit: number = 10) {
// const res = useCustomFetch<OssAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/oss`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useGeolocationData(limit: number = 10) {
// const res = useCustomFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }
// export function useDevicesData(limit: number = 10) {
// const res = useCustomFetch<DevicesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/devices`,
// () => signHeaders(createFromToHeaders({ 'x-query-limit': limit.toString() })).headers,
// { lazy: false, watchProps: [snapshot] }
// );
// return res;
// }

View File

@@ -7,6 +7,9 @@ const setLoggedUser = (authContext?: AuthContext) => {
loggedUser.value = authContext; loggedUser.value = authContext;
}; };
const isLogged = computed(() => {
return loggedUser.value?.logged;
})
function getUserRoles() { function getUserRoles() {
const isPremium = computed(() => { const isPremium = computed(() => {
@@ -17,6 +20,7 @@ function getUserRoles() {
if (!loggedUser.value?.logged) return false; if (!loggedUser.value?.logged) return false;
return loggedUser.value.user.roles.includes('ADMIN'); return loggedUser.value.user.roles.includes('ADMIN');
}); });
return { isPremium, isAdmin } return { isPremium, isAdmin }
} }
@@ -24,6 +28,7 @@ export const isAdminHidden = ref<boolean>(false);
export function useLoggedUser() { export function useLoggedUser() {
return { return {
isLogged,
user: loggedUser, user: loggedUser,
userRoles: getUserRoles(), userRoles: getUserRoles(),
setLoggedUser setLoggedUser

View File

@@ -1,20 +1,15 @@
const onlineUsers = ref<number>(-1); const onlineUsers = await useFetch<number>(`/api/data/live_users`, {
headers: useComputedHeaders({ useSnapshotDates: false }), immediate: false
async function getOnlineUsers() { });
const activeProject = useActiveProject();
if (!activeProject.value) return onlineUsers.value = -1;
const online = await $fetch<number>(`/api/metrics/${activeProject.value._id}/live_users`, signHeaders());
onlineUsers.value = online;
}
let watching: any; let watching: any;
function startWatching(instant: boolean = true) { function startWatching(instant: boolean = true) {
if (instant) getOnlineUsers(); if (instant) onlineUsers.execute();
watching = setInterval(async () => { watching = setInterval(async () => {
await getOnlineUsers(); onlineUsers.refresh();
}, 20000); }, 20000);
} }
@@ -26,7 +21,6 @@ export function useOnlineUsers() {
return { return {
onlineUsers, onlineUsers,
getOnlineUsers,
startWatching, startWatching,
stopWatching stopWatching
} }

View File

@@ -13,7 +13,26 @@ const projectsRequest = useFetch<TProject[]>('/api/project/list', {
}) })
}); });
const projectList = computed(() => projectsRequest.data.value); const guestProjectsRequest = useFetch<TProject[]>('/api/project/list_guest', {
headers: computed(() => {
return {
'Authorization': `Bearer ${token.value}`
}
})
});
const projectList = computed(() => {
return projectsRequest.data.value;
});
const allProjectList = computed(() => {
return [...(projectsRequest.data.value || []), ...(guestProjectsRequest.data.value || [])]
})
const guestProjectList = computed(() => {
return guestProjectsRequest.data.value;
})
const refreshProjectsList = () => projectsRequest.refresh(); const refreshProjectsList = () => projectsRequest.refresh();
const activeProjectId = ref<string | undefined>(); const activeProjectId = ref<string | undefined>();
@@ -42,6 +61,11 @@ const project = computed(() => {
return projectList.value[0]; return projectList.value[0];
}) })
const isGuest = computed(() => {
return (projectList.value || []).find(e => e._id.toString() === activeProjectId.value);
})
export function useProject() { export function useProject() {
const actions = { const actions = {
@@ -49,5 +73,5 @@ export function useProject() {
setActiveProject setActiveProject
} }
return { project, projectList, actions, projectId: activeProjectId } return { project, allProjectList, guestProjectList, projectList, actions, projectId: activeProjectId, isGuest }
} }

View File

@@ -1,31 +1,37 @@
import type { TProjectSnapshot } from "@schema/ProjectSnapshot"; import type { TProjectSnapshot } from "@schema/ProjectSnapshot";
const { projectId, project } = useProject();
const headers = computed(() => {
return {
'Authorization': signHeaders().headers.Authorization,
'x-pid': projectId.value ?? ''
}
});
const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', { const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
...signHeaders(), headers
immediate: false
}); });
const activeProject = useActiveProject(); watch(project, async () => {
watch(activeProject, async () => {
await remoteSnapshots.refresh(); await remoteSnapshots.refresh();
snapshot.value = isLiveDemo() ? snapshots.value[0] : snapshots.value[1]; snapshot.value = isLiveDemo() ? snapshots.value[0] : snapshots.value[1];
}); });
const snapshots = computed(() => { const snapshots = computed(() => {
const activeProject = useActiveProject();
const getDefaultSnapshots: () => TProjectSnapshot[] = () => [ const getDefaultSnapshots: () => TProjectSnapshot[] = () => [
{ {
project_id: activeProject.value?._id as any, project_id: project.value?._id as any,
_id: 'default0' as any, _id: 'default0' as any,
name: 'All', name: 'All',
from: new Date(activeProject.value?.created_at || 0), from: new Date(project.value?.created_at || 0),
to: new Date(Date.now()), to: new Date(Date.now()),
color: '#CCCCCC' color: '#CCCCCC'
}, },
{ {
project_id: activeProject.value?._id as any, project_id: project.value?._id as any,
_id: 'default1' as any, _id: 'default1' as any,
name: 'Last month', name: 'Last month',
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
@@ -33,7 +39,7 @@ const snapshots = computed(() => {
color: '#00CC00' color: '#00CC00'
}, },
{ {
project_id: activeProject.value?._id as any, project_id: project.value?._id as any,
_id: 'default2' as any, _id: 'default2' as any,
name: 'Last week', name: 'Last week',
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
@@ -41,7 +47,7 @@ const snapshots = computed(() => {
color: '#0F02D2' color: '#0F02D2'
}, },
{ {
project_id: activeProject.value?._id as any, project_id: project.value?._id as any,
_id: 'default3' as any, _id: 'default3' as any,
name: 'Last day', name: 'Last day',
from: new Date(Date.now() - 1000 * 60 * 60 * 24), from: new Date(Date.now() - 1000 * 60 * 60 * 24),
@@ -75,8 +81,5 @@ const snapshotDuration = computed(() => {
}); });
export function useSnapshot() { export function useSnapshot() {
if (remoteSnapshots.status.value === 'idle') {
remoteSnapshots.execute();
}
return { snapshot, snapshots, safeSnapshotDates, updateSnapshots, snapshotDuration } return { snapshot, snapshots, safeSnapshotDates, updateSnapshots, snapshotDuration }
} }

View File

@@ -4,10 +4,8 @@ import type { Section } from '~/components/CVerticalNavigation.vue';
import { Lit } from 'litlyx-js'; import { Lit } from 'litlyx-js';
const activeProject = useActiveProject(); const { userRoles, isLogged } = useLoggedUser();
const isPremium = computed(() => { const { project } = useProject();
return activeProject.value?.premium;
});
const pricingDrawer = usePricingDrawer(); const pricingDrawer = usePricingDrawer();
@@ -19,9 +17,9 @@ const sections: Section[] = [
{ label: 'Events', to: '/events', icon: 'fal fa-square-bolt' }, { label: 'Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' }, { label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
{ label: 'Security', to: '/security', icon: 'fal fa-shield' }, { label: 'Security', to: '/security', icon: 'fal fa-shield' },
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true }, // { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true }, // { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
{ label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true }, // { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' }, { label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{ {
grow: true, grow: true,
@@ -33,8 +31,8 @@ const sections: Section[] = [
to: '#', to: '#',
premiumOnly: true, premiumOnly: true,
action() { action() {
if (isGuest.value === true) return; if (isLogged.value === true) return;
if (isPremium.value === true) { if (userRoles.isPremium.value === true) {
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank'); window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
} else { } else {
pricingDrawer.visible.value = true; pricingDrawer.visible.value = true;

View File

@@ -4,17 +4,16 @@ import VueMarkdown from 'vue-markdown-render';
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject(); const { project, isGuest } = useProject();
const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${activeProject.value?._id}/chats_list`, { const { data: chatsList, refresh: reloadChatsList } = useFetch(`/api/ai/${project.value?._id}/chats_list`, {
...signHeaders() ...signHeaders()
}); });
const viewChatsList = computed(() => (chatsList.value || []).toReversed()); const viewChatsList = computed(() => (chatsList.value || []).toReversed());
const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${activeProject.value?._id}/chats_remaining`, signHeaders()); const { data: chatsRemaining, refresh: reloadChatsRemaining } = useFetch(`/api/ai/${project.value?._id}/chats_remaining`, signHeaders());
const currentText = ref<string>(""); const currentText = ref<string>("");
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
@@ -27,7 +26,7 @@ const scroller = ref<HTMLDivElement | null>(null);
async function sendMessage() { async function sendMessage() {
if (loading.value) return; if (loading.value) return;
if (!activeProject.value) return; if (!project.value) return;
loading.value = true; loading.value = true;
@@ -42,7 +41,7 @@ async function sendMessage() {
try { try {
const res = await $fetch(`/api/ai/${activeProject.value._id.toString()}/send_message`, { const res = await $fetch(`/api/ai/${project.value._id.toString()}/send_message`, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
...signHeaders({ 'Content-Type': 'application/json' }) ...signHeaders({ 'Content-Type': 'application/json' })
@@ -74,7 +73,7 @@ async function sendMessage() {
async function openChat(chat_id?: string) { async function openChat(chat_id?: string) {
menuOpen.value = false; menuOpen.value = false;
if (!activeProject.value) return; if (!project.value) return;
currentChatMessages.value = []; currentChatMessages.value = [];
@@ -83,7 +82,7 @@ async function openChat(chat_id?: string) {
return; return;
} }
currentChatId.value = chat_id; currentChatId.value = chat_id;
const messages = await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/get_messages`, signHeaders()); const messages = await $fetch(`/api/ai/${project.value._id}/${chat_id}/get_messages`, signHeaders());
if (!messages) return; if (!messages) return;
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any; currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
@@ -117,14 +116,14 @@ const defaultPrompts = [
] ]
async function deleteChat(chat_id: string) { async function deleteChat(chat_id: string) {
if (!activeProject.value) return; if (!project.value) return;
const sure = confirm("Are you sure to delete the chat ?"); const sure = confirm("Are you sure to delete the chat ?");
if (!sure) return; if (!sure) return;
if (currentChatId.value === chat_id) { if (currentChatId.value === chat_id) {
currentChatId.value = ""; currentChatId.value = "";
currentChatMessages.value = []; currentChatMessages.value = [];
} }
await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/delete`, signHeaders()); await $fetch(`/api/ai/${project.value._id}/${chat_id}/delete`, signHeaders());
await reloadChatsList(); await reloadChatsList();
} }

View File

@@ -10,12 +10,9 @@ const selectLabelsEvents = [
]; ];
const eventsStackedSelectIndex = ref<number>(0); const eventsStackedSelectIndex = ref<number>(0);
const activeProject = useActiveProject(); const { projectId } = useProject();
const { snapshot, safeSnapshotDates } = useSnapshot(); const { snapshot, safeSnapshotDates } = useSnapshot();
const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`);
const headers = computed(() => { const headers = computed(() => {
return { return {
@@ -23,7 +20,7 @@ const headers = computed(() => {
'x-to': safeSnapshotDates.value.to, 'x-to': safeSnapshotDates.value.to,
'Authorization': authorizationHeaderComputed.value, 'Authorization': authorizationHeaderComputed.value,
'x-schema': 'events', 'x-schema': 'events',
'x-pid': activeProject.value?._id.toString() || '' 'x-pid': projectId.value ?? ''
} }
}); });

View File

@@ -0,0 +1,92 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id, from, to } = data;
const { name: eventName } = getQuery(event);
if (!eventName) return setResponseStatus(event, 400, 'name is required');
const allEvents = await EventModel.find({
project_id: project_id,
name: eventName,
created_at: {
$gte: new Date(from),
$lte: new Date(to),
}
}, { flowHash: 1 });
const allFlowHashes = new Map<string, number>();
allEvents.forEach(e => {
if (!e.flowHash) return;
if (e.flowHash.length == 0) return;
if (allFlowHashes.has(e.flowHash)) {
const count = allFlowHashes.get(e.flowHash) as number;
allFlowHashes.set(e.flowHash, count + 1);
} else {
allFlowHashes.set(e.flowHash, 1);
}
});
const flowHashIds = Array.from(allFlowHashes.keys());
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]++;
}
const eventsCount = allEvents.length;
const allGroupedValue = Object.keys(grouped)
.map(key => grouped[key])
.reduce((a, e) => a + e, 0);
for (const key in grouped) {
grouped[key] = 100 / allGroupedValue * grouped[key];
}
return grouped;
});

View File

@@ -0,0 +1,43 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { EventModel } from "@schema/metrics/EventSchema";
import { EVENT_METADATA_FIELDS_EXPIRE_TIME, Redis } from "~/server/services/CacheService";
import { PipelineStage } from "mongoose";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { name: eventName, field, from, to } = getQuery(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!eventName) return setResponseStatus(event, 400, 'name is required');
if (!field) return setResponseStatus(event, 400, 'field is required');
const aggregation: PipelineStage[] = [
{
$match: {
project_id, name: eventName,
created_at: {
$gte: new Date(from.toString()),
$lte: new Date(to.toString()),
}
}
},
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]
const metadataGrouped = await EventModel.aggregate(aggregation);
return metadataGrouped;
});

View File

@@ -0,0 +1,32 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const { name: eventName } = getQuery(event);
if (!eventName) return [];
const fields: string[] = await Redis.useCache({
key: `metadata_fields:${project_id}:${eventName}`,
exp: 60
}, async () => {
const eventsWithName = await EventModel.find({ project_id, name: eventName }, { metadata: 1 }, { limit: 10, sort: { created_at: -1 } });
const allMetadata = eventsWithName.map(e => e.metadata);
const allFields = new Set<string>();
for (const metadata of allMetadata) {
const keys = Object.keys(metadata || {});
keys.forEach(key => allFields.add(key));
}
return Array.from(allFields.values());
});
return fields;
});

View File

@@ -0,0 +1,26 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const names: string[] = await Redis.useCache({
key: `event_names:${project_id}`,
exp: 60
}, async () => {
const namesAggregation = await EventModel.aggregate([
{ $match: { project_id } },
{ $group: { _id: "$name" } }
]);
return namesAggregation.map(e => e._id);
});
return names;
});

View File

@@ -0,0 +1,25 @@
import { SessionModel } from "@schema/metrics/SessionSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { project_id } = data;
const online_users = await SessionModel.aggregate([
{
$match: {
project_id,
updated_at: { $gt: new Date(Date.now() - 1000 * 60 * 5) }
}
},
{ $count: 'count' }
]);
if (!online_users[0]) return 0;
return online_users[0].count;
});

View File

@@ -1,21 +0,0 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return;
if (!project.customer_id) return;
const customer = await StripeService.getCustomer(project.customer_id);
if (customer?.deleted) return;
return customer?.address;
});

View File

@@ -5,16 +5,10 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!project_id) return; if (!data) return;
const user = getRequestUser(event); const { project, pid } = data;
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
const body = await readBody(event); const body = await readBody(event);
@@ -30,7 +24,7 @@ export default defineEventHandler(async event => {
const intent = await StripeService.createOnetimePayment( const intent = await StripeService.createOnetimePayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok', 'https://dashboard.litlyx.com/payment_ok',
project_id, pid,
project.customer_id project.customer_id
) )

View File

@@ -1,20 +1,13 @@
import { getPlanFromId } from "@data/PREMIUM"; import { getPlanFromId } from "@data/PREMIUM";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService'; import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!project_id) return; if (!data) return;
const user = getRequestUser(event); const { project, pid } = data;
if (!user?.logged) return setResponseStatus(event, 400, 'User need to be logged');
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
if (project.owner.toString() != user.id) return setResponseStatus(event, 400, 'You cannot upgrade a project as guest');
const body = await readBody(event); const body = await readBody(event);
@@ -30,7 +23,7 @@ export default defineEventHandler(async event => {
const checkout = await StripeService.createPayment( const checkout = await StripeService.createPayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok', 'https://dashboard.litlyx.com/payment_ok',
project_id, pid,
project.customer_id project.customer_id
); );

View File

@@ -0,0 +1,16 @@
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const { project } = data;
const customer = await StripeService.getCustomer(project.customer_id);
if (customer?.deleted) return;
return customer?.address;
});

View File

@@ -12,17 +12,14 @@ export type InvoiceData = {
} }
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!data) return;
const project_id = getRequestProjectId(event); const { project, pid } = data;
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return;
if (!project.customer_id) return []; if (!project.customer_id) return [];
return await Redis.useCache({ key: `invoices:${project_id}`, exp: 10 }, async () => { return await Redis.useCache({ key: `invoices:${pid}`, exp: 10 }, async () => {
const invoices = await StripeService.getInvoices(project.customer_id); const invoices = await StripeService.getInvoices(project.customer_id);
if (!invoices) return []; if (!invoices) return [];

View File

@@ -4,12 +4,10 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event); const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!project_id) return setResponseStatus(event, 400, 'Cannot get project_id'); if (!data) return;
const user = getRequestUser(event); const { project } = data;
const project = await getUserProjectFromId(project_id, user, false);
if (!project) return setResponseStatus(event, 400, 'Cannot get user from project_id');
if (!project.customer_id) return setResponseStatus(event, 400, 'Project has no customer_id'); if (!project.customer_id) return setResponseStatus(event, 400, 'Project has no customer_id');

View File

@@ -1,24 +1,10 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const { project } = data;
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { name } = await readBody(event); const { name } = await readBody(event);

View File

@@ -8,19 +8,12 @@ import { AiChatModel } from "@schema/ai/AiChatSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const body = await readBody(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!data) return;
const project_id = body.project_id; const { project, user, project_id } = data;
const userData = getRequestUser(event); const projects = await ProjectModel.countDocuments({ owner: user.id });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not exist');
if (userData.id != project.owner.toString()) return setResponseStatus(event, 400, 'You cannot delete a project as guest');
const projects = await ProjectModel.countDocuments({ owner: userData.id });
if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project'); if (projects == 1) return setResponseStatus(event, 400, 'Cannot delete last project');
if (project.premium === true) return setResponseStatus(event, 400, 'Cannot delete premium project'); if (project.premium === true) return setResponseStatus(event, 400, 'Cannot delete premium project');
@@ -29,26 +22,22 @@ export default defineEventHandler(async event => {
await StripeService.deleteCustomer(project.customer_id); await StripeService.deleteCustomer(project.customer_id);
} }
const projectDeletation = await ProjectModel.deleteOne({ _id: project_id }); const projectDeletation = ProjectModel.deleteOne({ _id: project_id });
const countDeletation = await ProjectCountModel.deleteMany({ project_id }); const countDeletation = ProjectCountModel.deleteMany({ project_id });
const limitdeletation = await ProjectLimitModel.deleteMany({ project_id }); const limitdeletation = ProjectLimitModel.deleteMany({ project_id });
const sessionsDeletation = await SessionModel.deleteMany({ project_id }); const sessionsDeletation = SessionModel.deleteMany({ project_id });
const notifiesDeletation = await LimitNotifyModel.deleteMany({ project_id }); const notifiesDeletation = LimitNotifyModel.deleteMany({ project_id });
const aiChatsDeletation = await AiChatModel.deleteMany({ project_id }); const aiChatsDeletation = AiChatModel.deleteMany({ project_id });
const ok = countDeletation.acknowledged && limitdeletation.acknowledged && const results = await Promise.all([
projectDeletation.acknowledged && sessionsDeletation.acknowledged && notifiesDeletation.acknowledged && aiChatsDeletation.acknowledged projectDeletation,
countDeletation,
limitdeletation,
sessionsDeletation,
notifiesDeletation,
aiChatsDeletation
])
return { return { data: results };
ok,
data: [
countDeletation.acknowledged,
limitdeletation.acknowledged,
projectDeletation.acknowledged,
sessionsDeletation.acknowledged,
notifiesDeletation.acknowledged,
aiChatsDeletation.acknowledged
]
};
}); });

View File

@@ -1,24 +1,14 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema"; import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema"; import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project'); if (!data) return;
const project_id = currentActiveProject.active_project_id; const { project_id } = data;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { email } = await readBody(event); const { email } = await readBody(event);

View File

@@ -1,24 +1,14 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema"; import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema"; import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const { project_id } = data;
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { email } = await readBody(event); const { email } = await readBody(event);

View File

@@ -1,22 +1,15 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema"; import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const { project_id, user } = data;
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id; await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
await TeamMemberModel.deleteOne({ project_id, user_id: userData.id });
return { ok: true } return { ok: true }

View File

@@ -6,16 +6,10 @@ import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const { project_id, project, user } = data;
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const owner = await UserModel.findById(project.owner); const owner = await UserModel.findById(project.owner);
if (!owner) return setResponseStatus(event, 400, 'No owner'); if (!owner) return setResponseStatus(event, 400, 'No owner');
@@ -29,7 +23,7 @@ export default defineEventHandler(async event => {
name: owner.name, name: owner.name,
role: 'OWNER', role: 'OWNER',
pending: false, pending: false,
me: userData.id === owner.id me: user.id === owner.id
}) })
for (const member of members) { for (const member of members) {
@@ -40,7 +34,7 @@ export default defineEventHandler(async event => {
name: userMember.name, name: userMember.name,
role: member.role, role: member.role,
pending: member.pending, pending: member.pending,
me: userData.id === userMember.id me: user.id === userMember.id
}) })
} }

View File

@@ -1,21 +1,12 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { ProjectLimitModel } from "@schema/ProjectsLimits"; import { ProjectLimitModel } from "@schema/ProjectsLimits";
import { UserSettingsModel } from "@schema/UserSettings";
import StripeService from '~/server/services/StripeService'; import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const { project, project_id } = data;
if (project.subscription_id === 'onetime') { if (project.subscription_id === 'onetime') {

View File

@@ -1,16 +1,12 @@
import { ProjectSnapshotModel, TProjectSnapshot } from "@schema/ProjectSnapshot"; import { ProjectSnapshotModel, TProjectSnapshot } from "@schema/ProjectSnapshot";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userData = getRequestUser(event); const data = await getRequestData(event, { requireSchema: false, allowLitlyx: false });
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); if (!data) return;
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); const { project_id } = data;
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const snapshots = await ProjectSnapshotModel.find({ project_id }); const snapshots = await ProjectSnapshotModel.find({ project_id });

View File

@@ -14,7 +14,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data; const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:bouncing_rate:${pid}:${from}:${to}`; const cacheKey = `timeline:bouncing_rate:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60 * 60; //1 hour const cacheExp = 60 * 60; //1 hour
return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => { return await Redis.useCacheV2(cacheKey, cacheExp, async (noStore, updateExp) => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data; const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:events:${pid}:${from}:${to}`; const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -0,0 +1,28 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { executeAdvancedTimelineAggregation} from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
if (!data) return;
const { from, to, slice, project_id } = data;
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
const timelineStackedEvents = await executeAdvancedTimelineAggregation<{ name: String }>({
model: EventModel,
projectId: project_id,
from, to, slice,
customProjection: { name: "$_id.name" },
customIdGroup: { name: '$name' },
})
// const filledDates = DateService.createBetweenDates(from, to, slice);
// const merged = DateService.mergeFilledDates(filledDates.dates, timelineStackedEvents, '_id', slice, { count: 0, name: '' });
return timelineStackedEvents;
});
});

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data; const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:sessions:${pid}:${from}:${to}`; const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data; const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:sessions_duration:${pid}:${from}:${to}`; const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -9,7 +9,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, slice, project_id } = data; const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:visits:${pid}:${from}:${to}`; const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -96,6 +96,10 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, option
if (!allowLitlyx) return setResponseStatus(event, 400, 'no access to project'); if (!allowLitlyx) return setResponseStatus(event, 400, 'no access to project');
} }
return { from, to, pid, project_id, project, user, limit, slice, schemaName, model } return {
from: from as string,
to: to as string,
pid, project_id, project, user, limit, slice, schemaName, model
}
} }