From 79a93ed9fc3e99fce9f89456796acb9bd4d0c0d5 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 20 May 2024 21:10:46 -0700 Subject: [PATCH] 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]; }