mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Breakdown report.
This commit is contained in:
parent
79ea9974b7
commit
e3cc19638c
21 changed files with 495 additions and 456 deletions
|
|
@ -1,6 +1,6 @@
|
|||
generator client {
|
||||
provider = "prisma-client"
|
||||
previewFeatures = ["queryCompiler", "driverAdapters"]
|
||||
previewFeatures = ["driverAdapters"]
|
||||
output = "../src/generated/prisma"
|
||||
moduleFormat = "esm"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
"@react-spring/web": "^9.7.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@umami/react-zen": "^0.136.0",
|
||||
"@umami/react-zen": "^0.137.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
|
|
|
|||
490
pnpm-lock.yaml
generated
490
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@ import {
|
|||
Logo,
|
||||
Grid2X2,
|
||||
Settings,
|
||||
LockKeyhole,
|
||||
} from '@/components/icons';
|
||||
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
|
||||
|
||||
|
|
@ -41,6 +42,11 @@ export function SideNav(props: any) {
|
|||
href: renderTeamUrl('/settings'),
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.admin),
|
||||
href: renderTeamUrl('/admin'),
|
||||
icon: <LockKeyhole />,
|
||||
},
|
||||
].filter(n => n);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ReportsNav } from './ReportsNav';
|
|||
|
||||
export function ReportsLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||
return (
|
||||
<Grid columns="200px 1fr" gap="6">
|
||||
<Grid columns="180px 1fr" gap="6">
|
||||
<Column>
|
||||
<ReportsNav websiteId={websiteId} />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Row, NavMenu, NavMenuItem, Icon, Text } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Funnel, Lightbulb, Magnet, Money, Network, Path, Tag, Target } from '@/components/icons';
|
||||
import { Funnel, Sheet, Magnet, Money, Network, Path, Tag, Target } from '@/components/icons';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function ReportsNav({ websiteId }: { websiteId: string }) {
|
||||
|
|
@ -32,6 +32,12 @@ export function ReportsNav({ websiteId }: { websiteId: string }) {
|
|||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: formatMessage(labels.breakdown),
|
||||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
|
|
@ -50,12 +56,6 @@ export function ReportsNav({ websiteId }: { websiteId: string }) {
|
|||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
},
|
||||
{
|
||||
id: 'insights',
|
||||
label: formatMessage(labels.insights),
|
||||
icon: <Lightbulb />,
|
||||
path: '/insights',
|
||||
},
|
||||
];
|
||||
|
||||
const selected = links.find(({ path }) => path && pathname.endsWith(path))?.id || 'goals';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Grid, Column, Heading } from '@umami/react-zen';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
|
|
@ -125,8 +125,7 @@ export function Attribution({
|
|||
};
|
||||
|
||||
return (
|
||||
<Panel key={value}>
|
||||
<Heading>{label}</Heading>
|
||||
<Panel key={value} title={label}>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<ListTable
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
|
|
@ -142,25 +141,15 @@ export function Attribution({
|
|||
</Panel>
|
||||
);
|
||||
})}
|
||||
<Grid gap>
|
||||
<Panel title="UTM">
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.sources)} utm={'utm_source'} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.medium)} utm={'utm_medium'} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.campaigns)} utm={'utm_campaign'} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.content)} utm={'utm_content'} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.terms)} utm={'utm_term'} />
|
||||
</Panel>
|
||||
<UTMTable data={data} title={formatMessage(labels.sources)} utm={'utm_source'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.medium)} utm={'utm_medium'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.campaigns)} utm={'utm_campaign'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.content)} utm={'utm_content'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.terms)} utm={'utm_term'} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Column>
|
||||
</LoadingPanel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useDateRange, useMessages } from '@/components/hooks';
|
|||
export function AttributionPage({ websiteId }: { websiteId: string }) {
|
||||
const [model, setModel] = useState('first-click');
|
||||
const [type, setType] = useState('page');
|
||||
const [step, setStep] = useState('');
|
||||
const [step, setStep] = useState('/');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
|
|
@ -44,6 +44,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
|
|||
<SearchField
|
||||
label={formatMessage(labels.conversionStep)}
|
||||
value={step}
|
||||
defaultValue={step}
|
||||
onSearch={setStep}
|
||||
/>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { Text, DataTable, DataColumn } from '@umami/react-zen';
|
||||
import { useMessages, useResultQuery, useFormat, useFields } from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { formatShortTime } from '@/lib/format';
|
||||
|
||||
export interface BreakdownProps {
|
||||
websiteId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
parameters: {
|
||||
fields: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function Breakdown({ websiteId, parameters, startDate, endDate }: BreakdownProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { fields } = useFields();
|
||||
const { data, error, isLoading } = useResultQuery<any>(
|
||||
'breakdown',
|
||||
{
|
||||
websiteId,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
parameters,
|
||||
},
|
||||
{ enabled: !!parameters.fields.length },
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingPanel isEmpty={!data?.length} isLoading={isLoading} error={error}>
|
||||
<DataTable data={data}>
|
||||
{parameters?.fields.map(field => {
|
||||
return (
|
||||
<DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}>
|
||||
{row => {
|
||||
const value = formatValue(row[field], field);
|
||||
return (
|
||||
<Text truncate title={value}>
|
||||
{value}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
);
|
||||
})}
|
||||
<DataColumn id="views" label={formatMessage(labels.views)} align="end">
|
||||
{row => row?.['views']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
|
||||
{row => row?.['visits']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
|
||||
{row => row?.['visitors']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
|
||||
{row => {
|
||||
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
|
||||
return Math.round(+n) + '%';
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="visitDuration" label={formatMessage(labels.visitDuration)} align="end">
|
||||
{row => {
|
||||
const n = (row?.['totaltime'] / row?.['visits']) * 100;
|
||||
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
'use client';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Button,
|
||||
Column,
|
||||
Box,
|
||||
Grid,
|
||||
Text,
|
||||
Icon,
|
||||
Popover,
|
||||
DialogTrigger,
|
||||
} from '@umami/react-zen';
|
||||
import { useDateRange, useMessages, useFields } from '@/components/hooks';
|
||||
import { SquarePlus, Chevron } from '@/components/icons';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { Breakdown } from './Breakdown';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
|
||||
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
const [fields, setFields] = useState([]);
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Box>
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
</Box>
|
||||
<Panel height="900px" overflow="auto" allowFullscreen>
|
||||
<Breakdown
|
||||
websiteId={websiteId}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
parameters={{ fields }}
|
||||
/>
|
||||
</Panel>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const FieldsButton = ({ value, onChange }) => {
|
||||
const [selected, setSelected] = useState(value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { fields } = useFields();
|
||||
|
||||
const handleChange = value => {
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
setIsOpen(false);
|
||||
onChange?.(selected);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="quiet" onPress={() => setIsOpen(!isOpen)}>
|
||||
<Icon>
|
||||
<SquarePlus />
|
||||
</Icon>
|
||||
<Text>Fields</Text>
|
||||
<Icon rotate={90}>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popover placement="bottom start" isOpen={isOpen}>
|
||||
<Column width="300px" padding="2" border borderRadius shadow="3" backgroundColor gap>
|
||||
<List value={selected} onChange={handleChange} selectionMode="multiple">
|
||||
{fields.map(({ name, label }) => {
|
||||
return (
|
||||
<ListItem key={name} id={name}>
|
||||
{label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Button onPress={handleClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button onPress={handleApply} variant="primary">
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Column>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { Metadata } from 'next';
|
||||
import { InsightsPage } from './InsightsPage';
|
||||
import { BreakdownPage } from './BreakdownPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <InsightsPage websiteId={websiteId} />;
|
||||
return <BreakdownPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -24,7 +24,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
|||
<Grid columns="1fr 1fr" gap>
|
||||
{result?.data?.map((report: any) => (
|
||||
<Panel key={report.id}>
|
||||
<Goal {...report} reportId={report.id} startDate={startDate} endDate={endDate} />
|
||||
<Goal {...report} startDate={startDate} endDate={endDate} />
|
||||
</Panel>
|
||||
))}
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Grid, Row, Column, Text, Loading, Icon } from '@umami/react-zen';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { Users } from '@/components/icons';
|
||||
import { useMessages, useLocale, useResultQuery } from '@/components/hooks';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
|
||||
const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
|
||||
|
||||
export interface AttributionProps {
|
||||
websiteId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
days?: number[];
|
||||
}
|
||||
|
||||
export function Insights({ websiteId, days = DAYS, startDate, endDate }: AttributionProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { data, error, isLoading } = useResultQuery<any>('insights', {
|
||||
websiteId,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
parameters: {
|
||||
days,
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading position="page" />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
|
||||
const { date, visitors, day } = row;
|
||||
if (day === 0) {
|
||||
return arr.concat({
|
||||
date,
|
||||
visitors,
|
||||
records: days
|
||||
.reduce((arr, day) => {
|
||||
arr[day] = data.find(x => x.date === date && x.day === day);
|
||||
return arr;
|
||||
}, [])
|
||||
.filter(n => n),
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
const totalDays = rows.length;
|
||||
|
||||
return (
|
||||
<LoadingPanel isEmpty={!data?.length} isLoading={isLoading} error={error}>
|
||||
<Panel allowFullscreen height="900px">
|
||||
<Column gap="1" width="100%" overflow="auto">
|
||||
<Grid
|
||||
columns="120px repeat(10, 100px)"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
height="50px"
|
||||
autoFlow="column"
|
||||
>
|
||||
<Column>
|
||||
<Text weight="bold">{formatMessage(labels.cohort)}</Text>
|
||||
</Column>
|
||||
{days.map(n => (
|
||||
<Column key={n}>
|
||||
<Text weight="bold" align="center" wrap="nowrap">
|
||||
{formatMessage(labels.day)} {n}
|
||||
</Text>
|
||||
</Column>
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
return (
|
||||
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
|
||||
<Column justifyContent="center" gap="1">
|
||||
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Users />
|
||||
</Icon>
|
||||
<Text>{formatLongNumber(visitors)}</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return (
|
||||
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
</Panel>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const Cell = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Column
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100px"
|
||||
height="100px"
|
||||
backgroundColor="2"
|
||||
borderRadius
|
||||
>
|
||||
{children}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Insights } from './Insights';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { useDateRange } from '@/components/hooks';
|
||||
|
||||
export function InsightsPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Insights websiteId={websiteId} startDate={startDate} endDate={endDate} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { unauthorized, json } from '@/lib/response';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { getInsights } from '@/queries';
|
||||
import { getBreakdown } from '@/queries';
|
||||
import { reportResultSchema } from '@/lib/schema';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
|
@ -14,27 +14,17 @@ export async function POST(request: Request) {
|
|||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
fields,
|
||||
filters,
|
||||
parameters: { fields },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getInsights(websiteId, fields, {
|
||||
...convertFilters(filters),
|
||||
const data = await getBreakdown(websiteId, fields, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
||||
function convertFilters(filters: any[]) {
|
||||
return filters.reduce((obj, filter) => {
|
||||
obj[filter.name] = filter;
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
|
@ -7,11 +7,13 @@ import {
|
|||
Button,
|
||||
TooltipTrigger,
|
||||
Tooltip,
|
||||
Heading,
|
||||
} from '@umami/react-zen';
|
||||
import { Maximize, Close } from '@/components/icons';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export interface PanelProps extends ColumnProps {
|
||||
title?: string;
|
||||
allowFullscreen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ const fullscreenStyles = {
|
|||
zIndex: 9999,
|
||||
} as any;
|
||||
|
||||
export function Panel({ allowFullscreen, style, children, ...props }: PanelProps) {
|
||||
export function Panel({ title, allowFullscreen, style, children, ...props }: PanelProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
|
|
@ -44,6 +46,7 @@ export function Panel({ allowFullscreen, style, children, ...props }: PanelProps
|
|||
{...props}
|
||||
style={{ ...style, ...(isFullscreen ? fullscreenStyles : {}) }}
|
||||
>
|
||||
{title && <Heading>{title}</Heading>}
|
||||
{allowFullscreen && (
|
||||
<Row justifyContent="flex-end" alignItems="center">
|
||||
<TooltipTrigger delay={0} isDisabled={isFullscreen}>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ export {
|
|||
ChartPie,
|
||||
ChevronRight as Chevron,
|
||||
Clock,
|
||||
X as Close,
|
||||
Copy,
|
||||
Download,
|
||||
Edit,
|
||||
Ellipsis,
|
||||
Eye,
|
||||
|
|
@ -29,11 +29,15 @@ export {
|
|||
RefreshCw as Refresh,
|
||||
Settings,
|
||||
Share,
|
||||
Sheet,
|
||||
Slash,
|
||||
SquarePen,
|
||||
SquarePlus,
|
||||
Sun,
|
||||
Trash,
|
||||
Upload,
|
||||
User,
|
||||
Users,
|
||||
X as Close,
|
||||
} from 'lucide-react';
|
||||
export * from '@/components/svg';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const labels = defineMessages({
|
|||
user: { id: 'label.user', defaultMessage: 'User' },
|
||||
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
|
||||
manage: { id: 'label.manage', defaultMessage: 'Manage' },
|
||||
admin: { id: 'label.admin', defaultMessage: 'Administrator' },
|
||||
admin: { id: 'label.admin', defaultMessage: 'Admin' },
|
||||
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
|
||||
details: { id: 'label.details', defaultMessage: 'Details' },
|
||||
website: { id: 'label.website', defaultMessage: 'Website' },
|
||||
|
|
@ -215,6 +215,7 @@ export const labels = defineMessages({
|
|||
value: { id: 'label.value', defaultMessage: 'Value' },
|
||||
overview: { id: 'label.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'label.total-records', defaultMessage: 'Total records' },
|
||||
insight: { id: 'label.insight', defaultMessage: 'Insight' },
|
||||
insights: { id: 'label.insights', defaultMessage: 'Insights' },
|
||||
insightsDescription: {
|
||||
id: 'label.insights-description',
|
||||
|
|
|
|||
|
|
@ -54,15 +54,31 @@ export const urlOrPathParam = z.string().refine(
|
|||
},
|
||||
);
|
||||
|
||||
export const fieldsParam = z.enum([
|
||||
'url',
|
||||
'referrer',
|
||||
'title',
|
||||
'query',
|
||||
'os',
|
||||
'browser',
|
||||
'device',
|
||||
'country',
|
||||
'region',
|
||||
'city',
|
||||
'tag',
|
||||
'host',
|
||||
'language',
|
||||
]);
|
||||
|
||||
export const reportTypeParam = z.enum([
|
||||
'attribution',
|
||||
'breakdown',
|
||||
'funnel',
|
||||
'insight',
|
||||
'retention',
|
||||
'utm',
|
||||
'goal',
|
||||
'journey',
|
||||
'retention',
|
||||
'revenue',
|
||||
'attribution',
|
||||
'utm',
|
||||
]);
|
||||
|
||||
export const reportParms = {
|
||||
|
|
@ -141,8 +157,11 @@ export const attributionReportSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
export const insightsReportSchema = z.object({
|
||||
type: z.literal('insights'),
|
||||
export const breakdownReportSchema = z.object({
|
||||
type: z.literal('breakdown'),
|
||||
parameters: z.object({
|
||||
fields: z.array(fieldsParam),
|
||||
}),
|
||||
});
|
||||
|
||||
export const reportBaseSchema = z.object({
|
||||
|
|
@ -160,7 +179,7 @@ export const reportTypeSchema = z.discriminatedUnion('type', [
|
|||
utmReportSchema,
|
||||
revenueReportSchema,
|
||||
attributionReportSchema,
|
||||
insightsReportSchema,
|
||||
breakdownReportSchema,
|
||||
]);
|
||||
|
||||
export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export * from '@/queries/sql/events/saveEvent';
|
|||
export * from '@/queries/sql/reports/getFunnel';
|
||||
export * from '@/queries/sql/reports/getJourney';
|
||||
export * from '@/queries/sql/reports/getRetention';
|
||||
export * from '@/queries/sql/reports/getInsights';
|
||||
export * from '@/queries/sql/reports/getBreakdown';
|
||||
export * from '@/queries/sql/reports/getUTM';
|
||||
export * from '@/queries/sql/pageviews/getPageviewMetrics';
|
||||
export * from '@/queries/sql/pageviews/getPageviewStats';
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import clickhouse from '@/lib/clickhouse';
|
|||
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
|
||||
import { QueryFilters } from '@/lib/types';
|
||||
|
||||
export async function getInsights(
|
||||
...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters]
|
||||
export async function getBreakdown(
|
||||
...args: [websiteId: string, fields: string[], filters: QueryFilters]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
|
|
@ -15,7 +15,7 @@ export async function getInsights(
|
|||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
fields: { name: string; type?: string }[],
|
||||
fields: string[],
|
||||
filters: QueryFilters,
|
||||
): Promise<
|
||||
{
|
||||
|
|
@ -31,7 +31,7 @@ async function relationalQuery(
|
|||
eventType: EVENT_TYPE.pageView,
|
||||
},
|
||||
{
|
||||
joinSession: !!fields.find(({ name }) => SESSION_COLUMNS.includes(name)),
|
||||
joinSession: !!fields.find(name => SESSION_COLUMNS.includes(name)),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ async function relationalQuery(
|
|||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
fields: { name: string; type?: string }[],
|
||||
fields: string[],
|
||||
filters: QueryFilters,
|
||||
): Promise<
|
||||
{
|
||||
|
|
@ -118,10 +118,10 @@ async function clickhouseQuery(
|
|||
);
|
||||
}
|
||||
|
||||
function parseFields(fields: { name: any }[]) {
|
||||
return fields.map(({ name }) => `${FILTER_COLUMNS[name]} as "${name}"`).join(',');
|
||||
function parseFields(fields: string[]) {
|
||||
return fields.map(name => `${FILTER_COLUMNS[name]} as "${name}"`).join(',');
|
||||
}
|
||||
|
||||
function parseFieldsByName(fields: { name: any }[]) {
|
||||
return `${fields.map(({ name }) => name).join(',')}`;
|
||||
function parseFieldsByName(fields: string[]) {
|
||||
return `${fields.map(name => name).join(',')}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue