mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-01 20:39:44 +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({
|
export function GoalsAddForm({
|
||||||
type: defaultType = 'url',
|
type: defaultType = 'url',
|
||||||
value: defaultValue = '',
|
value: defaultValue = '',
|
||||||
|
property: defaultProperty = '',
|
||||||
|
operator: defaultAggregae = null,
|
||||||
goal: defaultGoal = 10,
|
goal: defaultGoal = 10,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
type?: string;
|
type?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
operator?: string;
|
||||||
|
property?: string;
|
||||||
goal?: number;
|
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 [type, setType] = useState(defaultType);
|
||||||
const [value, setValue] = useState(defaultValue);
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const [operator, setOperator] = useState(defaultAggregae);
|
||||||
|
const [property, setProperty] = useState(defaultProperty);
|
||||||
const [goal, setGoal] = useState(defaultGoal);
|
const [goal, setGoal] = useState(defaultGoal);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const items = [
|
const items = [
|
||||||
{ label: formatMessage(labels.url), value: 'url' },
|
{ label: formatMessage(labels.url), value: 'url' },
|
||||||
{ label: formatMessage(labels.event), value: 'event' },
|
{ 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 isDisabled = !type || !value;
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onChange({ type, value, goal });
|
onChange(
|
||||||
|
type === 'event-data' ? { type, value, goal, operator, property } : { type, value, goal },
|
||||||
|
);
|
||||||
setValue('');
|
setValue('');
|
||||||
|
setProperty('');
|
||||||
setGoal(10);
|
setGoal(10);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,6 +66,10 @@ export function GoalsAddForm({
|
|||||||
return items.find(item => item.value === value)?.label;
|
return items.find(item => item.value === value)?.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderoperatorValue = (value: any) => {
|
||||||
|
return operators.find(item => item.value === value)?.label;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox direction="column" gap={10}>
|
<Flexbox direction="column" gap={10}>
|
||||||
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||||
@ -70,6 +95,31 @@ export function GoalsAddForm({
|
|||||||
/>
|
/>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</FormRow>
|
</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)}>
|
<FormRow label={formatMessage(labels.goal)}>
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -11,19 +11,36 @@ export function GoalsChart({ className }: { className?: string; isLoading?: bool
|
|||||||
|
|
||||||
const { data } = report || {};
|
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 (
|
return (
|
||||||
<div className={classNames(styles.chart, className)}>
|
<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;
|
const percent = result > goal ? 100 : (result / goal) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={styles.goal}>
|
<div key={index} className={styles.goal}>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<span className={styles.label}>
|
<span className={styles.label}>{formatMessage(getLabel(type))}</span>
|
||||||
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
|
<span className={styles.item}>{`${value}${
|
||||||
</span>
|
type === 'event-data' ? `:(${operator}):${property}` : ''
|
||||||
<span className={styles.item}>{value}</span>
|
}`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.track}>
|
<div className={styles.track}>
|
||||||
<div
|
<div
|
||||||
|
@ -4,6 +4,16 @@
|
|||||||
font-weight: 600;
|
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 {
|
.goal {
|
||||||
color: var(--blue900);
|
color: var(--blue900);
|
||||||
background-color: var(--blue100);
|
background-color: var(--blue100);
|
||||||
@ -11,4 +21,5 @@
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { formatNumber } from 'lib/format';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Flexbox,
|
||||||
Form,
|
Form,
|
||||||
FormButtons,
|
FormButtons,
|
||||||
FormRow,
|
FormRow,
|
||||||
@ -79,17 +80,34 @@ export function GoalsParameters() {
|
|||||||
<BaseParameters allowWebsiteSelect={!id} />
|
<BaseParameters allowWebsiteSelect={!id} />
|
||||||
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}>
|
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}>
|
||||||
<ParameterList>
|
<ParameterList>
|
||||||
{goals.map((goal: { type: string; value: string; goal: number }, index: number) => {
|
{goals.map(
|
||||||
|
(
|
||||||
|
goal: {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
goal: number;
|
||||||
|
operator?: string;
|
||||||
|
property?: string;
|
||||||
|
},
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<PopupTrigger key={index}>
|
<PopupTrigger key={index}>
|
||||||
<ParameterList.Item
|
<ParameterList.Item
|
||||||
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||||
onRemove={() => handleRemoveGoals(index)}
|
onRemove={() => handleRemoveGoals(index)}
|
||||||
>
|
>
|
||||||
|
<Flexbox direction="column" gap={5}>
|
||||||
<div className={styles.value}>{goal.value}</div>
|
<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}>
|
<div className={styles.goal}>
|
||||||
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
||||||
</div>
|
</div>
|
||||||
|
</Flexbox>
|
||||||
</ParameterList.Item>
|
</ParameterList.Item>
|
||||||
<Popup alignment="start">
|
<Popup alignment="start">
|
||||||
{(close: () => void) => (
|
{(close: () => void) => (
|
||||||
@ -98,6 +116,8 @@ export function GoalsParameters() {
|
|||||||
type={goal.type}
|
type={goal.type}
|
||||||
value={goal.value}
|
value={goal.value}
|
||||||
goal={goal.goal}
|
goal={goal.goal}
|
||||||
|
operator={goal.operator}
|
||||||
|
property={goal.property}
|
||||||
onChange={handleUpdateGoals.bind(null, close, index)}
|
onChange={handleUpdateGoals.bind(null, close, index)}
|
||||||
/>
|
/>
|
||||||
</PopupForm>
|
</PopupForm>
|
||||||
@ -105,7 +125,8 @@ export function GoalsParameters() {
|
|||||||
</Popup>
|
</Popup>
|
||||||
</PopupTrigger>
|
</PopupTrigger>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</ParameterList>
|
</ParameterList>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
|
@ -95,6 +95,9 @@ export const labels = defineMessages({
|
|||||||
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
||||||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
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' },
|
event: { id: 'label.event', defaultMessage: 'Event' },
|
||||||
events: { id: 'label.events', defaultMessage: 'Events' },
|
events: { id: 'label.events', defaultMessage: 'Events' },
|
||||||
query: { id: 'label.query', defaultMessage: 'Query' },
|
query: { id: 'label.query', defaultMessage: 'Query' },
|
||||||
@ -107,6 +110,7 @@ export const labels = defineMessages({
|
|||||||
views: { id: 'label.views', defaultMessage: 'Views' },
|
views: { id: 'label.views', defaultMessage: 'Views' },
|
||||||
none: { id: 'label.none', defaultMessage: 'None' },
|
none: { id: 'label.none', defaultMessage: 'None' },
|
||||||
clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' },
|
clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' },
|
||||||
|
property: { id: 'label.property', defaultMessage: 'Property' },
|
||||||
today: { id: 'label.today', defaultMessage: 'Today' },
|
today: { id: 'label.today', defaultMessage: 'Today' },
|
||||||
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
||||||
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
||||||
@ -178,8 +182,6 @@ export const labels = defineMessages({
|
|||||||
before: { id: 'label.before', defaultMessage: 'Before' },
|
before: { id: 'label.before', defaultMessage: 'Before' },
|
||||||
after: { id: 'label.after', defaultMessage: 'After' },
|
after: { id: 'label.after', defaultMessage: 'After' },
|
||||||
total: { id: 'label.total', defaultMessage: 'Total' },
|
total: { id: 'label.total', defaultMessage: 'Total' },
|
||||||
sum: { id: 'label.sum', defaultMessage: 'Sum' },
|
|
||||||
average: { id: 'label.average', defaultMessage: 'Average' },
|
|
||||||
min: { id: 'label.min', defaultMessage: 'Min' },
|
min: { id: 'label.min', defaultMessage: 'Min' },
|
||||||
max: { id: 'label.max', defaultMessage: 'Max' },
|
max: { id: 'label.max', defaultMessage: 'Max' },
|
||||||
unique: { id: 'label.unique', defaultMessage: 'Unique' },
|
unique: { id: 'label.unique', defaultMessage: 'Unique' },
|
||||||
@ -220,6 +222,10 @@ export const labels = defineMessages({
|
|||||||
id: 'message.viewed-page',
|
id: 'message.viewed-page',
|
||||||
defaultMessage: 'Viewed page',
|
defaultMessage: 'Viewed page',
|
||||||
},
|
},
|
||||||
|
collectedData: {
|
||||||
|
id: 'message.collected-data',
|
||||||
|
defaultMessage: 'Collected data',
|
||||||
|
},
|
||||||
triggeredEvent: {
|
triggeredEvent: {
|
||||||
id: 'message.triggered-event',
|
id: 'message.triggered-event',
|
||||||
defaultMessage: 'Triggered event',
|
defaultMessage: 'Triggered event',
|
||||||
@ -241,7 +247,6 @@ export const labels = defineMessages({
|
|||||||
id: 'label.goals-description',
|
id: 'label.goals-description',
|
||||||
defaultMessage: 'Track your goals for pageviews and events.',
|
defaultMessage: 'Track your goals for pageviews and events.',
|
||||||
},
|
},
|
||||||
count: { id: 'label.count', defaultMessage: 'Count' },
|
|
||||||
journey: { id: 'label.journey', defaultMessage: 'Journey' },
|
journey: { id: 'label.journey', defaultMessage: 'Journey' },
|
||||||
journeyDescription: {
|
journeyDescription: {
|
||||||
id: 'label.journey-description',
|
id: 'label.journey-description',
|
||||||
|
@ -28,9 +28,23 @@ const schema = {
|
|||||||
.array()
|
.array()
|
||||||
.of(
|
.of(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
type: yup.string().required(),
|
type: yup
|
||||||
|
.string()
|
||||||
|
.matches(/url|event|event-data/i)
|
||||||
|
.required(),
|
||||||
value: yup.string().required(),
|
value: yup.string().required(),
|
||||||
goal: yup.number().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)
|
.min(1)
|
||||||
|
@ -8,7 +8,7 @@ export async function getGoals(
|
|||||||
criteria: {
|
criteria: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: 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: {
|
criteria: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
goals: { type: string; value: string; goal: number }[];
|
goals: { type: string; value: string; goal: number; operator?: string }[];
|
||||||
},
|
},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { startDate, endDate, goals } = criteria;
|
const { startDate, endDate, goals } = criteria;
|
||||||
const { rawQuery } = prisma;
|
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 urls = goals.filter(a => a.type === 'url');
|
||||||
const events = goals.filter(a => a.type === 'event');
|
const events = goals.filter(a => a.type === 'event');
|
||||||
|
const eventData = goals.filter(a => a.type === 'event-data');
|
||||||
|
|
||||||
const hasUrl = urls.length > 0;
|
const hasUrl = urls.length > 0;
|
||||||
const hasEvent = events.length > 0;
|
const hasEvent = events.length > 0;
|
||||||
|
const hasEventData = eventData.length > 0;
|
||||||
|
|
||||||
function getParameters(
|
function getParameters(
|
||||||
urls: { type: string; value: string; goal: number }[],
|
urls: { type: string; value: string; goal: number }[],
|
||||||
events: { 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) => {
|
const urlParam = urls.reduce((acc, cv, i) => {
|
||||||
acc[`${cv.type}${i}`] = cv.value;
|
acc[`${cv.type}${i}`] = cv.value;
|
||||||
@ -145,41 +58,258 @@ async function clickhouseQuery(
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const eventDataParam = eventData.reduce((acc, cv, i) => {
|
||||||
|
acc[`eventData${i}`] = cv.value;
|
||||||
|
acc[`property${i}`] = cv.property;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
urls: { ...urlParam, startDate, endDate, websiteId },
|
urls: { ...urlParam, startDate, endDate, websiteId },
|
||||||
events: { ...eventParam, startDate, endDate, websiteId },
|
events: { ...eventParam, startDate, endDate, websiteId },
|
||||||
|
eventData: { ...eventDataParam, startDate, endDate, websiteId },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColumns(
|
function getColumns(
|
||||||
urls: { type: string; value: string; goal: number }[],
|
urls: { type: string; value: string; goal: number }[],
|
||||||
events: { 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
|
const urlColumns = urls
|
||||||
.map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`)
|
.map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.slice(0, -1);
|
.slice(0, -1);
|
||||||
const eventColumns = events
|
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')
|
.join('\n')
|
||||||
.slice(0, -1);
|
.slice(0, -1);
|
||||||
|
|
||||||
return { url: urlColumns, events: eventColumns };
|
return { url: urlColumns, events: eventColumns, eventData: eventDataColumns };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhere(
|
function getWhere(
|
||||||
urls: { type: string; value: string; goal: number }[],
|
urls: { type: string; value: string; goal: number }[],
|
||||||
events: { 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 urlWhere = urls.map((a, i) => `{url${i}:String}`).join(',');
|
||||||
const eventWhere = events.map((a, i) => `{event${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 parameters = getParameters(urls, events, eventData);
|
||||||
const columns = getColumns(urls, events);
|
const columns = getColumns(urls, events, eventData);
|
||||||
const where = getWhere(urls, events);
|
const where = getWhere(urls, events, eventData);
|
||||||
|
|
||||||
const urlResults = hasUrl
|
const urlResults = hasUrl
|
||||||
? await rawQuery<any>(
|
? 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