mirror of
https://github.com/umami-software/umami.git
synced 2026-02-25 06:55:35 +01:00
This commit is contained in:
parent
bbf569e8bc
commit
5144aff9cd
10 changed files with 822 additions and 227 deletions
|
|
@ -12,7 +12,7 @@ import { useLoginQuery, useMessages, useUpdateQuery, useUser } from '@/component
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) {
|
export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) {
|
||||||
const { t, labels, messages, getMessage } = useMessages();
|
const { t, labels, messages, getErrorMessage } = useMessages();
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
const { user: login } = useLoginQuery();
|
const { user: login } = useLoginQuery();
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getMessage(error?.code)} values={user}>
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} values={user}>
|
||||||
<FormField name="username" label={t(labels.username)}>
|
<FormField name="username" label={t(labels.username)}>
|
||||||
<TextField data-test="input-username" />
|
<TextField data-test="input-username" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
import { Column, Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
||||||
import classNames from 'classnames';
|
import { useMemo, useState } from 'react';
|
||||||
import { colord } from 'colord';
|
import { GridRow } from '@/components/common/GridRow';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { BarChart } from '@/components/charts/BarChart';
|
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
import { useDateRange, useMessages, useResultQuery } from '@/components/hooks';
|
||||||
import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
|
||||||
import { CurrencySelect } from '@/components/input/CurrencySelect';
|
import { CurrencySelect } from '@/components/input/CurrencySelect';
|
||||||
import { ListTable } from '@/components/metrics/ListTable';
|
import { ListTable } from '@/components/metrics/ListTable';
|
||||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
|
import { MetricLabel } from '@/components/metrics/MetricLabel';
|
||||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { RevenueChart } from '@/components/metrics/RevenueChart';
|
||||||
import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
|
import { CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
|
||||||
import { generateTimeSeries } from '@/lib/date';
|
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
import { getItem, setItem } from '@/lib/storage';
|
import { getItem, setItem } from '@/lib/storage';
|
||||||
|
|
||||||
|
|
@ -35,83 +32,49 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const { locale, dateLocale } = useLocale();
|
const { compare, isAllTime } = useDateRange();
|
||||||
const { countryNames } = useCountryNames(locale);
|
|
||||||
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
const { data, error, isLoading } = useResultQuery<any>('revenue', {
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
currency,
|
currency,
|
||||||
|
compare,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCountryName = useCallback(
|
|
||||||
({ label: code }) => (
|
|
||||||
<Row className={classNames(locale)} gap>
|
|
||||||
<TypeIcon type="country" value={code} />
|
|
||||||
<Text>{countryNames[code] || t(labels.unknown)}</Text>
|
|
||||||
</Row>
|
|
||||||
),
|
|
||||||
[countryNames, locale],
|
|
||||||
);
|
|
||||||
|
|
||||||
const chartData: any = useMemo(() => {
|
|
||||||
if (!data) return [];
|
|
||||||
|
|
||||||
const map = (data.chart as any[]).reduce((obj, { x, t, y }) => {
|
|
||||||
if (!obj[x]) {
|
|
||||||
obj[x] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
obj[x].push({ x: t, y });
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
datasets: Object.keys(map).map((key, index) => {
|
|
||||||
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
|
||||||
return {
|
|
||||||
label: key,
|
|
||||||
data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
|
|
||||||
lineTension: 0,
|
|
||||||
backgroundColor: color.alpha(0.6).toRgbString(),
|
|
||||||
borderColor: color.alpha(0.7).toRgbString(),
|
|
||||||
borderWidth: 1,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}, [data, startDate, endDate, unit]);
|
|
||||||
|
|
||||||
const metrics = useMemo(() => {
|
const metrics = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
const { sum, count, unique_count } = data.total;
|
const { sum, count, average, unique_count, comparison } = data.total;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: sum,
|
value: sum,
|
||||||
label: t(labels.total),
|
label: t(labels.total),
|
||||||
formatValue: n => formatLongCurrency(n, currency),
|
change: comparison ? sum - comparison.sum : 0,
|
||||||
|
formatValue: (n: number) => formatLongCurrency(n, currency),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: count ? sum / count : 0,
|
value: average,
|
||||||
label: t(labels.average),
|
label: t(labels.average),
|
||||||
formatValue: n => formatLongCurrency(n, currency),
|
change: comparison ? average - comparison.average : 0,
|
||||||
|
formatValue: (n: number) => formatLongCurrency(n, currency),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: count,
|
value: count,
|
||||||
label: t(labels.transactions),
|
label: t(labels.transactions),
|
||||||
|
change: comparison ? count - comparison.count : 0,
|
||||||
formatValue: formatLongNumber,
|
formatValue: formatLongNumber,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: unique_count,
|
value: unique_count,
|
||||||
label: t(labels.uniqueCustomers),
|
label: t(labels.uniqueCustomers),
|
||||||
|
change: comparison ? unique_count - comparison.unique_count : 0,
|
||||||
formatValue: formatLongNumber,
|
formatValue: formatLongNumber,
|
||||||
},
|
},
|
||||||
] as any;
|
] as any;
|
||||||
}, [data, locale]);
|
}, [data]);
|
||||||
|
|
||||||
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
|
const renderLabel = (type: string) => (data: any) => <MetricLabel type={type} data={data} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
|
|
@ -122,37 +85,119 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
{data && (
|
{data && (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<MetricsBar>
|
<MetricsBar>
|
||||||
{metrics?.map(({ label, value, formatValue }) => {
|
{metrics?.map(({ label, value, change, formatValue }) => {
|
||||||
return (
|
return (
|
||||||
<MetricCard key={label} value={value} label={label} formatValue={formatValue} />
|
<MetricCard
|
||||||
|
key={label}
|
||||||
|
value={value}
|
||||||
|
label={label}
|
||||||
|
change={change}
|
||||||
|
formatValue={formatValue}
|
||||||
|
showChange={!isAllTime}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</MetricsBar>
|
</MetricsBar>
|
||||||
<Panel>
|
<Panel>
|
||||||
<BarChart
|
<RevenueChart
|
||||||
chartData={chartData}
|
data={data.chart}
|
||||||
|
unit={unit}
|
||||||
minDate={startDate}
|
minDate={startDate}
|
||||||
maxDate={endDate}
|
maxDate={endDate}
|
||||||
unit={unit}
|
|
||||||
stacked={true}
|
|
||||||
currency={currency}
|
currency={currency}
|
||||||
renderXLabel={renderXLabel}
|
|
||||||
height="400px"
|
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<Grid gap="3">
|
||||||
|
<GridRow layout="two">
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Heading size="2xl">{t(labels.sources)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="referrer">{t(labels.referrers)}</Tab>
|
||||||
|
<Tab id="channel">{t(labels.channels)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="referrer">
|
||||||
<ListTable
|
<ListTable
|
||||||
title={t(labels.country)}
|
title={t(labels.referrer)}
|
||||||
metric={t(labels.revenue)}
|
metric={t(labels.revenue)}
|
||||||
data={data?.country.map(({ name, value }: { name: string; value: number }) => ({
|
data={data?.referrer.map(
|
||||||
|
({ name, value }: { name: string; value: number }) => ({
|
||||||
label: name,
|
label: name,
|
||||||
count: Number(value),
|
count: Number(value),
|
||||||
percent: (value / data?.total.sum) * 100,
|
percent: (value / data?.total.sum) * 100,
|
||||||
}))}
|
}),
|
||||||
|
)}
|
||||||
currency={currency}
|
currency={currency}
|
||||||
renderLabel={renderCountryName}
|
renderLabel={renderLabel('referrer')}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="channel">
|
||||||
|
<ListTable
|
||||||
|
title={t(labels.channel)}
|
||||||
|
metric={t(labels.revenue)}
|
||||||
|
data={data?.channel.map(
|
||||||
|
({ name, value }: { name: string; value: number }) => ({
|
||||||
|
label: name,
|
||||||
|
count: Number(value),
|
||||||
|
percent: (value / data?.total.sum) * 100,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
currency={currency}
|
||||||
|
renderLabel={renderLabel('channel')}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2xl">{t(labels.location)}</Heading>
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab id="country">{t(labels.countries)}</Tab>
|
||||||
|
<Tab id="region">{t(labels.regions)}</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel id="country">
|
||||||
|
<ListTable
|
||||||
|
title={t(labels.country)}
|
||||||
|
metric={t(labels.revenue)}
|
||||||
|
data={data?.country.map(
|
||||||
|
({ name, value }: { name: string; value: number }) => ({
|
||||||
|
label: name,
|
||||||
|
count: Number(value),
|
||||||
|
percent: (value / data?.total.sum) * 100,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
currency={currency}
|
||||||
|
renderLabel={renderLabel('country')}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel id="region">
|
||||||
|
<ListTable
|
||||||
|
title={t(labels.region)}
|
||||||
|
metric={t(labels.revenue)}
|
||||||
|
data={data?.region.map(
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
country,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
country: string;
|
||||||
|
}) => ({
|
||||||
|
label: name,
|
||||||
|
country,
|
||||||
|
count: Number(value),
|
||||||
|
percent: (value / data?.total.sum) * 100,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
currency={currency}
|
||||||
|
renderLabel={renderLabel('region')}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
</Column>
|
</Column>
|
||||||
)}
|
)}
|
||||||
</LoadingPanel>
|
</LoadingPanel>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { getCompareDate } from '@/lib/date';
|
||||||
import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
|
import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { reportResultSchema } from '@/lib/schema';
|
import { reportResultSchema } from '@/lib/schema';
|
||||||
import { canViewWebsite } from '@/permissions';
|
import { canViewWebsite } from '@/permissions';
|
||||||
import { getRevenue, type RevenuParameters } from '@/queries/sql/reports/getRevenue';
|
import { getRevenue, type RevenuParameters } from '@/queries/sql/reports/getRevenue';
|
||||||
|
import { getRevenueMetrics } from '@/queries/sql/reports/getRevenueMetrics';
|
||||||
|
import { getRevenueStats } from '@/queries/sql/reports/getRevenueStats';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const { auth, body, error } = await parseRequest(request, reportResultSchema);
|
const { auth, body, error } = await parseRequest(request, reportResultSchema);
|
||||||
|
|
@ -20,7 +23,19 @@ export async function POST(request: Request) {
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId);
|
||||||
|
|
||||||
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
|
const [{ chart }, total, metrics] = await Promise.all([
|
||||||
|
getRevenue(websiteId, parameters as RevenuParameters, filters),
|
||||||
|
getRevenueStats(websiteId, parameters as RevenuParameters, filters),
|
||||||
|
getRevenueMetrics(websiteId, parameters as RevenuParameters, filters),
|
||||||
|
]);
|
||||||
|
|
||||||
return json(data);
|
const { compare = 'prev' } = parameters as RevenuParameters;
|
||||||
|
const { startDate, endDate } = getCompareDate(compare, parameters.startDate, parameters.endDate);
|
||||||
|
const comparison = await getRevenueStats(
|
||||||
|
websiteId,
|
||||||
|
{ ...(parameters as RevenuParameters), startDate, endDate },
|
||||||
|
filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
return json({ chart, total: { ...total, comparison }, ...metrics });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
src/components/metrics/RevenueChart.tsx
Normal file
60
src/components/metrics/RevenueChart.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { colord } from 'colord';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { BarChart } from '@/components/charts/BarChart';
|
||||||
|
import { useLocale } from '@/components/hooks';
|
||||||
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
|
import { CHART_COLORS } from '@/lib/constants';
|
||||||
|
import { generateTimeSeries } from '@/lib/date';
|
||||||
|
|
||||||
|
export interface RevenueChartProps {
|
||||||
|
data: { x: string; t: string; y: number; count: number }[];
|
||||||
|
unit: string;
|
||||||
|
minDate: Date;
|
||||||
|
maxDate: Date;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RevenueChart({ data, unit, minDate, maxDate, currency }: RevenueChartProps) {
|
||||||
|
const { locale, dateLocale } = useLocale();
|
||||||
|
|
||||||
|
const chartData: any = useMemo(() => {
|
||||||
|
if (!data?.length) return { datasets: [] };
|
||||||
|
|
||||||
|
const map = data.reduce(
|
||||||
|
(obj, { x, t, y }) => {
|
||||||
|
if (!obj[x]) obj[x] = [];
|
||||||
|
obj[x].push({ x: t, y });
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{} as Record<string, { x: string; y: number }[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
datasets: Object.keys(map).map((key, index) => {
|
||||||
|
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
||||||
|
return {
|
||||||
|
label: key,
|
||||||
|
data: generateTimeSeries(map[key], minDate, maxDate, unit, dateLocale),
|
||||||
|
backgroundColor: color.alpha(0.6).toRgbString(),
|
||||||
|
borderColor: color.alpha(0.7).toRgbString(),
|
||||||
|
borderWidth: 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [data, minDate, maxDate, unit, dateLocale]);
|
||||||
|
|
||||||
|
const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BarChart
|
||||||
|
chartData={chartData}
|
||||||
|
unit={unit}
|
||||||
|
minDate={minDate}
|
||||||
|
maxDate={maxDate}
|
||||||
|
currency={currency}
|
||||||
|
stacked={true}
|
||||||
|
renderXLabel={renderXLabel}
|
||||||
|
height="400px"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -229,6 +229,7 @@ export const revenueReportSchema = z.object({
|
||||||
unit: unitParam.optional(),
|
unit: unitParam.optional(),
|
||||||
timezone: timezoneParam.optional(),
|
timezone: timezoneParam.optional(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
|
compare: z.enum(['prev', 'yoy']).optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ async function relationalQuery(
|
||||||
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
||||||
when ${toPostgresPositionClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
when ${toPostgresPositionClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
||||||
when ${toPostgresPositionClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
when ${toPostgresPositionClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
||||||
|
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||||
else '' end as "name",
|
else '' end as "name",
|
||||||
session_id,
|
session_id,
|
||||||
visit_id,
|
visit_id,
|
||||||
|
|
@ -136,30 +137,32 @@ async function clickhouseQuery(
|
||||||
sum(if(t.c = 1, 1, 0)) as "bounces",
|
sum(if(t.c = 1, 1, 0)) as "bounces",
|
||||||
sum(max_time-min_time) as "totaltime"
|
sum(max_time-min_time) as "totaltime"
|
||||||
from (
|
from (
|
||||||
select case when multiSearchAny(utm_medium, ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
select
|
||||||
|
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
||||||
case
|
case
|
||||||
when referrer_domain = '' and url_query = '' then 'direct'
|
when referrer_domain = '' and url_query = '' then 'direct'
|
||||||
when multiSearchAny(url_query, [${toClickHouseStringArray(
|
when multiSearchAny(lower(url_query), [${toClickHouseStringArray(
|
||||||
PAID_AD_PARAMS,
|
PAID_AD_PARAMS,
|
||||||
)}]) != 0 then 'paidAds'
|
)}]) != 0 then 'paidAds'
|
||||||
when multiSearchAny(utm_medium, ['referral', 'app','link']) != 0 then 'referral'
|
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||||
when position(utm_medium, 'affiliate') > 0 then 'affiliate'
|
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||||
when position(utm_medium, 'sms') > 0 or position(utm_source, 'sms') > 0 then 'sms'
|
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SEARCH_DOMAINS,
|
SEARCH_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
|
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SOCIAL_DOMAINS,
|
SOCIAL_DOMAINS,
|
||||||
)}]) != 0 then concat(prefix, 'Social')
|
)}]) != 0 then concat(prefix, 'Social')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
EMAIL_DOMAINS,
|
EMAIL_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
|
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SHOPPING_DOMAINS,
|
SHOPPING_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
|
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
VIDEO_DOMAINS,
|
VIDEO_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
|
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||||
|
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||||
else '' end AS name,
|
else '' end AS name,
|
||||||
session_id,
|
session_id,
|
||||||
visit_id,
|
visit_id,
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
||||||
when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
||||||
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
||||||
|
wwhen referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||||
else '' end AS x,
|
else '' end AS x,
|
||||||
count(distinct session_id) y
|
count(distinct session_id) y
|
||||||
from prefix
|
from prefix
|
||||||
|
|
@ -90,30 +91,32 @@ async function clickhouseQuery(
|
||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
WITH channels as (
|
WITH channels as (
|
||||||
select case when multiSearchAny(utm_medium, ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
select
|
||||||
|
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
||||||
case
|
case
|
||||||
when referrer_domain = '' and url_query = '' then 'direct'
|
when referrer_domain = '' and url_query = '' then 'direct'
|
||||||
when multiSearchAny(url_query, [${toClickHouseStringArray(
|
when multiSearchAny(lower(url_query), [${toClickHouseStringArray(
|
||||||
PAID_AD_PARAMS,
|
PAID_AD_PARAMS,
|
||||||
)}]) != 0 then 'paidAds'
|
)}]) != 0 then 'paidAds'
|
||||||
when multiSearchAny(utm_medium, ['referral', 'app','link']) != 0 then 'referral'
|
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||||
when position(utm_medium, 'affiliate') > 0 then 'affiliate'
|
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||||
when position(utm_medium, 'sms') > 0 or position(utm_source, 'sms') > 0 then 'sms'
|
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SEARCH_DOMAINS,
|
SEARCH_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
|
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SOCIAL_DOMAINS,
|
SOCIAL_DOMAINS,
|
||||||
)}]) != 0 then concat(prefix, 'Social')
|
)}]) != 0 then concat(prefix, 'Social')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
EMAIL_DOMAINS,
|
EMAIL_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
|
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
SHOPPING_DOMAINS,
|
SHOPPING_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
|
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
VIDEO_DOMAINS,
|
VIDEO_DOMAINS,
|
||||||
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
|
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||||
|
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||||
else '' end AS x,
|
else '' end AS x,
|
||||||
count(distinct session_id) y
|
count(distinct session_id) y
|
||||||
from website_event
|
from website_event
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,7 @@ export interface RevenuParameters {
|
||||||
unit: string;
|
unit: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
}
|
compare?: string;
|
||||||
|
|
||||||
export interface RevenueResult {
|
|
||||||
chart: { x: string; t: string; y: number }[];
|
|
||||||
country: { name: string; value: number }[];
|
|
||||||
total: { sum: number; count: number; average: number; unique_count: number };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRevenue(
|
export async function getRevenue(
|
||||||
|
|
@ -30,7 +25,7 @@ async function relationalQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
parameters: RevenuParameters,
|
parameters: RevenuParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<RevenueResult> {
|
) {
|
||||||
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
||||||
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
||||||
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||||
|
|
@ -58,7 +53,8 @@ async function relationalQuery(
|
||||||
select
|
select
|
||||||
revenue.event_name x,
|
revenue.event_name x,
|
||||||
${getDateSQL('revenue.created_at', unit, timezone)} t,
|
${getDateSQL('revenue.created_at', unit, timezone)} t,
|
||||||
sum(revenue.revenue) y
|
sum(revenue.revenue) y,
|
||||||
|
count(revenue.event_id) count
|
||||||
from revenue
|
from revenue
|
||||||
${joinQuery}
|
${joinQuery}
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
|
|
@ -73,54 +69,14 @@ async function relationalQuery(
|
||||||
queryParams,
|
queryParams,
|
||||||
);
|
);
|
||||||
|
|
||||||
const country = await rawQuery(
|
return { chart };
|
||||||
`
|
|
||||||
select
|
|
||||||
session.country as "name",
|
|
||||||
sum(revenue) as "value"
|
|
||||||
from revenue
|
|
||||||
${joinQuery}
|
|
||||||
join session
|
|
||||||
on session.website_id = revenue.website_id
|
|
||||||
and session.session_id = revenue.session_id
|
|
||||||
${cohortQuery}
|
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
|
||||||
and upper(revenue.currency) = {{currency}}
|
|
||||||
${filterQuery}
|
|
||||||
group by session.country
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
const total = await rawQuery(
|
|
||||||
`
|
|
||||||
select
|
|
||||||
sum(revenue.revenue) as sum,
|
|
||||||
count(distinct revenue.event_id) as count,
|
|
||||||
count(distinct revenue.session_id) as unique_count
|
|
||||||
from revenue
|
|
||||||
${joinQuery}
|
|
||||||
${cohortQuery}
|
|
||||||
${joinSessionQuery}
|
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
|
||||||
and upper(revenue.currency) = {{currency}}
|
|
||||||
${filterQuery}
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
).then(result => result?.[0]);
|
|
||||||
|
|
||||||
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
|
|
||||||
|
|
||||||
return { chart, country, total };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickhouseQuery(
|
async function clickhouseQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
parameters: RevenuParameters,
|
parameters: RevenuParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<RevenueResult> {
|
) {
|
||||||
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
|
||||||
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
||||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
|
|
@ -143,18 +99,13 @@ async function clickhouseQuery(
|
||||||
and website_event.event_id = website_revenue.event_id`
|
and website_event.event_id = website_revenue.event_id`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const chart = await rawQuery<
|
const chart = await rawQuery<{ x: string; t: string; y: number; count: number }[]>(
|
||||||
{
|
|
||||||
x: string;
|
|
||||||
t: string;
|
|
||||||
y: number;
|
|
||||||
}[]
|
|
||||||
>(
|
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
website_revenue.event_name x,
|
website_revenue.event_name x,
|
||||||
${getDateSQL('website_revenue.created_at', unit, timezone)} t,
|
${getDateSQL('website_revenue.created_at', unit, timezone)} t,
|
||||||
sum(website_revenue.revenue) y
|
sum(website_revenue.revenue) y,
|
||||||
|
count(website_revenue.event_id) count
|
||||||
from website_revenue
|
from website_revenue
|
||||||
${joinQuery}
|
${joinQuery}
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
|
|
@ -168,59 +119,5 @@ async function clickhouseQuery(
|
||||||
queryParams,
|
queryParams,
|
||||||
);
|
);
|
||||||
|
|
||||||
const country = await rawQuery<
|
return { chart };
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
}[]
|
|
||||||
>(
|
|
||||||
`
|
|
||||||
select
|
|
||||||
website_event.country as "name",
|
|
||||||
sum(website_revenue.revenue) as "value"
|
|
||||||
from website_revenue
|
|
||||||
any left join (
|
|
||||||
select *
|
|
||||||
from website_event
|
|
||||||
where website_id = {websiteId:UUID}
|
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
and event_type = 2) website_event
|
|
||||||
on website_event.website_id = website_revenue.website_id
|
|
||||||
and website_event.session_id = website_revenue.session_id
|
|
||||||
and website_event.event_id = website_revenue.event_id
|
|
||||||
${cohortQuery}
|
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
and upper(website_revenue.currency) = {currency:String}
|
|
||||||
${filterQuery}
|
|
||||||
group by website_event.country
|
|
||||||
order by value desc
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
const total = await rawQuery<{
|
|
||||||
sum: number;
|
|
||||||
count: number;
|
|
||||||
unique_count: number;
|
|
||||||
}>(
|
|
||||||
`
|
|
||||||
select
|
|
||||||
sum(website_revenue.revenue) as sum,
|
|
||||||
uniqExact(website_revenue.event_id) as count,
|
|
||||||
uniqExact(website_revenue.session_id) as unique_count
|
|
||||||
from website_revenue
|
|
||||||
${joinQuery}
|
|
||||||
${cohortQuery}
|
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
and upper(website_revenue.currency) = {currency:String}
|
|
||||||
${filterQuery}
|
|
||||||
`,
|
|
||||||
queryParams,
|
|
||||||
).then(result => result?.[0]);
|
|
||||||
|
|
||||||
total.average = total.count > 0 ? total.sum / total.count : 0;
|
|
||||||
|
|
||||||
return { chart, country, total };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
451
src/queries/sql/reports/getRevenueMetrics.ts
Normal file
451
src/queries/sql/reports/getRevenueMetrics.ts
Normal file
|
|
@ -0,0 +1,451 @@
|
||||||
|
import clickhouse from '@/lib/clickhouse';
|
||||||
|
import {
|
||||||
|
EMAIL_DOMAINS,
|
||||||
|
PAID_AD_PARAMS,
|
||||||
|
SEARCH_DOMAINS,
|
||||||
|
SHOPPING_DOMAINS,
|
||||||
|
SOCIAL_DOMAINS,
|
||||||
|
VIDEO_DOMAINS,
|
||||||
|
} from '@/lib/constants';
|
||||||
|
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import type { QueryFilters } from '@/lib/types';
|
||||||
|
import type { RevenuParameters } from './getRevenue';
|
||||||
|
|
||||||
|
export interface RevenueMetricsResult {
|
||||||
|
country: { name: string; value: number }[];
|
||||||
|
region: { name: string; value: number; country: string }[];
|
||||||
|
referrer: { name: string; value: number }[];
|
||||||
|
channel: { name: string; value: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRevenueMetrics(
|
||||||
|
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
|
||||||
|
) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(
|
||||||
|
websiteId: string,
|
||||||
|
parameters: RevenuParameters,
|
||||||
|
filters: QueryFilters,
|
||||||
|
): Promise<RevenueMetricsResult> {
|
||||||
|
const { startDate, endDate, currency } = parameters;
|
||||||
|
const { rawQuery, parseFilters } = prisma;
|
||||||
|
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||||
|
...filters,
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
currency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinQuery =
|
||||||
|
filterQuery || cohortQuery
|
||||||
|
? `join (select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = revenue.website_id
|
||||||
|
and website_event.session_id = revenue.session_id
|
||||||
|
and website_event.event_id = revenue.event_id`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const country = await rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
session.country as "name",
|
||||||
|
sum(revenue) as "value"
|
||||||
|
from revenue
|
||||||
|
${joinQuery}
|
||||||
|
join session
|
||||||
|
on session.website_id = revenue.website_id
|
||||||
|
and session.session_id = revenue.session_id
|
||||||
|
${cohortQuery}
|
||||||
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
|
and upper(revenue.currency) = {{currency}}
|
||||||
|
${filterQuery}
|
||||||
|
group by session.country
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const region = await rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
session.country,
|
||||||
|
session.region as "name",
|
||||||
|
sum(revenue.revenue) as "value"
|
||||||
|
from revenue
|
||||||
|
${joinQuery}
|
||||||
|
join session
|
||||||
|
on session.website_id = revenue.website_id
|
||||||
|
and session.session_id = revenue.session_id
|
||||||
|
${cohortQuery}
|
||||||
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
|
and upper(revenue.currency) = {{currency}}
|
||||||
|
${filterQuery}
|
||||||
|
group by session.country, session.region
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrer = await rawQuery(
|
||||||
|
`
|
||||||
|
WITH events AS (
|
||||||
|
select
|
||||||
|
revenue.website_id,
|
||||||
|
revenue.session_id,
|
||||||
|
sum(revenue.revenue) as "value"
|
||||||
|
from revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
${joinSessionQuery}
|
||||||
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
|
and upper(revenue.currency) = {{currency}}
|
||||||
|
${filterQuery}
|
||||||
|
group by revenue.website_id, revenue.session_id),
|
||||||
|
|
||||||
|
revenue_data AS (
|
||||||
|
select
|
||||||
|
e.website_id,
|
||||||
|
e.session_id,
|
||||||
|
e.value,
|
||||||
|
we.min_date as created_at
|
||||||
|
from events e
|
||||||
|
join (
|
||||||
|
select session_id, min(created_at) as min_date
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
group by session_id
|
||||||
|
) we on we.session_id = e.session_id)
|
||||||
|
|
||||||
|
select
|
||||||
|
we.referrer_domain as "name",
|
||||||
|
sum(revenue_data.value) as "value"
|
||||||
|
from revenue_data
|
||||||
|
join (
|
||||||
|
select website_id, session_id, referrer_domain, created_at
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}) we
|
||||||
|
on we.website_id = revenue_data.website_id
|
||||||
|
and we.session_id = revenue_data.session_id
|
||||||
|
and we.created_at = revenue_data.created_at
|
||||||
|
group by we.referrer_domain
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const channel = await rawQuery(
|
||||||
|
`
|
||||||
|
WITH events AS (
|
||||||
|
select
|
||||||
|
revenue.website_id,
|
||||||
|
revenue.session_id,
|
||||||
|
sum(revenue.revenue) as "value"
|
||||||
|
from revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
${joinSessionQuery}
|
||||||
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
|
and upper(revenue.currency) = {{currency}}
|
||||||
|
${filterQuery}
|
||||||
|
group by revenue.website_id, revenue.session_id),
|
||||||
|
|
||||||
|
revenue_data AS (
|
||||||
|
select
|
||||||
|
e.website_id,
|
||||||
|
e.session_id,
|
||||||
|
e.value,
|
||||||
|
we.min_date as created_at
|
||||||
|
from events e
|
||||||
|
join (
|
||||||
|
select session_id, min(created_at) as min_date
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
group by session_id
|
||||||
|
) we on we.session_id = e.session_id),
|
||||||
|
|
||||||
|
revenue_prefix AS (
|
||||||
|
select
|
||||||
|
case when we.utm_medium ilike '%cp%' OR
|
||||||
|
we.utm_medium ilike '%ppc%' OR
|
||||||
|
we.utm_medium ilike '%retargeting%' OR
|
||||||
|
we.utm_medium ilike '%paid%' then 'paid' else 'organic' end AS prefix,
|
||||||
|
we.referrer_domain,
|
||||||
|
we.url_query,
|
||||||
|
we.utm_medium,
|
||||||
|
we.utm_source,
|
||||||
|
we.hostname,
|
||||||
|
r.value
|
||||||
|
from revenue_data r
|
||||||
|
join (
|
||||||
|
select website_id, session_id, referrer_domain, url_query, utm_medium, utm_source, hostname, created_at
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}) we
|
||||||
|
on we.website_id = r.website_id
|
||||||
|
and we.session_id = r.session_id
|
||||||
|
and we.created_at = r.created_at),
|
||||||
|
|
||||||
|
channels AS (
|
||||||
|
select
|
||||||
|
case
|
||||||
|
when referrer_domain = '' and url_query = '' then 'direct'
|
||||||
|
when ${toPostgresLikeClause('url_query', PAID_AD_PARAMS)} then 'paidAds'
|
||||||
|
when ${toPostgresLikeClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
|
||||||
|
when utm_medium ilike '%affiliate%' then 'affiliate'
|
||||||
|
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
|
||||||
|
when ${toPostgresLikeClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
|
||||||
|
when ${toPostgresLikeClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
|
||||||
|
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
|
||||||
|
when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
|
||||||
|
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
|
||||||
|
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||||
|
else 'Unknown' end AS "name",
|
||||||
|
value
|
||||||
|
from revenue_prefix)
|
||||||
|
|
||||||
|
select name, sum(value) as value
|
||||||
|
from channels
|
||||||
|
group by name
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { country, region, referrer, channel };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(
|
||||||
|
websiteId: string,
|
||||||
|
parameters: RevenuParameters,
|
||||||
|
filters: QueryFilters,
|
||||||
|
): Promise<RevenueMetricsResult> {
|
||||||
|
const { startDate, endDate, currency } = parameters;
|
||||||
|
const { rawQuery, parseFilters } = clickhouse;
|
||||||
|
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
|
...filters,
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
currency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinQuery = filterQuery
|
||||||
|
? `any left join (
|
||||||
|
select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = website_revenue.website_id
|
||||||
|
and website_event.session_id = website_revenue.session_id
|
||||||
|
and website_event.event_id = website_revenue.event_id`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const country = await rawQuery<{ name: string; value: number }[]>(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
website_event.country as "name",
|
||||||
|
sum(website_revenue.revenue) as "value"
|
||||||
|
from website_revenue
|
||||||
|
any left join (
|
||||||
|
select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = website_revenue.website_id
|
||||||
|
and website_event.session_id = website_revenue.session_id
|
||||||
|
and website_event.event_id = website_revenue.event_id
|
||||||
|
${cohortQuery}
|
||||||
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
|
${filterQuery}
|
||||||
|
group by website_event.country
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const region = await rawQuery<{ name: string; value: number; country: string }[]>(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
website_event.country,
|
||||||
|
website_event.region as "name",
|
||||||
|
sum(website_revenue.revenue) as "value"
|
||||||
|
from website_revenue
|
||||||
|
any left join (
|
||||||
|
select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = website_revenue.website_id
|
||||||
|
and website_event.session_id = website_revenue.session_id
|
||||||
|
and website_event.event_id = website_revenue.event_id
|
||||||
|
${cohortQuery}
|
||||||
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
|
${filterQuery}
|
||||||
|
group by 1,2
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrer = await rawQuery<{ name: string; value: number }[]>(
|
||||||
|
`
|
||||||
|
WITH events AS (
|
||||||
|
select distinct
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
sum(revenue) as "value"
|
||||||
|
from website_revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and upper(currency) = {currency:String}
|
||||||
|
${filterQuery}
|
||||||
|
group by 1,2),
|
||||||
|
|
||||||
|
revenue AS (
|
||||||
|
select
|
||||||
|
e.website_id,
|
||||||
|
e.session_id,
|
||||||
|
e.value,
|
||||||
|
we.min_date as created_at
|
||||||
|
from events e
|
||||||
|
join (select session_id, min(created_at) min_date
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
group by 1
|
||||||
|
) we
|
||||||
|
on we.session_id = e.session_id)
|
||||||
|
|
||||||
|
select
|
||||||
|
website_event.referrer_domain as "name",
|
||||||
|
sum(revenue.value) as "value"
|
||||||
|
from revenue
|
||||||
|
any left join (
|
||||||
|
select website_id, session_id, referrer_domain, created_at
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
|
||||||
|
on website_event.website_id = revenue.website_id
|
||||||
|
and website_event.session_id = revenue.session_id
|
||||||
|
and website_event.created_at = revenue.created_at
|
||||||
|
group by 1
|
||||||
|
order by value desc
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
const channel = await rawQuery<{ name: string; value: number }[]>(
|
||||||
|
`
|
||||||
|
WITH events AS (
|
||||||
|
select distinct
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
sum(revenue) as "value"
|
||||||
|
from website_revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and upper(currency) = {currency:String}
|
||||||
|
${filterQuery}
|
||||||
|
group by 1,2),
|
||||||
|
|
||||||
|
revenue AS (
|
||||||
|
select
|
||||||
|
e.website_id,
|
||||||
|
e.session_id,
|
||||||
|
e.value,
|
||||||
|
we.min_date as created_at
|
||||||
|
from events e
|
||||||
|
join (select session_id, min(created_at) min_date
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
group by 1
|
||||||
|
) we
|
||||||
|
on we.session_id = e.session_id),
|
||||||
|
|
||||||
|
channels AS (
|
||||||
|
select
|
||||||
|
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
||||||
|
case
|
||||||
|
when referrer_domain = '' and url_query = '' then 'direct'
|
||||||
|
when multiSearchAny(lower(url_query), [${toClickHouseStringArray(
|
||||||
|
PAID_AD_PARAMS,
|
||||||
|
)}]) != 0 then 'paidAds'
|
||||||
|
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||||
|
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||||
|
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||||
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
|
SEARCH_DOMAINS,
|
||||||
|
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||||
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
|
SOCIAL_DOMAINS,
|
||||||
|
)}]) != 0 then concat(prefix, 'Social')
|
||||||
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
|
EMAIL_DOMAINS,
|
||||||
|
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||||
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
|
SHOPPING_DOMAINS,
|
||||||
|
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||||
|
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||||
|
VIDEO_DOMAINS,
|
||||||
|
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||||
|
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||||
|
else 'Unknown' end AS "name",
|
||||||
|
sum(revenue.value) as "value"
|
||||||
|
from revenue
|
||||||
|
any left join (
|
||||||
|
select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
|
||||||
|
on website_event.website_id = revenue.website_id
|
||||||
|
and website_event.session_id = revenue.session_id
|
||||||
|
and website_event.created_at = revenue.created_at
|
||||||
|
group by 1, 2)
|
||||||
|
|
||||||
|
select name, sum(value) value
|
||||||
|
from channels
|
||||||
|
group by 1
|
||||||
|
order by value desc;
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { country, region, referrer, channel };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toClickHouseStringArray(arr: string[]): string {
|
||||||
|
return arr.map(p => `'${p.replace(/'/g, "\\'")}'`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPostgresLikeClause(column: string, arr: string[]) {
|
||||||
|
return arr.map(val => `${column} ilike '%${val.replace(/'/g, "''")}%'`).join(' OR\n ');
|
||||||
|
}
|
||||||
120
src/queries/sql/reports/getRevenueStats.ts
Normal file
120
src/queries/sql/reports/getRevenueStats.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import clickhouse from '@/lib/clickhouse';
|
||||||
|
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import type { QueryFilters } from '@/lib/types';
|
||||||
|
import type { RevenuParameters } from './getRevenue';
|
||||||
|
|
||||||
|
export interface RevenueStatsResult {
|
||||||
|
sum: number;
|
||||||
|
count: number;
|
||||||
|
average: number;
|
||||||
|
unique_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRevenueStats(
|
||||||
|
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
|
||||||
|
) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(
|
||||||
|
websiteId: string,
|
||||||
|
parameters: RevenuParameters,
|
||||||
|
filters: QueryFilters,
|
||||||
|
): Promise<RevenueStatsResult> {
|
||||||
|
const { startDate, endDate, currency } = parameters;
|
||||||
|
const { rawQuery, parseFilters } = prisma;
|
||||||
|
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||||
|
...filters,
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
currency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinQuery =
|
||||||
|
filterQuery || cohortQuery
|
||||||
|
? `join (select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = revenue.website_id
|
||||||
|
and website_event.session_id = revenue.session_id
|
||||||
|
and website_event.event_id = revenue.event_id`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const total = await rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
sum(revenue.revenue) as sum,
|
||||||
|
count(distinct revenue.event_id) as count,
|
||||||
|
count(distinct revenue.session_id) as unique_count
|
||||||
|
from revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
${joinSessionQuery}
|
||||||
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
|
and upper(revenue.currency) = {{currency}}
|
||||||
|
${filterQuery}
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
|
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(
|
||||||
|
websiteId: string,
|
||||||
|
parameters: RevenuParameters,
|
||||||
|
filters: QueryFilters,
|
||||||
|
): Promise<RevenueStatsResult> {
|
||||||
|
const { startDate, endDate, currency } = parameters;
|
||||||
|
const { rawQuery, parseFilters } = clickhouse;
|
||||||
|
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
|
...filters,
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
currency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinQuery = filterQuery
|
||||||
|
? `any left join (
|
||||||
|
select *
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and event_type = 2) website_event
|
||||||
|
on website_event.website_id = website_revenue.website_id
|
||||||
|
and website_event.session_id = website_revenue.session_id
|
||||||
|
and website_event.event_id = website_revenue.event_id`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const total = await rawQuery<{ sum: number; count: number; unique_count: number }>(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
sum(website_revenue.revenue) as sum,
|
||||||
|
uniqExact(website_revenue.event_id) as count,
|
||||||
|
uniqExact(website_revenue.session_id) as unique_count
|
||||||
|
from website_revenue
|
||||||
|
${joinQuery}
|
||||||
|
${cohortQuery}
|
||||||
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
|
${filterQuery}
|
||||||
|
`,
|
||||||
|
queryParams,
|
||||||
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
|
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue