From 67394194af0fbc0503856bbafac5b98046b26379 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 18 Oct 2022 15:54:17 -0700 Subject: [PATCH] eventdata api --- components/pages/TestConsole.js | 10 ++-- lib/clickhouse.js | 40 +++++++++++++++ lib/prisma.js | 56 +++++++++++++++++++++ pages/api/collect.js | 11 ++--- pages/api/websites/[id]/eventdata.js | 50 +++++++++++++++++++ pages/console/[[...id]].js | 6 +++ queries/analytics/event/getEventData.js | 65 +++++++++++++++++++++++++ queries/analytics/event/saveEvent.js | 5 +- queries/index.js | 1 + 9 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 pages/api/websites/[id]/eventdata.js create mode 100644 queries/analytics/event/getEventData.js diff --git a/components/pages/TestConsole.js b/components/pages/TestConsole.js index 215c350c..6200f7e5 100644 --- a/components/pages/TestConsole.js +++ b/components/pages/TestConsole.js @@ -24,9 +24,9 @@ export default function TestConsole() { return null; } - const options = data.map(({ name, websiteId }) => ({ label: name, value: websiteId })); - const website = data.find(({ websiteId }) => websiteId === +websiteId); - const selectedValue = options.find(({ value }) => value === website?.websiteId)?.value; + const options = data.map(({ name, websiteUuid }) => ({ label: name, value: websiteUuid })); + const website = data.find(({ websiteUuid }) => websiteId === websiteUuid); + const selectedValue = options.find(({ value }) => value === website?.websiteUuid)?.value; function handleSelect(value) { router.push(`/console/${value}`); @@ -104,13 +104,13 @@ export default function TestConsole() {
Events - +
diff --git a/lib/clickhouse.js b/lib/clickhouse.js index a01a36cc..bf4bc5c5 100644 --- a/lib/clickhouse.js +++ b/lib/clickhouse.js @@ -69,6 +69,44 @@ function getBetweenDates(field, start_at, end_at) { and ${getDateFormat(end_at)}`; } +function getJsonField(column, property) { + return `${column}.${property}`; +} + +function getEventDataColumnsQuery(column, columns) { + const query = Object.keys(columns).reduce((arr, key) => { + const filter = columns[key]; + + if (filter === undefined) { + return arr; + } + + arr.push(`${filter}(${getJsonField(column, key)})`); + + return arr; + }, []); + + return query.join(',\n'); +} + +function getEventDataFilterQuery(column, filters) { + const query = Object.keys(filters).reduce((arr, key) => { + const filter = filters[key]; + + if (filter === undefined) { + return arr; + } + + arr.push( + `${getJsonField(column, key)} = ${typeof filter === 'string' ? `'${filter}'` : filter}`, + ); + + return arr; + }, []); + + return query.join('\nand '); +} + function getFilterQuery(column, filters = {}, params = []) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -186,6 +224,8 @@ export default { getDateFormat, getCommaSeparatedStringFormat, getBetweenDates, + getEventDataColumnsQuery, + getEventDataFilterQuery, getFilterQuery, parseFilters, findUnique, diff --git a/lib/prisma.js b/lib/prisma.js index b93afafa..1ebf8aa4 100644 --- a/lib/prisma.js +++ b/lib/prisma.js @@ -85,6 +85,60 @@ function getTimestampInterval(field) { } } +function getJsonField(column, property, value) { + const db = getDatabaseType(process.env.DATABASE_URL); + + if (db === POSTGRESQL) { + let accessor = `${column} ->> '${property}'`; + + if (value && typeof value === 'number') { + accessor = `CAST(${accessor} AS DECIMAL)`; + } + + return accessor; + } + + if (db === MYSQL) { + return `${column} ->> "$.${property}"`; + } +} + +function getEventDataColumnsQuery(column, columns) { + const query = Object.keys(columns).reduce((arr, key) => { + const filter = columns[key]; + + if (filter === undefined) { + return arr; + } + + arr.push(`${filter}(${getJsonField(column, key)})`); + + return arr; + }, []); + + return query.join(',\n'); +} + +function getEventDataFilterQuery(column, filters) { + const query = Object.keys(filters).reduce((arr, key) => { + const filter = filters[key]; + + if (filter === undefined) { + return arr; + } + + arr.push( + `${getJsonField(column, key, filter)} = ${ + typeof filter === 'string' ? `'${filter}'` : filter + }`, + ); + + return arr; + }, []); + + return query.join('\nand '); +} + function getFilterQuery(table, column, filters = {}, params = []) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -193,6 +247,8 @@ export default { getDateQuery, getTimestampInterval, getFilterQuery, + getEventDataColumnsQuery, + getEventDataFilterQuery, parseFilters, rawQuery, transaction, diff --git a/pages/api/collect.js b/pages/api/collect.js index 42fd309a..5a4411d5 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -58,13 +58,11 @@ export default async (req, res) => { await useSession(req, res); - const { - session: { website, session }, - } = req; + const { website, session } = req.session; const { type, payload } = getJsonBody(req); - let { url, referrer, eventName, eventData } = payload; + let { url, referrer, event_name: eventName, event_data: eventData } = payload; if (process.env.REMOVE_TRAILING_SLASH) { url = url.replace(/\/$/, ''); @@ -88,9 +86,8 @@ export default async (req, res) => { const token = createToken( { - websiteId: website.websiteUuid, - sessionId: session.sessionId, - sessionUuid: session.sessionUuid, + website, + session, }, secret(), ); diff --git a/pages/api/websites/[id]/eventdata.js b/pages/api/websites/[id]/eventdata.js new file mode 100644 index 00000000..61cd8671 --- /dev/null +++ b/pages/api/websites/[id]/eventdata.js @@ -0,0 +1,50 @@ +import moment from 'moment-timezone'; +import { getEventData } from 'queries'; +import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; +import { allowQuery } from 'lib/auth'; +import { useAuth, useCors } from 'lib/middleware'; + +const unitTypes = ['year', 'month', 'hour', 'day']; + +export default async (req, res) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'POST') { + if (!(await allowQuery(req))) { + return unauthorized(res); + } + + const { + website_id: websiteId, + start_at, + end_at, + unit, + timezone, + event_name: eventName, + columns, + filters, + } = req.body; + + if (!moment.tz.zone(timezone) || !unitTypes.includes(unit)) { + return badRequest(res); + } + + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); + + const events = await getEventData(websiteId, { + startDate, + endDate, + timezone, + unit, + eventName, + columns, + filters, + }); + + return ok(res, events); + } + + return methodNotAllowed(res); +}; diff --git a/pages/console/[[...id]].js b/pages/console/[[...id]].js index 8e37e77a..a13537f8 100644 --- a/pages/console/[[...id]].js +++ b/pages/console/[[...id]].js @@ -18,3 +18,9 @@ export default function ConsolePage({ enabled }) { ); } + +export async function getServerSideProps() { + return { + props: { enabled: !!process.env.ENABLE_TEST_CONSOLE }, + }; +} diff --git a/queries/analytics/event/getEventData.js b/queries/analytics/event/getEventData.js new file mode 100644 index 00000000..8fa87f2f --- /dev/null +++ b/queries/analytics/event/getEventData.js @@ -0,0 +1,65 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; + +export async function getEventData(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId, + { startDate, endDate, timezone = 'utc', unit = 'day', event_name, columns, filters }, +) { + const { rawQuery, getDateQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma; + const params = [startDate, endDate]; + + return rawQuery( + `select + ${getDateQuery('event.created_at', unit, timezone)} t, + ${getEventDataColumnsQuery('event_data.event_data', columns)} + from event + join website + on event.website_id = website.website_id + join event_data + on event.event_id = event_data.event_id + where website_uuid='${websiteId}' + and event.created_at between $1 and $2 + ${event_name ? `and event_name = ${event_name}` : ''} + ${filters ? `and ${getEventDataFilterQuery('event_data.event_data', filters)}` : ''} + group by 1 + order by 2`, + params, + ); +} + +async function clickhouseQuery( + websiteId, + { startDate, endDate, timezone = 'UTC', unit = 'day', event_name, columns, filters }, +) { + const { + rawQuery, + getDateQuery, + getBetweenDates, + getEventDataColumnsQuery, + getEventDataFilterQuery, + } = clickhouse; + const params = [websiteId]; + + return rawQuery( + `select + event_name x, + ${getDateQuery('created_at', unit, timezone)} t, + ${getEventDataColumnsQuery('event_data', columns)} + from event + where website_id= $1 + ${event_name ? `and event_name = ${event_name}` : ''} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filters ? `and ${getEventDataFilterQuery('event_data', filters)}` : ''} + group by x, t + order by t`, + params, + ); +} diff --git a/queries/analytics/event/saveEvent.js b/queries/analytics/event/saveEvent.js index 26cf18b7..de7a1cae 100644 --- a/queries/analytics/event/saveEvent.js +++ b/queries/analytics/event/saveEvent.js @@ -12,13 +12,14 @@ export async function saveEvent(...args) { async function relationalQuery( { websiteId }, - { session: { id: sessionId }, url, eventName, eventData }, + { session: { id: sessionId }, eventUuid, url, eventName, eventData }, ) { const data = { websiteId, sessionId, url: url?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), + eventUuid, }; if (eventData) { @@ -47,7 +48,7 @@ async function clickhouseQuery( created_at: getDateFormat(new Date()), url: url?.substring(0, URL_LENGTH), event_name: eventName?.substring(0, EVENT_NAME_LENGTH), - event_data: JSON.stringify(eventData), + event_data: eventData ? JSON.stringify(eventData) : null, ...sessionArgs, country: country ? country : null, }; diff --git a/queries/index.js b/queries/index.js index d6b4093a..abff147a 100644 --- a/queries/index.js +++ b/queries/index.js @@ -17,6 +17,7 @@ export * from './admin/website/resetWebsite'; export * from './admin/website/updateWebsite'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEvents'; +export * from './analytics/event/getEventData'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewMetrics'; export * from './analytics/pageview/getPageviewParams';