From 3e36e23b37b7a5e2321b882e16e70a4950eba368 Mon Sep 17 00:00:00 2001 From: Emily Date: Sat, 22 Jun 2024 02:03:13 +0200 Subject: [PATCH] . --- dashboard/composables/useDataService.ts | 5 +- .../[project_id]/timeline/referrers.post.ts | 40 +++++++------- .../[project_id]/timeline/sessions.post.ts | 37 +++++-------- .../[project_id]/timeline/visits.post.ts | 29 ++++------ dashboard/server/services/TimelineService.ts | 53 +++++++++++++++++++ 5 files changed, 97 insertions(+), 67 deletions(-) create mode 100644 dashboard/server/services/TimelineService.ts diff --git a/dashboard/composables/useDataService.ts b/dashboard/composables/useDataService.ts index ffc1754..2062ea6 100644 --- a/dashboard/composables/useDataService.ts +++ b/dashboard/composables/useDataService.ts @@ -16,7 +16,8 @@ export function useFirstInteractionData() { return metricsInfo; } -export async function useTimeline(endpoint: 'visits' | 'sessions', slice: Slice, fromDate?: string, toDate?: string) { + +export async function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Slice, fromDate?: string, toDate?: string) { const { from, to } = DateService.prepareDateRange( fromDate || DateService.getDefaultRange(slice).from, @@ -36,6 +37,8 @@ export async function useTimeline(endpoint: 'visits' | 'sessions', slice: Slice, } + + export async function useTimelineDataRaw(timelineEndpointName: string, slice: SliceName) { const activeProject = useActiveProject(); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts index b36998d..e98803d 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts @@ -3,6 +3,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema"; import DateService from "@services/DateService"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; +import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -12,30 +13,27 @@ export default defineEventHandler(async event => { const project = await getUserProjectFromId(project_id, user); if (!project) return; - const { slice, referrer, from, to } = await readBody(event); + const { slice, from, to } = await readBody(event); - const { group, sort, fromParts } = DateService.getQueryDateRange(slice); + 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'); - const aggregation = [ - { - $match: { - project_id: project._id, - created_at: { - $gte: new Date(from), - $lte: new Date(to) - }, - referrer + return await Redis.useCache({ + key: `timeline:referrers:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, + exp: TIMELINE_EXPIRE_TIME + }, async () => { + const timelineData = await executeAdvancedTimelineAggregation({ + projectId: project._id, + model: VisitModel, + from, to, slice, + customMatch: { + referrer: '$referrer' } - }, - { $group: { _id: group, count: { $sum: 1 } } }, - { $sort: sort }, - { $project: { _id: { $dateFromParts: fromParts }, count: "$count" } } - ] + }); + const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + return timelineFilledMerged; + }); - const timelineReferrers: { _id: string, count: number }[] = await VisitModel.aggregate(aggregation); - - const filledDates = DateService.fillDates(timelineReferrers.map(e => e._id), slice); - const merged = DateService.mergeFilledDates(filledDates, timelineReferrers, '_id', slice, { count: 0 }); - return merged; }); \ No newline at end of file diff --git a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts index 881af36..c81db86 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts @@ -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"; +import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -19,30 +19,17 @@ export default defineEventHandler(async event => { 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; - + return await Redis.useCache({ + key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, + exp: TIMELINE_EXPIRE_TIME + }, async () => { + const timelineData = await executeTimelineAggregation({ + projectId: project._id, + model: SessionModel, + from, to, slice + }); + const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + return timelineFilledMerged; }); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts index 6ad255c..a87e35c 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts @@ -2,6 +2,7 @@ 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"; +import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { @@ -23,28 +24,16 @@ export default defineEventHandler(async event => { 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; - + const timelineData = await executeTimelineAggregation({ + projectId: project._id, + model: VisitModel, + from, to, slice + }); + const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + return timelineFilledMerged; }); + }); \ No newline at end of file diff --git a/dashboard/server/services/TimelineService.ts b/dashboard/server/services/TimelineService.ts new file mode 100644 index 0000000..13fc2ca --- /dev/null +++ b/dashboard/server/services/TimelineService.ts @@ -0,0 +1,53 @@ + +import { Slice } from "@services/DateService"; +import DateService from "@services/DateService"; +import type mongoose from "mongoose"; + + +export type TimelineAggregationOptions = { + projectId: mongoose.Schema.Types.ObjectId, + model: mongoose.Model, + from: string | number, + to: string | number, + slice: Slice +} + +export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & { + customMatch?: Record +} + +export async function executeAdvancedTimelineAggregation(options: AdvancedTimelineAggregationOptions) { + + options.customMatch = options.customMatch || {}; + + const { group, sort, fromParts } = DateService.getQueryDateRange(options.slice); + + const aggregation = [ + { + $match: { + project_id: options.projectId, + created_at: { $gte: new Date(options.from), $lte: new Date(options.to) }, + ...options.customMatch + } + }, + { $group: { _id: group, count: { $sum: 1 } } }, + { $sort: sort }, + { $project: { _id: { $dateFromParts: fromParts }, count: "$count" } } + ] + + const timeline: { _id: string, count: number }[] = await options.model.aggregate(aggregation); + + return timeline; + +} + +export async function executeTimelineAggregation(options: TimelineAggregationOptions) { + return executeAdvancedTimelineAggregation(options); +} + + +export function fillAndMergeTimelineAggregation(timeline: { _id: string, count: number }[], slice: Slice) { + const filledDates = DateService.fillDates(timeline.map(e => e._id), slice); + const merged = DateService.mergeFilledDates(filledDates, timeline, '_id', slice, { count: 0 }); + return merged; +} \ No newline at end of file