From 9e2a47800153b301bb685b54d3b5dbdb67b05fae Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 3 May 2023 17:17:57 -0700 Subject: [PATCH] Add user usage. --- pages/api/users/[id]/usage.ts | 74 +++++++++++++++++++ pages/api/users/[id]/websites.ts | 4 +- queries/analytics/event/getEventUsage.ts | 32 ++++++++ .../analytics/eventData/getEventDataUsage.ts | 32 ++++++++ queries/index.js | 2 + 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 pages/api/users/[id]/usage.ts create mode 100644 queries/analytics/event/getEventUsage.ts create mode 100644 queries/analytics/eventData/getEventDataUsage.ts diff --git a/pages/api/users/[id]/usage.ts b/pages/api/users/[id]/usage.ts new file mode 100644 index 00000000..0118df92 --- /dev/null +++ b/pages/api/users/[id]/usage.ts @@ -0,0 +1,74 @@ +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries'; + +export interface UserUsageRequestQuery { + id: string; + startAt: string; + endAt: string; +} + +export interface UserUsageRequestResponse { + websiteEventUsage: number; + eventDataUsage: number; + websites: { + websiteEventUsage: number; + eventDataUsage: number; + websiteId: string; + websiteName: string; + }[]; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { user } = req.auth; + + if (req.method === 'GET') { + if (!user.isAdmin) { + return unauthorized(res); + } + + const { id: userId, startAt, endAt } = req.query; + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const websites = await getUserWebsites(userId); + + const websiteIds = websites.map(a => a.id); + + const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); + const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); + + const websiteUsage = websites.map(a => ({ + websiteId: a.id, + websiteName: a.name, + websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, + eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, + })); + + const usage = websiteUsage.reduce( + (acc, cv) => { + acc.websiteEventUsage += cv.websiteEventUsage; + acc.eventDataUsage += cv.eventDataUsage; + + return acc; + }, + { websiteEventUsage: 0, eventDataUsage: 0 }, + ); + + return ok(res, { + ...usage, + websites: websiteUsage, + }); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index c8b874bb..de4a3a3a 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -4,14 +4,14 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUserWebsites } from 'queries'; -export interface WebsitesRequestBody { +export interface UserWebsitesRequestBody { name: string; domain: string; shareId: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/queries/analytics/event/getEventUsage.ts b/queries/analytics/event/getEventUsage.ts new file mode 100644 index 00000000..1465264c --- /dev/null +++ b/queries/analytics/event/getEventUsage.ts @@ -0,0 +1,32 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; + +export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websiteIds: string[], startDate: Date, endDate: Date) { + throw new Error('Not Implemented'); +} + +function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) { + const { rawQuery } = clickhouse; + + return rawQuery( + `select + website_id as websiteId, + count(*) as count + from website_event + where created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_id in {websiteIds:Array(UUID)} + group by website_id`, + { + websiteIds, + startDate, + endDate, + }, + ); +} diff --git a/queries/analytics/eventData/getEventDataUsage.ts b/queries/analytics/eventData/getEventDataUsage.ts new file mode 100644 index 00000000..5d470c3c --- /dev/null +++ b/queries/analytics/eventData/getEventDataUsage.ts @@ -0,0 +1,32 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; + +export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websiteIds: string[], startDate: Date, endDate: Date) { + throw new Error('Not Implemented'); +} + +function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) { + const { rawQuery } = clickhouse; + + return rawQuery( + `select + website_id as websiteId, + count(*) as count + from event_data + where created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_id in {websiteIds:Array(UUID)} + group by website_id`, + { + websiteIds, + startDate, + endDate, + }, + ); +} diff --git a/queries/index.js b/queries/index.js index 1275e173..d87d5dd5 100644 --- a/queries/index.js +++ b/queries/index.js @@ -3,8 +3,10 @@ export * from './admin/teamUser'; export * from './admin/user'; export * from './admin/website'; export * from './analytics/event/getEventMetrics'; +export * from './analytics/event/getEventUsage'; export * from './analytics/event/getEvents'; export * from './analytics/eventData/getEventData'; +export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewMetrics'; export * from './analytics/pageview/getPageviewStats';