Fixed attribution report. New metric cards. Converted ListTable.

This commit is contained in:
Mike Cao 2025-06-11 00:05:34 -07:00
parent b2aa37a3df
commit 4995a0e1e4
19 changed files with 167 additions and 288 deletions

View file

@ -17,9 +17,7 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
return ( return (
<Column gap> <Column gap>
<WebsiteControls websiteId={websiteId} allowCompare={true} /> <WebsiteControls websiteId={websiteId} allowCompare={true} />
<Panel> <WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
</Panel>
<Panel> <Panel>
<WebsiteChart websiteId={websiteId} compareMode={compare} /> <WebsiteChart websiteId={websiteId} compareMode={compare} />
</Panel> </Panel>

View file

@ -32,7 +32,7 @@ export function WebsiteTableView({ websiteId }: { websiteId: string }) {
</Panel> </Panel>
</GridRow> </GridRow>
<GridRow layout="two-one"> <GridRow layout="two-one">
<Panel padding="0" gridColumn="span 2"> <Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={websiteId} /> <WorldMap websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>

View file

@ -23,9 +23,7 @@ export function EventsPage({ websiteId }) {
return ( return (
<Column gap="3"> <Column gap="3">
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} />
<Panel> <EventsMetricsBar websiteId={websiteId} />
<EventsMetricsBar websiteId={websiteId} />
</Panel>
<GridRow layout="two-one"> <GridRow layout="two-one">
<Panel gridColumn="span 2"> <Panel gridColumn="span 2">
<EventsChart websiteId={websiteId} focusLabel={label} /> <EventsChart websiteId={websiteId} focusLabel={label} />

View file

@ -13,7 +13,7 @@ import { RealtimeUrls } from './RealtimeUrls';
import { RealtimeCountries } from './RealtimeCountries'; import { RealtimeCountries } from './RealtimeCountries';
import { percentFilter } from '@/lib/filters'; import { percentFilter } from '@/lib/filters';
export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) { export function RealtimePage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useRealtimeQuery(websiteId); const { data, isLoading, error } = useRealtimeQuery(websiteId);
if (isLoading || error) { if (isLoading || error) {
@ -28,9 +28,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
return ( return (
<Grid gap="3"> <Grid gap="3">
<Panel> <RealtimeHeader data={data} />
<RealtimeHeader data={data} />
</Panel>
<Panel> <Panel>
<RealtimeChart data={data} unit="minute" /> <RealtimeChart data={data} unit="minute" />
</Panel> </Panel>
@ -46,7 +44,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
<Panel> <Panel>
<RealtimeCountries data={countries} /> <RealtimeCountries data={countries} />
</Panel> </Panel>
<Panel padding="0" gridColumn="span 2"> <Panel gridColumn="span 2" noPadding>
<WorldMap data={countries} /> <WorldMap data={countries} />
</Panel> </Panel>
</GridRow> </GridRow>

View file

@ -1,10 +1,10 @@
import { WebsiteRealtimePage } from './WebsiteRealtimePage'; import { RealtimePage } from './RealtimePage';
import { Metadata } from 'next'; import { Metadata } from 'next';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params; const { websiteId } = await params;
return <WebsiteRealtimePage websiteId={websiteId} />; return <RealtimePage websiteId={websiteId} />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {

View file

@ -2,13 +2,12 @@ import { Grid, Column } from '@umami/react-zen';
import { useMessages, useResultQuery } from '@/components/hooks'; import { useMessages, useResultQuery } from '@/components/hooks';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { formatLongNumber } from '@/lib/format';
import { CHART_COLORS } from '@/lib/constants';
import { PieChart } from '@/components/charts/PieChart';
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 { MetricsBar } from '@/components/metrics/MetricsBar'; import { MetricsBar } from '@/components/metrics/MetricsBar';
import { SectionHeader } from '@/components/common/SectionHeader';
import { formatLongNumber } from '@/lib/format';
import { percentFilter } from '@/lib/filters';
export interface AttributionProps { export interface AttributionProps {
websiteId: string; websiteId: string;
@ -44,16 +43,8 @@ export function Attribution({
const isEmpty = !Object.keys(data || {}).length; const isEmpty = !Object.keys(data || {}).length;
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const ATTRIBUTION_PARAMS = [
{ value: 'referrer', label: formatMessage(labels.referrers) },
{ value: 'paidAds', label: formatMessage(labels.paidAds) },
];
if (!data) { const { pageviews, visitors, visits } = data?.total || {};
return null;
}
const { pageviews, visitors, visits } = data.total;
const metrics = data const metrics = data
? [ ? [
@ -75,22 +66,18 @@ export function Attribution({
] ]
: []; : [];
function UTMTable(UTMTableProps: { data: any; title: string; utm: string }) { function UTMTable({ data = [], title }: { data: any; title: string }) {
const { data, title, utm } = UTMTableProps;
const total = data[utm].reduce((sum, { value }) => {
return +sum + +value;
}, 0);
return ( return (
<ListTable <ListTable
title={title} title={title}
metric={formatMessage(currency ? labels.revenue : labels.visitors)} metric={formatMessage(currency ? labels.revenue : labels.visitors)}
currency={currency} currency={currency}
data={data[utm].map(({ name, value }) => ({ data={percentFilter(
x: name, data.map(({ name, value }) => ({
y: Number(value), x: name,
z: (value / total) * 100, y: Number(value),
}))} })),
)}
/> />
); );
} }
@ -98,58 +85,58 @@ export function Attribution({
return ( return (
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}> <LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
<Column gap> <Column gap>
<Panel> <MetricsBar isFetched={data}>
<MetricsBar isFetched={data}> {metrics?.map(({ label, value, formatValue }) => {
{metrics?.map(({ label, value, formatValue }) => { return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
return ( })}
<MetricCard key={label} value={value} label={label} formatValue={formatValue} /> </MetricsBar>
); <SectionHeader title={formatMessage(labels.sources)} />
})} <Grid columns="1fr 1fr" gap>
</MetricsBar> <Panel>
</Panel> <ListTable
{ATTRIBUTION_PARAMS.map(({ value, label }) => { title={formatMessage(labels.referrer)}
const items = data[value]; metric={formatMessage(currency ? labels.revenue : labels.visitors)}
const total = items.reduce((sum, { value }) => { currency={currency}
return +sum + +value; data={percentFilter(
}, 0); data?.['referrer']?.map(({ name, value }) => ({
x: name,
const chartData = { y: Number(value),
labels: items.map(({ name }) => name), })),
datasets: [ )}
{ />
data: items.map(({ value }) => value), </Panel>
backgroundColor: CHART_COLORS, <Panel>
borderWidth: 0, <ListTable
}, title={formatMessage(labels.paidAds)}
], metric={formatMessage(currency ? labels.revenue : labels.visitors)}
}; currency={currency}
data={percentFilter(
return ( data?.['paidAds']?.map(({ name, value }) => ({
<Panel key={value} title={label}> x: name,
<Grid columns="1fr 1fr" gap> y: Number(value),
<ListTable })),
metric={formatMessage(currency ? labels.revenue : labels.visitors)} )}
currency={currency} />
data={items.map(({ name, value }) => ({ </Panel>
x: name, </Grid>
y: Number(value), <SectionHeader title="UTM" />
z: (value / total) * 100, <Grid columns="1fr 1fr" gap>
}))} <Panel>
/> <UTMTable data={data?.['utm_source']} title={formatMessage(labels.sources)} />
<PieChart type="doughnut" data={chartData} isLoading={isLoading} /> </Panel>
</Grid> <Panel>
</Panel> <UTMTable data={data?.['utm_medium']} title={formatMessage(labels.medium)} />
); </Panel>
})} <Panel>
<Panel title="UTM"> <UTMTable data={data?.['utm_cmapaign']} title={formatMessage(labels.campaigns)} />
<Grid columns="1fr 1fr" gap> </Panel>
<UTMTable data={data} title={formatMessage(labels.sources)} utm={'utm_source'} /> <Panel>
<UTMTable data={data} title={formatMessage(labels.medium)} utm={'utm_medium'} /> <UTMTable data={data?.['utm_content']} title={formatMessage(labels.content)} />
<UTMTable data={data} title={formatMessage(labels.campaigns)} utm={'utm_campaign'} /> </Panel>
<UTMTable data={data} title={formatMessage(labels.content)} utm={'utm_content'} /> <Panel>
<UTMTable data={data} title={formatMessage(labels.terms)} utm={'utm_term'} /> <UTMTable data={data?.['utm_term']} title={formatMessage(labels.terms)} />
</Grid> </Panel>
</Panel> </Grid>
</Column> </Column>
</LoadingPanel> </LoadingPanel>
); );

View file

@ -15,7 +15,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
} = useDateRange(websiteId); } = useDateRange(websiteId);
return ( return (
<Column gap> <Column gap="6">
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} />
<Grid columns="1fr 1fr 1fr" gap> <Grid columns="1fr 1fr 1fr" gap>
<Column> <Column>
@ -46,6 +46,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
value={step} value={step}
defaultValue={step} defaultValue={step}
onSearch={setStep} onSearch={setStep}
delay={1000}
/> />
</Column> </Column>
</Grid> </Grid>

View file

@ -22,7 +22,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange(websiteId);
const [fields, setFields] = useState([]); const [fields, setFields] = useState(['url']);
return ( return (
<Column gap> <Column gap>

View file

@ -154,15 +154,13 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
<LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}> <LoadingPanel isEmpty={isEmpty} isLoading={isLoading} error={error}>
<Column gap> <Column gap>
<Panel> <MetricsBar isFetched={!!data}>
<MetricsBar isFetched={!!data}> {metricData?.map(({ label, value, formatValue }) => {
{metricData?.map(({ label, value, formatValue }) => { return (
return ( <MetricCard key={label} value={value} label={label} formatValue={formatValue} />
<MetricCard key={label} value={value} label={label} formatValue={formatValue} /> );
); })}
})} </MetricsBar>
</MetricsBar>
</Panel>
{data && ( {data && (
<> <>
<Panel> <Panel>

View file

@ -18,11 +18,9 @@ export function SessionsPage({ websiteId }) {
return ( return (
<Column gap="3"> <Column gap="3">
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} />
<Panel> <SessionsMetricsBar websiteId={websiteId} />
<SessionsMetricsBar websiteId={websiteId} />
</Panel>
<GridRow layout="two-one"> <GridRow layout="two-one">
<Panel padding="0" gridColumn="span 2"> <Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={websiteId} /> <WorldMap websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>

View file

@ -2,6 +2,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
width: 100%;
} }
.row.inactive { .row.inactive {

View file

@ -15,6 +15,7 @@ import { useMessages } from '@/components/hooks';
export interface PanelProps extends ColumnProps { export interface PanelProps extends ColumnProps {
title?: string; title?: string;
allowFullscreen?: boolean; allowFullscreen?: boolean;
noPadding?: boolean;
} }
const fullscreenStyles = { const fullscreenStyles = {
@ -27,7 +28,14 @@ const fullscreenStyles = {
zIndex: 9999, zIndex: 9999,
} as any; } as any;
export function Panel({ title, allowFullscreen, style, children, ...props }: PanelProps) { export function Panel({
title,
allowFullscreen,
noPadding,
style,
children,
...props
}: PanelProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
@ -37,7 +45,7 @@ export function Panel({ title, allowFullscreen, style, children, ...props }: Pan
return ( return (
<Column <Column
padding="6" padding={!noPadding ? '6' : undefined}
border border
borderRadius="3" borderRadius="3"
backgroundColor backgroundColor

View file

@ -1,110 +0,0 @@
.table {
position: relative;
display: grid;
grid-template-rows: fit-content(100%) auto;
overflow: hidden;
flex: 1;
}
.body {
position: relative;
height: 100%;
overflow: auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
line-height: 40px;
}
.title {
display: flex;
font-weight: 600;
}
.metric {
font-weight: 600;
text-align: center;
width: 100px;
}
.row {
position: relative;
height: 30px;
line-height: 30px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
overflow: hidden;
border-radius: 4px;
}
.row:hover {
background-color: var(--base-color-2);
}
.label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex: 2;
padding-left: 10px;
}
.label a {
color: inherit;
text-decoration: none;
}
.label a:hover {
color: var(--primary-color);
}
.label:empty {
color: #b3b3b3;
}
.label:empty:before {
content: 'Unknown';
}
.value {
display: flex;
align-items: center;
gap: 10px;
text-align: end;
margin-inline-end: 5px;
font-weight: 600;
}
.percent {
position: relative;
width: 50px;
color: var(--base-color-9);
border-inline-start: 1px solid var(--base-color-9);
padding-inline-start: 10px;
z-index: 1;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 30px;
opacity: 0.1;
background: var(--primary-color);
z-index: -1;
}
.empty {
min-height: 200px;
}
@media only screen and (max-width: 992px) {
.body {
height: auto;
}
}

View file

@ -1,12 +1,11 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { useSpring, config } from '@react-spring/web'; import { useSpring, config } from '@react-spring/web';
import classNames from 'classnames'; import { Grid, Row, Column, Text } from '@umami/react-zen';
import { AnimatedDiv } from '@/components/common/AnimatedDiv'; import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import styles from './ListTable.module.css';
const ITEM_SIZE = 30; const ITEM_SIZE = 30;
@ -28,7 +27,6 @@ export function ListTable({
data = [], data = [],
title, title,
metric, metric,
className,
renderLabel, renderLabel,
renderChange, renderChange,
animate = true, animate = true,
@ -40,7 +38,7 @@ export function ListTable({
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const getRow = (row: { x: any; y: any; z: any }, index: number) => { const getRow = (row: { x: any; y: any; z: any }, index: number) => {
const { x: label, y: value, z: percent } = row; const { x: label, y: value, z: percent } = row || {};
return ( return (
<AnimatedRow <AnimatedRow
@ -56,18 +54,20 @@ export function ListTable({
); );
}; };
const Row = ({ index, style }) => { const ListTableRow = ({ index, style }) => {
return <div style={style}>{getRow(data[index], index)}</div>; return <div style={style}>{getRow(data[index], index)}</div>;
}; };
return ( return (
<div className={classNames(styles.table, className)}> <Column gap>
<div className={styles.header}> <Grid alignItems="center" justifyContent="space-between" paddingLeft="2" columns="1fr 100px">
<div className={styles.title}>{title}</div> <Text weight="bold">{title}</Text>
<div className={styles.metric}>{metric}</div> <Text weight="bold" align="center">
</div> {metric}
<div className={styles.body}> </Text>
{data?.length === 0 && <Empty className={styles.empty} />} </Grid>
<Column gap="1">
{data?.length === 0 && <Empty />}
{virtualize && data.length > 0 ? ( {virtualize && data.length > 0 ? (
<FixedSizeList <FixedSizeList
width="100%" width="100%"
@ -75,13 +75,13 @@ export function ListTable({
itemCount={data.length} itemCount={data.length}
itemSize={ITEM_SIZE} itemSize={ITEM_SIZE}
> >
{Row} {ListTableRow}
</FixedSizeList> </FixedSizeList>
) : ( ) : (
data.map(getRow) data.map(getRow)
)} )}
</div> </Column>
</div> </Column>
); );
} }
@ -102,22 +102,33 @@ const AnimatedRow = ({
}); });
return ( return (
<div className={styles.row}> <Grid columns="1fr 50px 50px" paddingLeft="2" alignItems="center" hoverBackgroundColor="2" gap>
<div className={styles.label}>{label}</div> <Row alignItems="center">
<div className={styles.value}> <Text>{label}</Text>
</Row>
<Row alignItems="center" height="30px" justifyContent="flex-end">
{change} {change}
<AnimatedDiv className={styles.value} title={props?.y as any}> <Text weight="bold">
{currency <AnimatedDiv title={props?.y as any}>
? props.y?.to(n => formatLongCurrency(n, currency)) {currency
: props.y?.to(formatLongNumber)} ? props.y?.to(n => formatLongCurrency(n, currency))
</AnimatedDiv> : props.y?.to(formatLongNumber)}
</div> </AnimatedDiv>
</Text>
</Row>
{showPercentage && ( {showPercentage && (
<div className={styles.percent}> <Row
<AnimatedDiv className={styles.bar} style={{ width: props.width.to(n => `${n}%`) }} /> alignItems="center"
justifyContent="flex-start"
position="relative"
border="left"
borderColor="8"
color="muted"
paddingLeft="3"
>
<AnimatedDiv>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</AnimatedDiv> <AnimatedDiv>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</AnimatedDiv>
</div> </Row>
)} )}
</div> </Grid>
); );
}; };

View file

@ -1,7 +0,0 @@
.card {
border-right: 1px solid var(--border-color);
}
.card:last-child {
border-right: 0;
}

View file

@ -3,7 +3,6 @@ import { useSpring } from '@react-spring/web';
import { formatNumber } from '@/lib/format'; import { formatNumber } from '@/lib/format';
import { AnimatedDiv } from '@/components/common/AnimatedDiv'; import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { ChangeLabel } from '@/components/metrics/ChangeLabel'; import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import styles from './MetricCard.module.css';
export interface MetricCardProps { export interface MetricCardProps {
value: number; value: number;
@ -34,7 +33,14 @@ export const MetricCard = ({
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } }); const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return ( return (
<Column className={styles.card} justifyContent="center" paddingX="8"> <Column
justifyContent="center"
paddingX="6"
paddingY="4"
borderRadius="3"
backgroundColor
border
>
{showLabel && ( {showLabel && (
<Text weight="bold" wrap="nowrap"> <Text weight="bold" wrap="nowrap">
{label} {label}

View file

@ -1,24 +1,22 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Grid, Loading } from '@umami/react-zen'; import { Grid } from '@umami/react-zen';
import { ErrorMessage } from '@/components/common/ErrorMessage'; import { LoadingPanel } from '@/components/common/LoadingPanel';
export interface MetricsBarProps { export interface MetricsBarProps {
isLoading?: boolean; isLoading?: boolean;
isFetched?: boolean; isFetched?: boolean;
error?: unknown; error?: Error;
children?: ReactNode; children?: ReactNode;
} }
export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) { export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) {
return ( return (
<> <LoadingPanel isLoading={isLoading} isFetched={isFetched} error={error}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{!isLoading && !error && isFetched && ( {!isLoading && !error && isFetched && (
<Grid columns="repeat(auto-fill, minmax(200px, 1fr))" width="100%" gapY="3"> <Grid columns="repeat(auto-fit, minmax(200px, 1fr))" width="100%" gap>
{children} {children}
</Grid> </Grid>
)} )}
</> </LoadingPanel>
); );
} }

View file

@ -47,6 +47,7 @@ export const emptyFilter = (data: any[]) => {
}; };
export const percentFilter = (data: any[]) => { export const percentFilter = (data: any[]) => {
if (!data) return [];
const total = data.reduce((n, { y }) => n + y, 0); const total = data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props })); return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
}; };

View file

@ -12,6 +12,17 @@ export interface AttributionCriteria {
currency?: string; currency?: string;
} }
export interface AttributionResult {
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}
export async function getAttribution(...args: [websiteId: string, criteria: AttributionCriteria]) { export async function getAttribution(...args: [websiteId: string, criteria: AttributionCriteria]) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -22,20 +33,11 @@ export async function getAttribution(...args: [websiteId: string, criteria: Attr
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: AttributionCriteria, criteria: AttributionCriteria,
): Promise<{ ): Promise<AttributionResult> {
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}> {
const { startDate, endDate, model, type, step, currency } = criteria; const { startDate, endDate, model, type, step, currency } = criteria;
const { rawQuery } = prisma; const { rawQuery } = prisma;
const eventType = type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'url' ? 'url_path' : 'event_name'; const column = type === 'page' ? 'url_path' : 'event_name';
const db = getDatabaseType(); const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like'; const like = db === POSTGRESQL ? 'ilike' : 'like';
@ -81,7 +83,7 @@ async function relationalQuery(
group by 1),`; group by 1),`;
function getModelQuery(model: string) { function getModelQuery(model: string) {
return model === 'firstClick' return model === 'first-click'
? `\n ? `\n
model AS (select e.session_id, model AS (select e.session_id,
min(we.created_at) created_at min(we.created_at) created_at
@ -239,20 +241,11 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: AttributionCriteria, criteria: AttributionCriteria,
): Promise<{ ): Promise<AttributionResult> {
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}> {
const { startDate, endDate, model, type, step, currency } = criteria; const { startDate, endDate, model, type, step, currency } = criteria;
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const eventType = type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'url' ? 'url_path' : 'event_name'; const column = type === 'page' ? 'url_path' : 'event_name';
function getUTMQuery(utmColumn: string) { function getUTMQuery(utmColumn: string) {
return ` return `
@ -296,7 +289,7 @@ async function clickhouseQuery(
group by 1),`; group by 1),`;
function getModelQuery(model: string) { function getModelQuery(model: string) {
return model === 'firstClick' return model === 'first-click'
? `\n ? `\n
model AS (select e.session_id, model AS (select e.session_id,
min(we.created_at) created_at min(we.created_at) created_at