From 79a93ed9fc3e99fce9f89456796acb9bd4d0c0d5 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 20 May 2024 21:10:46 -0700 Subject: [PATCH 1/4] add event_data to goal. --- src/app/(main)/reports/goals/GoalsAddForm.tsx | 54 +++++++++++- src/app/(main)/reports/goals/GoalsChart.tsx | 27 ++++-- .../reports/goals/GoalsParameters.module.css | 11 +++ .../(main)/reports/goals/GoalsParameters.tsx | 75 ++++++++++------ src/components/messages.ts | 11 ++- src/pages/api/reports/goals.ts | 16 +++- src/queries/analytics/reports/getGoals.ts | 85 ++++++++++++++++--- 7 files changed, 231 insertions(+), 48 deletions(-) diff --git a/src/app/(main)/reports/goals/GoalsAddForm.tsx b/src/app/(main)/reports/goals/GoalsAddForm.tsx index a8a77c58..a82eea28 100644 --- a/src/app/(main)/reports/goals/GoalsAddForm.tsx +++ b/src/app/(main)/reports/goals/GoalsAddForm.tsx @@ -6,27 +6,48 @@ import styles from './GoalsAddForm.module.css'; export function GoalsAddForm({ type: defaultType = 'url', value: defaultValue = '', + property: defaultProperty = '', + operator: defaultAggregae = null, goal: defaultGoal = 10, onChange, }: { type?: string; value?: string; + operator?: string; + property?: string; goal?: number; - onChange?: (step: { type: string; value: string; goal: number }) => void; + onChange?: (step: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }) => void; }) { const [type, setType] = useState(defaultType); const [value, setValue] = useState(defaultValue); + const [operator, setOperator] = useState(defaultAggregae); + const [property, setProperty] = useState(defaultProperty); const [goal, setGoal] = useState(defaultGoal); const { formatMessage, labels } = useMessages(); const items = [ { label: formatMessage(labels.url), value: 'url' }, { label: formatMessage(labels.event), value: 'event' }, + { label: formatMessage(labels.eventData), value: 'event-data' }, + ]; + const operators = [ + { label: formatMessage(labels.count), value: 'count' }, + { label: formatMessage(labels.average), value: 'average' }, + { label: formatMessage(labels.sum), value: 'sum' }, ]; const isDisabled = !type || !value; const handleSave = () => { - onChange({ type, value, goal }); + onChange( + type === 'event-data' ? { type, value, goal, operator, property } : { type, value, goal }, + ); setValue(''); + setProperty(''); setGoal(10); }; @@ -45,6 +66,10 @@ export function GoalsAddForm({ return items.find(item => item.value === value)?.label; }; + const renderoperatorValue = (value: any) => { + return operators.find(item => item.value === value)?.label; + }; + return ( @@ -70,6 +95,31 @@ export function GoalsAddForm({ /> + {type === 'event-data' && ( + + + setOperator(value)} + > + {({ value, label }) => { + return {label}; + }} + + handleChange(e, setProperty)} + autoFocus={true} + autoComplete="off" + onKeyDown={handleKeyDown} + /> + + + )} { + let label = ''; + switch (type) { + case 'url': + label = labels.viewedPage; + break; + case 'event': + label = labels.triggeredEvent; + break; + default: + label = labels.collectedData; + break; + } + + return label; + }; + return (
- {data?.map(({ type, value, goal, result }, index: number) => { + {data?.map(({ type, value, goal, result, property, operator }, index: number) => { const percent = result > goal ? 100 : (result / goal) * 100; return (
- - {formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)} - - {value} + {formatMessage(getLabel(type))} + {`${value}${ + type === 'event-data' ? `:(${operator}):${property}` : '' + }`}
}> - {goals.map((goal: { type: string; value: string; goal: number }, index: number) => { - return ( - - : } - onRemove={() => handleRemoveGoals(index)} - > -
{goal.value}
-
- {formatMessage(labels.goal)}: {formatNumber(goal.goal)} -
-
- - {(close: () => void) => ( - - - - )} - -
- ); - })} + {goals.map( + ( + goal: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }, + index: number, + ) => { + return ( + + : } + onRemove={() => handleRemoveGoals(index)} + > + +
{goal.value}
+ {goal.type === 'event-data' && ( +
+ {formatMessage(labels[goal.operator])}: {goal.property} +
+ )} +
+ {formatMessage(labels.goal)}: {formatNumber(goal.goal)} +
+
+
+ + {(close: () => void) => ( + + + + )} + +
+ ); + }, + )}
diff --git a/src/components/messages.ts b/src/components/messages.ts index 238ebf52..08448476 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -95,6 +95,9 @@ export const labels = defineMessages({ devices: { id: 'label.devices', defaultMessage: 'Devices' }, countries: { id: 'label.countries', defaultMessage: 'Countries' }, languages: { id: 'label.languages', defaultMessage: 'Languages' }, + count: { id: 'label.count', defaultMessage: 'Count' }, + average: { id: 'label.average', defaultMessage: 'Average' }, + sum: { id: 'label.sum', defaultMessage: 'Sum' }, event: { id: 'label.event', defaultMessage: 'Event' }, events: { id: 'label.events', defaultMessage: 'Events' }, query: { id: 'label.query', defaultMessage: 'Query' }, @@ -107,6 +110,7 @@ export const labels = defineMessages({ views: { id: 'label.views', defaultMessage: 'Views' }, none: { id: 'label.none', defaultMessage: 'None' }, clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' }, + property: { id: 'label.property', defaultMessage: 'Property' }, today: { id: 'label.today', defaultMessage: 'Today' }, lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, @@ -178,8 +182,6 @@ export const labels = defineMessages({ before: { id: 'label.before', defaultMessage: 'Before' }, after: { id: 'label.after', defaultMessage: 'After' }, total: { id: 'label.total', defaultMessage: 'Total' }, - sum: { id: 'label.sum', defaultMessage: 'Sum' }, - average: { id: 'label.average', defaultMessage: 'Average' }, min: { id: 'label.min', defaultMessage: 'Min' }, max: { id: 'label.max', defaultMessage: 'Max' }, unique: { id: 'label.unique', defaultMessage: 'Unique' }, @@ -220,6 +222,10 @@ export const labels = defineMessages({ id: 'message.viewed-page', defaultMessage: 'Viewed page', }, + collectedData: { + id: 'message.collected-data', + defaultMessage: 'Collected data', + }, triggeredEvent: { id: 'message.triggered-event', defaultMessage: 'Triggered event', @@ -241,7 +247,6 @@ export const labels = defineMessages({ id: 'label.goals-description', defaultMessage: 'Track your goals for pageviews and events.', }, - count: { id: 'label.count', defaultMessage: 'Count' }, journey: { id: 'label.journey', defaultMessage: 'Journey' }, journeyDescription: { id: 'label.journey-description', diff --git a/src/pages/api/reports/goals.ts b/src/pages/api/reports/goals.ts index bb766775..f775dc3c 100644 --- a/src/pages/api/reports/goals.ts +++ b/src/pages/api/reports/goals.ts @@ -28,9 +28,23 @@ const schema = { .array() .of( yup.object().shape({ - type: yup.string().required(), + type: yup + .string() + .matches(/url|event|event-data/i) + .required(), value: yup.string().required(), goal: yup.number().required(), + operator: yup + .string() + .matches(/count|sum|average/i) + .when('type', { + is: 'eventData', + then: yup.string().required(), + }), + property: yup.string().when('type', { + is: 'eventData', + then: yup.string().required(), + }), }), ) .min(1) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index d26998d0..66c1d951 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -8,7 +8,7 @@ export async function getGoals( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string }[]; }, ] ) { @@ -23,7 +23,7 @@ async function relationalQuery( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string }[]; }, ): Promise { const { startDate, endDate, goals } = criteria; @@ -119,7 +119,7 @@ async function clickhouseQuery( criteria: { startDate: Date; endDate: Date; - goals: { type: string; value: string; goal: number }[]; + goals: { type: string; value: string; goal: number; operator?: string; property?: string }[]; }, ): Promise<{ type: string; value: string; goal: number; result: number }[]> { const { startDate, endDate, goals } = criteria; @@ -127,13 +127,22 @@ async function clickhouseQuery( const urls = goals.filter(a => a.type === 'url'); const events = goals.filter(a => a.type === 'event'); + const eventData = goals.filter(a => a.type === 'event-data'); const hasUrl = urls.length > 0; const hasEvent = events.length > 0; + const hasEventData = eventData.length > 0; function getParameters( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlParam = urls.reduce((acc, cv, i) => { acc[`${cv.type}${i}`] = cv.value; @@ -145,41 +154,77 @@ async function clickhouseQuery( return acc; }, {}); + const eventDataParam = eventData.reduce((acc, cv, i) => { + acc[`eventData${i}`] = cv.value; + acc[`property${i}`] = cv.property; + return acc; + }, {}); + return { urls: { ...urlParam, startDate, endDate, websiteId }, events: { ...eventParam, startDate, endDate, websiteId }, + eventData: { ...eventDataParam, startDate, endDate, websiteId }, }; } function getColumns( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlColumns = urls .map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`) .join('\n') .slice(0, -1); const eventColumns = events - .map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i}`) + .map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i},`) + .join('\n') + .slice(0, -1); + const eventDataColumns = eventData + .map( + (a, i) => + `${a.operator === 'average' ? 'avg' : a.operator}If(${ + a.operator !== 'count' ? 'number_value, ' : '' + }event_name = {eventData${i}:String} AND data_key = {property${i}:String}) AS EVENT_DATA${i},`, + ) .join('\n') .slice(0, -1); - return { url: urlColumns, events: eventColumns }; + return { url: urlColumns, events: eventColumns, eventData: eventDataColumns }; } function getWhere( urls: { type: string; value: string; goal: number }[], events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], ) { const urlWhere = urls.map((a, i) => `{url${i}:String}`).join(','); const eventWhere = events.map((a, i) => `{event${i}:String}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{eventData${i}:String}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{property${i}:String}`).join(','); - return { urls: `and url_path in (${urlWhere})`, events: `and event_name in (${eventWhere})` }; + return { + urls: `and url_path in (${urlWhere})`, + events: `and event_name in (${eventWhere})`, + eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, + }; } - const parameters = getParameters(urls, events); - const columns = getColumns(urls, events); - const where = getWhere(urls, events); + const parameters = getParameters(urls, events, eventData); + const columns = getColumns(urls, events, eventData); + const where = getWhere(urls, events, eventData); const urlResults = hasUrl ? await rawQuery( @@ -221,5 +266,25 @@ async function clickhouseQuery( }) : []; - return [...urlResults, ...eventResults]; + const eventDataResults = hasEventData + ? await rawQuery( + ` + select + ${columns.eventData} + from event_data + where website_id = {websiteId:UUID} + ${where.eventData} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + `, + parameters.eventData, + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; + }); + }) + : []; + + return [...urlResults, ...eventResults, ...eventDataResults]; } From 5c9abe966bddc12964e78bd938c28bbc3c012193 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 20 May 2024 23:58:40 -0700 Subject: [PATCH 2/4] add psql. --- src/queries/analytics/reports/getGoals.ts | 181 ++++++++++++++++------ 1 file changed, 134 insertions(+), 47 deletions(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index 66c1d951..cecdd10c 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -29,89 +29,176 @@ async function relationalQuery( const { startDate, endDate, goals } = criteria; const { rawQuery } = prisma; - const hasUrl = goals.some(a => a.type === 'url'); - const hasEvent = goals.some(a => a.type === 'event'); + const urls = goals.filter(a => a.type === 'url'); + const events = goals.filter(a => a.type === 'event'); + const eventData = goals.filter(a => a.type === 'event-data'); - function getParameters(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); + const hasUrl = urls.length > 0; + const hasEvent = events.length > 0; + const hasEventData = eventData.length > 0; - const events = goals - .filter(a => a.type === 'event') - .reduce((acc, cv, i) => { - acc[`${cv.type}${i}`] = cv.value; - return acc; - }, {}); + function getParameters( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlParam = urls.reduce((acc, cv, i) => { + acc[`${cv.type}${i}`] = cv.value; + return acc; + }, {}); + + const eventParam = events.reduce((acc, cv, i) => { + acc[`${cv.type}${i}`] = cv.value; + return acc; + }, {}); + + const eventDataParam = eventData.reduce((acc, cv, i) => { + acc[`eventData${i}`] = cv.value; + acc[`property${i}`] = cv.property; + acc[`eventData${i + 999}`] = cv.value; + acc[`property${i + 999}`] = cv.property; + return acc; + }, {}); return { - urls: { ...urls, startDate, endDate, websiteId }, - events: { ...events, startDate, endDate, websiteId }, + urls: { ...urlParam, startDate, endDate, websiteId }, + events: { ...eventParam, startDate, endDate, websiteId }, + eventData: { ...eventDataParam, startDate, endDate, websiteId }, }; } - function getColumns(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i}`) - .join('\n'); - const events = goals - .filter(a => a.type === 'event') - .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i}`) - .join('\n'); + function getColumns( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlColumns = urls + .map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i},`) + .join('\n') + .slice(0, -1); + const eventColumns = events + .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i},`) + .join('\n') + .slice(0, -1); + const eventDataColumns = eventData + .map( + (a, i) => + `${ + a.operator === 'average' ? 'avg' : a.operator + }(CASE WHEN event_name = {{eventData${i}}} AND data_key = {{property${i}}} THEN ${ + a.operator === 'count' ? '1' : 'number_value' + } END) AS EVENT_DATA${i},`, + ) + .join('\n') + .slice(0, -1); - return { urls, events }; + return { urls: urlColumns, events: eventColumns, eventData: eventDataColumns }; } - function getWhere(goals: { type: string; value: string; goal: number }[]) { - const urls = goals - .filter(a => a.type === 'url') - .map((a, i) => `{{url${i}}}`) - .join(','); - const events = goals - .filter(a => a.type === 'event') - .map((a, i) => `{{event${i}}}`) - .join(','); + function getWhere( + urls: { type: string; value: string; goal: number }[], + events: { type: string; value: string; goal: number }[], + eventData: { + type: string; + value: string; + goal: number; + operator?: string; + property?: string; + }[], + ) { + const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(','); + const eventWhere = events.map((a, i) => `{{event${i}}}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i + 999}}}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{{property${i + 999}}}`).join(','); - return { urls: `and url_path in (${urls})`, events: `and event_name in (${events})` }; + return { + urls: `and url_path in (${urlWhere})`, + events: `and event_name in (${eventWhere})`, + eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`, + }; } - const parameters = getParameters(goals); - const columns = getColumns(goals); - const where = getWhere(goals); + const parameters = getParameters(urls, events, eventData); + const columns = getColumns(urls, events, eventData); + const where = getWhere(urls, events, eventData); - const urls = hasUrl + const urlResults = hasUrl ? await rawQuery( ` select ${columns.urls} from website_event - where websiteId = {{websiteId::uuid}} + where website_id = {{websiteId::uuid}} ${where.urls} and created_at between {{startDate}} and {{endDate}} `, parameters.urls, - ) + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => ({ + ...urls[i], + goal: Number(urls[i].goal), + result: Number(results[key]), + })); + }) : []; - const events = hasEvent + const eventResults = hasEvent ? await rawQuery( ` select ${columns.events} from website_event - where websiteId = {{websiteId::uuid}} + where website_id = {{websiteId::uuid}} ${where.events} and created_at between {{startDate}} and {{endDate}} `, parameters.events, - ) + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) }; + }); + }) : []; - return [...urls, ...events]; + const eventDataResults = hasEventData + ? await rawQuery( + ` + select + ${columns.eventData} + from website_event w + join event_data d + on d.website_event_id = w.event_id + where w.website_id = {{websiteId::uuid}} + ${where.eventData} + and w.created_at between {{startDate}} and {{endDate}} + `, + parameters.eventData, + ).then(a => { + const results = a[0]; + + return Object.keys(results).map((key, i) => { + return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) }; + }); + }) + : []; + + return [...urlResults, ...eventResults, ...eventDataResults]; } async function clickhouseQuery( From 7add6d583a1296f3d4be273aed81a0ddd290020a Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 21 May 2024 10:44:27 -0700 Subject: [PATCH 3/4] fix params --- src/queries/analytics/reports/getGoals.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index cecdd10c..21ab2f7f 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -61,8 +61,6 @@ async function relationalQuery( const eventDataParam = eventData.reduce((acc, cv, i) => { acc[`eventData${i}`] = cv.value; acc[`property${i}`] = cv.property; - acc[`eventData${i + 999}`] = cv.value; - acc[`property${i + 999}`] = cv.property; return acc; }, {}); @@ -120,8 +118,8 @@ async function relationalQuery( ) { const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(','); const eventWhere = events.map((a, i) => `{{event${i}}}`).join(','); - const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i + 999}}}`).join(','); - const eventDataKeyWhere = eventData.map((a, i) => `{{property${i + 999}}}`).join(','); + const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i}}}`).join(','); + const eventDataKeyWhere = eventData.map((a, i) => `{{property${i}}}`).join(','); return { urls: `and url_path in (${urlWhere})`, From 8e00a278db2788bf01822a480de1e3d0e2f43074 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 21 May 2024 11:14:38 -0700 Subject: [PATCH 4/4] fix eent query --- src/queries/analytics/reports/getGoals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/analytics/reports/getGoals.ts index 21ab2f7f..83b0ce97 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/analytics/reports/getGoals.ts @@ -87,7 +87,7 @@ async function relationalQuery( .join('\n') .slice(0, -1); const eventColumns = events - .map((a, i) => `COUNT(CASE WHEN url_path = {{event${i}}} THEN 1 END) AS EVENT${i},`) + .map((a, i) => `COUNT(CASE WHEN event_name = {{event${i}}} THEN 1 END) AS EVENT${i},`) .join('\n') .slice(0, -1); const eventDataColumns = eventData