mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
.
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import DateService, { type Slice } from '@services/DateService';
|
||||||
|
|
||||||
const data = ref<number[]>([]);
|
const data = ref<number[]>([]);
|
||||||
const labels = ref<string[]>([]);
|
const labels = ref<string[]>([]);
|
||||||
const ready = ref<boolean>(false);
|
const ready = ref<boolean>(false);
|
||||||
const key = ref<string>('0');
|
|
||||||
|
|
||||||
const props = defineProps<{ slice: SliceName }>();
|
const props = defineProps<{ slice: Slice }>();
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const response = await useTimelineData('sessions', props.slice);
|
const response = await useTimeline('sessions', props.slice);
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
data.value = response.data;
|
data.value = response.map(e => e.count);
|
||||||
labels.value = response.labels;
|
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
key.value = Date.now().toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
import DateService, { type Slice } from '@services/DateService';
|
||||||
|
|
||||||
const data = ref<number[]>([]);
|
const data = ref<number[]>([]);
|
||||||
const labels = ref<string[]>([]);
|
const labels = ref<string[]>([]);
|
||||||
const ready = ref<boolean>(false);
|
const ready = ref<boolean>(false);
|
||||||
|
|
||||||
const props = defineProps<{ slice: SliceName }>();
|
const props = defineProps<{ slice: Slice }>();
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const response = await useVisitsTimeline(props.slice);
|
const response = await useTimeline('visits', props.slice);
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
const fixed = fixMetrics(response, props.slice);
|
data.value = response.map(e => e.count);
|
||||||
console.log(fixed);
|
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
|
||||||
data.value = fixed.data;
|
|
||||||
labels.value = fixed.labels;
|
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function useFirstInteractionData() {
|
|||||||
return metricsInfo;
|
return metricsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function useVisitsTimeline(slice: Slice, fromDate?: string, toDate?: string) {
|
export async function useTimeline(endpoint: 'visits' | 'sessions', slice: Slice, fromDate?: string, toDate?: string) {
|
||||||
|
|
||||||
const { from, to } = DateService.prepareDateRange(
|
const { from, to } = DateService.prepareDateRange(
|
||||||
fromDate || DateService.getDefaultRange(slice).from,
|
fromDate || DateService.getDefaultRange(slice).from,
|
||||||
@@ -26,13 +26,13 @@ export async function useVisitsTimeline(slice: Slice, fromDate?: string, toDate?
|
|||||||
|
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
const response = await $fetch(
|
const response = await $fetch(
|
||||||
`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||||
body: JSON.stringify({ slice, from, to })
|
body: JSON.stringify({ slice, from, to })
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response as { _id: string, count: number }[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ watch(pending, () => {
|
|||||||
|
|
||||||
const selectLabels = [
|
const selectLabels = [
|
||||||
{ label: 'Hour', value: 'hour' },
|
{ label: 'Hour', value: 'hour' },
|
||||||
{ label: 'Day', value: 'day' }
|
{ label: 'Day', value: 'day' },
|
||||||
|
// { label: 'Month', value: 'month' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getTimeline } from "./generic";
|
|||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||||
|
import DateService from "@services/DateService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
const project_id = getRequestProjectId(event);
|
const project_id = getRequestProjectId(event);
|
||||||
@@ -12,11 +12,37 @@ export default defineEventHandler(async event => {
|
|||||||
const project = await getUserProjectFromId(project_id, user);
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const { slice, duration } = await readBody(event);
|
|
||||||
|
|
||||||
return await Redis.useCache({ key: `timeline:sessions:${project_id}:${slice}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
|
const { slice, from, to } = await readBody(event);
|
||||||
const timelineSessions = await getTimeline(SessionModel, project_id, slice, duration);
|
|
||||||
return timelineSessions;
|
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||||
|
if (!from) return setResponseStatus(event, 400, 'to is required');
|
||||||
|
if (!from) return setResponseStatus(event, 400, 'slice is required');
|
||||||
|
|
||||||
|
return await Redis.useCache({
|
||||||
|
key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
|
||||||
|
exp: TIMELINE_EXPIRE_TIME
|
||||||
|
}, async () => {
|
||||||
|
|
||||||
|
const { group, sort, fromParts } = DateService.getQueryDateRange(slice);
|
||||||
|
|
||||||
|
const aggregation = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id: project._id,
|
||||||
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $group: { _id: group, count: { $sum: 1 } } },
|
||||||
|
{ $sort: sort },
|
||||||
|
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count" } }
|
||||||
|
]
|
||||||
|
|
||||||
|
const timelineVisits: { _id: string, count: number }[] = await SessionModel.aggregate(aggregation);
|
||||||
|
const filledDates = DateService.fillDates(timelineVisits.map(e => e._id), slice);
|
||||||
|
const merged = DateService.mergeFilledDates(filledDates, timelineVisits, '_id', slice, { count: 0 });
|
||||||
|
return merged;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getTimeline } from "./generic";
|
|
||||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||||
|
import DateService from "@services/DateService";
|
||||||
|
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
@@ -12,11 +12,37 @@ export default defineEventHandler(async event => {
|
|||||||
const project = await getUserProjectFromId(project_id, user);
|
const project = await getUserProjectFromId(project_id, user);
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const { slice, duration } = await readBody(event);
|
|
||||||
|
|
||||||
return await Redis.useCache({ key: `timeline:visits:${project_id}:${slice}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
|
const { slice, from, to } = await readBody(event);
|
||||||
const timelineVisits = await getTimeline(VisitModel, project_id, slice, duration);
|
|
||||||
return timelineVisits;
|
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||||
|
if (!from) return setResponseStatus(event, 400, 'to is required');
|
||||||
|
if (!from) return setResponseStatus(event, 400, 'slice is required');
|
||||||
|
|
||||||
|
return await Redis.useCache({
|
||||||
|
key: `timeline:visits:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
|
||||||
|
exp: TIMELINE_EXPIRE_TIME
|
||||||
|
}, async () => {
|
||||||
|
|
||||||
|
const { group, sort, fromParts } = DateService.getQueryDateRange(slice);
|
||||||
|
|
||||||
|
const aggregation = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
project_id: project._id,
|
||||||
|
created_at: { $gte: new Date(from), $lte: new Date(to) },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $group: { _id: group, count: { $sum: 1 } } },
|
||||||
|
{ $sort: sort },
|
||||||
|
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count" } }
|
||||||
|
]
|
||||||
|
|
||||||
|
const timelineVisits: { _id: string, count: number }[] = await VisitModel.aggregate(aggregation);
|
||||||
|
const filledDates = DateService.fillDates(timelineVisits.map(e => e._id), slice);
|
||||||
|
const merged = DateService.mergeFilledDates(filledDates, timelineVisits, '_id', slice, { count: 0 });
|
||||||
|
return merged;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class Redis {
|
|||||||
url: runtimeConfig.REDIS_URL,
|
url: runtimeConfig.REDIS_URL,
|
||||||
username: runtimeConfig.REDIS_USERNAME,
|
username: runtimeConfig.REDIS_USERNAME,
|
||||||
password: runtimeConfig.REDIS_PASSWORD,
|
password: runtimeConfig.REDIS_PASSWORD,
|
||||||
|
database: process.dev ? 1 : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
static async init() {
|
static async init() {
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ class DateService {
|
|||||||
|
|
||||||
public slicesData = slicesData;
|
public slicesData = slicesData;
|
||||||
|
|
||||||
|
getChartLabelFromISO(iso: string, locale: string, slice: Slice) {
|
||||||
|
const date = dayjs(iso).locale(locale);
|
||||||
|
if (slice === 'hour') return date.format('HH:mm')
|
||||||
|
if (slice === 'day') return date.format('DD/MM')
|
||||||
|
return date.format();
|
||||||
|
}
|
||||||
|
|
||||||
getDefaultRange(slice: Slice) {
|
getDefaultRange(slice: Slice) {
|
||||||
return {
|
return {
|
||||||
from: new Date(Date.now() - slicesData[slice].fromOffset).toISOString(),
|
from: new Date(Date.now() - slicesData[slice].fromOffset).toISOString(),
|
||||||
@@ -39,22 +46,39 @@ class DateService {
|
|||||||
switch (slice) {
|
switch (slice) {
|
||||||
case 'hour':
|
case 'hour':
|
||||||
group.hour = { $hour: '$created_at' }
|
group.hour = { $hour: '$created_at' }
|
||||||
sort['_id.hour'] = 1;
|
|
||||||
fromParts.hour = "$_id.hour";
|
fromParts.hour = "$_id.hour";
|
||||||
case 'day':
|
case 'day':
|
||||||
group.day = { $dayOfMonth: '$created_at' }
|
group.day = { $dayOfMonth: '$created_at' }
|
||||||
sort['_id.day'] = 1;
|
|
||||||
fromParts.day = "$_id.day";
|
fromParts.day = "$_id.day";
|
||||||
case 'month':
|
case 'month':
|
||||||
group.month = { $month: '$created_at' }
|
group.month = { $month: '$created_at' }
|
||||||
sort['_id.month'] = 1;
|
|
||||||
fromParts.month = "$_id.month";
|
fromParts.month = "$_id.month";
|
||||||
case 'year':
|
case 'year':
|
||||||
group.year = { $year: '$created_at' }
|
group.year = { $year: '$created_at' }
|
||||||
sort['_id.year'] = 1;
|
|
||||||
fromParts.year = "$_id.year";
|
fromParts.year = "$_id.year";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (slice) {
|
||||||
|
case 'year':
|
||||||
|
sort['_id.year'] = 1;
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
sort['_id.year'] = 1;
|
||||||
|
sort['_id.month'] = 1;
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
sort['_id.year'] = 1;
|
||||||
|
sort['_id.month'] = 1;
|
||||||
|
sort['_id.day'] = 1;
|
||||||
|
break;
|
||||||
|
case 'hour':
|
||||||
|
sort['_id.year'] = 1;
|
||||||
|
sort['_id.month'] = 1;
|
||||||
|
sort['_id.day'] = 1;
|
||||||
|
sort['_id.hour'] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return { group, sort, fromParts }
|
return { group, sort, fromParts }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +107,15 @@ class DateService {
|
|||||||
const firstDate = dayjs(dates.at(0));
|
const firstDate = dayjs(dates.at(0));
|
||||||
const lastDate = dayjs(dates.at(-1));
|
const lastDate = dayjs(dates.at(-1));
|
||||||
let currentDate = firstDate.clone();
|
let currentDate = firstDate.clone();
|
||||||
while (currentDate.isBefore(lastDate)) {
|
|
||||||
|
console.log('currentDate', currentDate.toISOString());
|
||||||
|
console.log(' lastDate', lastDate.toISOString());
|
||||||
|
|
||||||
|
while (currentDate.isBefore(lastDate, slice)) {
|
||||||
currentDate = currentDate.add(1, slice);
|
currentDate = currentDate.add(1, slice);
|
||||||
allDates.push(currentDate);
|
allDates.push(currentDate);
|
||||||
}
|
}
|
||||||
|
console.log('alldates', allDates.length);
|
||||||
return allDates;
|
return allDates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,3 +132,31 @@ class DateService {
|
|||||||
|
|
||||||
const dateServiceInstance = new DateService();
|
const dateServiceInstance = new DateService();
|
||||||
export default dateServiceInstance;
|
export default dateServiceInstance;
|
||||||
|
|
||||||
|
|
||||||
|
dateServiceInstance.fillDates([
|
||||||
|
{ _id: "2024-06-21T00:00:00.000Z", count: 33 },
|
||||||
|
{ _id: "2024-06-21T01:00:00.000Z", count: 10 },
|
||||||
|
{ _id: "2024-06-21T02:00:00.000Z", count: 7 },
|
||||||
|
{ _id: "2024-06-21T03:00:00.000Z", count: 7 },
|
||||||
|
{ _id: "2024-06-21T04:00:00.000Z", count: 7 },
|
||||||
|
{ _id: "2024-06-21T05:00:00.000Z", count: 27 },
|
||||||
|
{ _id: "2024-06-21T06:00:00.000Z", count: 5 },
|
||||||
|
{ _id: "2024-06-21T07:00:00.000Z", count: 9 },
|
||||||
|
{ _id: "2024-06-21T08:00:00.000Z", count: 24 },
|
||||||
|
{ _id: "2024-06-21T09:00:00.000Z", count: 6 },
|
||||||
|
{ _id: "2024-06-21T10:00:00.000Z", count: 13 },
|
||||||
|
{ _id: "2024-06-21T11:00:00.000Z", count: 12 },
|
||||||
|
{ _id: "2024-06-21T12:00:00.000Z", count: 13 },
|
||||||
|
{ _id: "2024-06-21T13:00:00.000Z", count: 68 },
|
||||||
|
{ _id: "2024-06-21T14:00:00.000Z", count: 12 },
|
||||||
|
{ _id: "2024-06-21T15:00:00.000Z", count: 26 },
|
||||||
|
{ _id: "2024-06-21T16:00:00.000Z", count: 8 },
|
||||||
|
{ _id: "2024-06-21T17:00:00.000Z", count: 8 },
|
||||||
|
{ _id: "2024-06-21T18:00:00.000Z", count: 17 },
|
||||||
|
{ _id: "2024-06-20T19:00:00.000Z", count: 7 },
|
||||||
|
{ _id: "2024-06-20T20:00:00.000Z", count: 13 },
|
||||||
|
{ _id: "2024-06-20T21:00:00.000Z", count: 10 },
|
||||||
|
{ _id: "2024-06-20T22:00:00.000Z", count: 16 },
|
||||||
|
{ _id: "2024-06-20T23:00:00.000Z", count: 14 }
|
||||||
|
].map(e => e._id), 'hour')
|
||||||
Reference in New Issue
Block a user