mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-01 12:29:35 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
6b03935fca
@ -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 (
|
||||
<Flexbox direction="column" gap={10}>
|
||||
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||
@ -70,6 +95,31 @@ export function GoalsAddForm({
|
||||
/>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
{type === 'event-data' && (
|
||||
<FormRow label={formatMessage(labels.property)}>
|
||||
<Flexbox gap={10}>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={operators}
|
||||
value={operator}
|
||||
renderValue={renderoperatorValue}
|
||||
onChange={(value: any) => setOperator(value)}
|
||||
>
|
||||
{({ value, label }) => {
|
||||
return <Item key={value}>{label}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
value={property}
|
||||
onChange={e => handleChange(e, setProperty)}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
)}
|
||||
<FormRow label={formatMessage(labels.goal)}>
|
||||
<Flexbox gap={10}>
|
||||
<TextField
|
||||
|
@ -11,19 +11,36 @@ export function GoalsChart({ className }: { className?: string; isLoading?: bool
|
||||
|
||||
const { data } = report || {};
|
||||
|
||||
const getLabel = type => {
|
||||
let label = '';
|
||||
switch (type) {
|
||||
case 'url':
|
||||
label = labels.viewedPage;
|
||||
break;
|
||||
case 'event':
|
||||
label = labels.triggeredEvent;
|
||||
break;
|
||||
default:
|
||||
label = labels.collectedData;
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.chart, className)}>
|
||||
{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 (
|
||||
<div key={index} className={styles.goal}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.label}>
|
||||
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
|
||||
</span>
|
||||
<span className={styles.item}>{value}</span>
|
||||
<span className={styles.label}>{formatMessage(getLabel(type))}</span>
|
||||
<span className={styles.item}>{`${value}${
|
||||
type === 'event-data' ? `:(${operator}):${property}` : ''
|
||||
}`}</span>
|
||||
</div>
|
||||
<div className={styles.track}>
|
||||
<div
|
||||
|
@ -4,6 +4,16 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.eventData {
|
||||
color: var(--orange900);
|
||||
background-color: var(--orange100);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.goal {
|
||||
color: var(--blue900);
|
||||
background-color: var(--blue100);
|
||||
@ -11,4 +21,5 @@
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { formatNumber } from 'lib/format';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Flexbox,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormRow,
|
||||
@ -79,33 +80,53 @@ export function GoalsParameters() {
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}>
|
||||
<ParameterList>
|
||||
{goals.map((goal: { type: string; value: string; goal: number }, index: number) => {
|
||||
return (
|
||||
<PopupTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||
onRemove={() => handleRemoveGoals(index)}
|
||||
>
|
||||
<div className={styles.value}>{goal.value}</div>
|
||||
<div className={styles.goal}>
|
||||
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
||||
</div>
|
||||
</ParameterList.Item>
|
||||
<Popup alignment="start">
|
||||
{(close: () => void) => (
|
||||
<PopupForm>
|
||||
<GoalsAddForm
|
||||
type={goal.type}
|
||||
value={goal.value}
|
||||
goal={goal.goal}
|
||||
onChange={handleUpdateGoals.bind(null, close, index)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
})}
|
||||
{goals.map(
|
||||
(
|
||||
goal: {
|
||||
type: string;
|
||||
value: string;
|
||||
goal: number;
|
||||
operator?: string;
|
||||
property?: string;
|
||||
},
|
||||
index: number,
|
||||
) => {
|
||||
return (
|
||||
<PopupTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||
onRemove={() => handleRemoveGoals(index)}
|
||||
>
|
||||
<Flexbox direction="column" gap={5}>
|
||||
<div className={styles.value}>{goal.value}</div>
|
||||
{goal.type === 'event-data' && (
|
||||
<div className={styles.eventData}>
|
||||
{formatMessage(labels[goal.operator])}: {goal.property}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.goal}>
|
||||
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
||||
</div>
|
||||
</Flexbox>
|
||||
</ParameterList.Item>
|
||||
<Popup alignment="start">
|
||||
{(close: () => void) => (
|
||||
<PopupForm>
|
||||
<GoalsAddForm
|
||||
type={goal.type}
|
||||
value={goal.value}
|
||||
goal={goal.goal}
|
||||
operator={goal.operator}
|
||||
property={goal.property}
|
||||
onChange={handleUpdateGoals.bind(null, close, index)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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<any> {
|
||||
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<any>(
|
||||
@ -221,5 +351,25 @@ async function clickhouseQuery(
|
||||
})
|
||||
: [];
|
||||
|
||||
return [...urlResults, ...eventResults];
|
||||
const eventDataResults = hasEventData
|
||||
? await rawQuery<any>(
|
||||
`
|
||||
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];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user