- {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 6231c6f3..17f7b035 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..83b0ce97 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,117 +23,30 @@ 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;
const { rawQuery } = prisma;
- const hasUrl = goals.some(a => a.type === 'url');
- const hasEvent = goals.some(a => a.type === 'event');
-
- 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 events = goals
- .filter(a => a.type === 'event')
- .reduce((acc, cv, i) => {
- acc[`${cv.type}${i}`] = cv.value;
- return acc;
- }, {});
-
- return {
- urls: { ...urls, startDate, endDate, websiteId },
- events: { ...events, 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');
-
- return { urls, events };
- }
-
- 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(',');
-
- return { urls: `and url_path in (${urls})`, events: `and event_name in (${events})` };
- }
-
- const parameters = getParameters(goals);
- const columns = getColumns(goals);
- const where = getWhere(goals);
-
- const urls = hasUrl
- ? await rawQuery(
- `
- select
- ${columns.urls}
- from website_event
- where websiteId = {{websiteId::uuid}}
- ${where.urls}
- and created_at between {{startDate}} and {{endDate}}
- `,
- parameters.urls,
- )
- : [];
-
- const events = hasEvent
- ? await rawQuery(
- `
- select
- ${columns.events}
- from website_event
- where websiteId = {{websiteId::uuid}}
- ${where.events}
- and created_at between {{startDate}} and {{endDate}}
- `,
- parameters.events,
- )
- : [];
-
- return [...urls, ...events];
-}
-
-async function clickhouseQuery(
- websiteId: string,
- criteria: {
- startDate: Date;
- endDate: Date;
- goals: { type: string; value: string; goal: number }[];
- },
-): Promise<{ type: string; value: string; goal: number; result: number }[]> {
- const { startDate, endDate, goals } = criteria;
- const { rawQuery } = clickhouse;
-
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 +58,258 @@ 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) => `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 event_name = {{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: 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}}}`).join(',');
+ const eventWhere = events.map((a, i) => `{{event${i}}}`).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})`,
+ events: `and event_name in (${eventWhere})`,
+ eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`,
+ };
+ }
+
+ const parameters = getParameters(urls, events, eventData);
+ const columns = getColumns(urls, events, eventData);
+ const where = getWhere(urls, events, eventData);
+
+ const urlResults = hasUrl
+ ? await rawQuery(
+ `
+ select
+ ${columns.urls}
+ from website_event
+ 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 eventResults = hasEvent
+ ? await rawQuery(
+ `
+ select
+ ${columns.events}
+ from website_event
+ 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]) };
+ });
+ })
+ : [];
+
+ 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(
+ websiteId: string,
+ criteria: {
+ startDate: Date;
+ endDate: Date;
+ 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;
+ const { rawQuery } = clickhouse;
+
+ 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;
+ 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;
+ 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 +351,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];
}