This commit is contained in:
Emily
2024-06-21 21:37:58 +02:00
parent b86a298511
commit 80195cfc33
8 changed files with 143 additions and 33 deletions

View File

@@ -1,20 +1,20 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
const data = ref<number[]>([]);
const labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const key = ref<string>('0');
const props = defineProps<{ slice: SliceName }>();
const props = defineProps<{ slice: Slice }>();
async function loadData() {
const response = await useTimelineData('sessions', props.slice);
const response = await useTimeline('sessions', props.slice);
if (!response) return;
data.value = response.data;
labels.value = response.labels;
data.value = response.map(e => e.count);
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
ready.value = true;
key.value = Date.now().toString();
}
onMounted(async () => {

View File

@@ -1,19 +1,18 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService';
const data = ref<number[]>([]);
const labels = ref<string[]>([]);
const ready = ref<boolean>(false);
const props = defineProps<{ slice: SliceName }>();
const props = defineProps<{ slice: Slice }>();
async function loadData() {
const response = await useVisitsTimeline(props.slice);
const response = await useTimeline('visits', props.slice);
if (!response) return;
const fixed = fixMetrics(response, props.slice);
console.log(fixed);
data.value = fixed.data;
labels.value = fixed.labels;
data.value = response.map(e => e.count);
labels.value = response.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, props.slice));
ready.value = true;
}

View File

@@ -16,7 +16,7 @@ export function useFirstInteractionData() {
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(
fromDate || DateService.getDefaultRange(slice).from,
@@ -26,13 +26,13 @@ export async function useVisitsTimeline(slice: Slice, fromDate?: string, toDate?
const activeProject = useActiveProject();
const response = await $fetch(
`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`, {
method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ slice, from, to })
});
return response;
return response as { _id: string, count: number }[];
}

View File

@@ -63,7 +63,8 @@ watch(pending, () => {
const selectLabels = [
{ label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' }
{ label: 'Day', value: 'day' },
// { label: 'Month', value: 'month' },
];

View File

@@ -2,7 +2,7 @@ import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { SessionModel } from "@schema/metrics/SessionSchema";
import DateService from "@services/DateService";
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
@@ -12,11 +12,37 @@ export default defineEventHandler(async event => {
const project = await getUserProjectFromId(project_id, user);
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 timelineSessions = await getTimeline(SessionModel, project_id, slice, duration);
return timelineSessions;
const { slice, from, to } = await readBody(event);
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;
});

View File

@@ -1,7 +1,7 @@
import { getTimeline } from "./generic";
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import DateService from "@services/DateService";
export default defineEventHandler(async event => {
@@ -12,11 +12,37 @@ export default defineEventHandler(async event => {
const project = await getUserProjectFromId(project_id, user);
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 timelineVisits = await getTimeline(VisitModel, project_id, slice, duration);
return timelineVisits;
const { slice, from, to } = await readBody(event);
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;
});

View File

@@ -21,6 +21,7 @@ export class Redis {
url: runtimeConfig.REDIS_URL,
username: runtimeConfig.REDIS_USERNAME,
password: runtimeConfig.REDIS_PASSWORD,
database: process.dev ? 1 : 0
});
static async init() {

View File

@@ -23,6 +23,13 @@ class DateService {
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) {
return {
from: new Date(Date.now() - slicesData[slice].fromOffset).toISOString(),
@@ -39,22 +46,39 @@ class DateService {
switch (slice) {
case 'hour':
group.hour = { $hour: '$created_at' }
sort['_id.hour'] = 1;
fromParts.hour = "$_id.hour";
case 'day':
group.day = { $dayOfMonth: '$created_at' }
sort['_id.day'] = 1;
fromParts.day = "$_id.day";
case 'month':
group.month = { $month: '$created_at' }
sort['_id.month'] = 1;
fromParts.month = "$_id.month";
case 'year':
group.year = { $year: '$created_at' }
sort['_id.year'] = 1;
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 }
}
@@ -83,10 +107,15 @@ class DateService {
const firstDate = dayjs(dates.at(0));
const lastDate = dayjs(dates.at(-1));
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);
allDates.push(currentDate);
}
console.log('alldates', allDates.length);
return allDates;
}
@@ -102,4 +131,32 @@ class 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')