Update Retention report.

This commit is contained in:
Mike Cao 2025-06-28 21:16:50 -07:00
parent 184a387ecd
commit ee8750d9df
22 changed files with 214 additions and 280 deletions

View file

@ -1,17 +0,0 @@
.container {
position: relative;
display: flex;
flex-direction: column;
align-self: stretch;
}
.chart {
position: relative;
overflow: hidden;
}
.title {
font-size: var(--font-size-lg);
line-height: 60px;
font-weight: 600;
}

View file

@ -1,14 +0,0 @@
.container {
margin-bottom: 60px;
}
.nav {
width: 200px;
margin-top: 40px;
}
.title {
color: var(--base800);
text-align: center;
font-weight: 700;
}

View file

@ -2,23 +2,27 @@ import { Column, Row } from '@umami/react-zen';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar'; import { FilterBar } from '@/components/input/FilterBar';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
export function WebsiteControls({ export function WebsiteControls({
websiteId, websiteId,
allowFilter = true, allowFilter = true,
allowDateFilter = true,
allowMonthFilter,
allowCompare, allowCompare,
}: { }: {
websiteId: string; websiteId: string;
allowFilter?: boolean; allowFilter?: boolean;
allowCompare?: boolean; allowCompare?: boolean;
allowDateFilter?: boolean;
allowMonthFilter?: boolean;
}) { }) {
return ( return (
<Column gap> <Column gap>
<Row alignItems="center" justifyContent="space-between" gap="3"> <Row alignItems="center" justifyContent="space-between" gap="3">
{allowFilter && <WebsiteFilterButton websiteId={websiteId} />} {allowFilter && <WebsiteFilterButton websiteId={websiteId} />}
<Row alignItems="center" gap="3"> {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
<WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} /> {allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
</Row>
</Row> </Row>
<FilterBar /> <FilterBar />
</Column> </Column>

View file

@ -14,15 +14,17 @@ export function WebsiteHeader() {
return ( return (
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}> <PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
<Row alignItems="center" gap> <Row alignItems="center" gap="6">
<ActiveUsers websiteId={website.id} /> <ActiveUsers websiteId={website.id} />
<ShareButton websiteId={website.id} shareId={website.shareId} /> <Row alignItems="center" gap>
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}> <ShareButton websiteId={website.id} shareId={website.shareId} />
<Icon> <LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
<Edit /> <Icon>
</Icon> <Edit />
<Text>Edit</Text> </Icon>
</LinkButton> <Text>Edit</Text>
</LinkButton>
</Row>
</Row> </Row>
</PageHeader> </PageHeader>
); );

View file

@ -1,52 +0,0 @@
.container {
display: grid;
grid-template-columns: 2fr 1fr;
justify-content: space-between;
align-items: center;
background: var(--base50);
z-index: var(--z-index-above);
min-height: 120px;
padding-bottom: 20px;
}
.actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.vs {
display: flex;
align-items: center;
justify-content: flex-end;
flex-basis: 100%;
gap: 10px;
}
.dropdown {
min-width: 200px;
}
@media screen and (max-width: 1200px) {
.container {
grid-template-columns: 1fr;
}
.actions {
margin: 20px 0;
}
}
@media screen and (min-width: 992px) {
.sticky {
position: sticky;
top: -1px;
}
.isSticky {
padding: 10px 0;
border-bottom: 1px solid var(--base300);
}
}

View file

@ -11,28 +11,30 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
return ( return (
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}> <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<MetricsBar> {data && (
<MetricCard <MetricsBar>
value={data?.visitors?.value} <MetricCard
label={formatMessage(labels.visitors)} value={data?.visitors?.value}
formatValue={formatLongNumber} label={formatMessage(labels.visitors)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.visits?.value} <MetricCard
label={formatMessage(labels.visits)} value={data?.visits?.value}
formatValue={formatLongNumber} label={formatMessage(labels.visits)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.pageviews?.value} <MetricCard
label={formatMessage(labels.views)} value={data?.pageviews?.value}
formatValue={formatLongNumber} label={formatMessage(labels.views)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.events?.value} <MetricCard
label={formatMessage(labels.events)} value={data?.events?.value}
formatValue={formatLongNumber} label={formatMessage(labels.events)}
/> formatValue={formatLongNumber}
</MetricsBar> />
</MetricsBar>
)}
</LoadingPanel> </LoadingPanel>
); );
} }

View file

@ -67,7 +67,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
country: string; country: string;
device: string; device: string;
}) => { }) => {
const { __type, eventName, urlPath: url, browser, os, country, device } = log; const { __type, eventName, urlPath, browser, os, country, device } = log;
if (__type === TYPE_EVENT) { if (__type === TYPE_EVENT) {
return formatMessage(messages.eventLog, { return formatMessage(messages.eventLog, {
@ -75,12 +75,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
url: ( url: (
<a <a
key="a" key="a"
href={`//${website?.domain}${url}`} href={`//${website?.domain}${urlPath}`}
className={styles.link} className={styles.link}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
{url} {urlPath}
</a> </a>
), ),
}); });
@ -89,12 +89,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
if (__type === TYPE_PAGEVIEW) { if (__type === TYPE_PAGEVIEW) {
return ( return (
<a <a
href={`//${website?.domain}${url}`} href={`//${website?.domain}${urlPath}`}
className={styles.link} className={styles.link}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
{url} {urlPath}
</a> </a>
); );
} }

View file

@ -66,7 +66,13 @@ export function ReportsNav({ websiteId }: { websiteId: string }) {
const isSelected = selected === id; const isSelected = selected === id;
return ( return (
<Link key={id} href={renderUrl(`/websites/${websiteId}/reports${path}`)}> <Link
key={id}
href={renderUrl(
`/websites/${websiteId}/reports${path}`,
path === '/retention' ? false : null,
)}
>
<NavMenuItem isSelected={isSelected}> <NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon>{icon}</Icon> <Icon>{icon}</Icon>

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Grid, Row, Column, Text, Icon } from '@umami/react-zen'; import { Grid, Row, Column, Text, Icon } from '@umami/react-zen';
import { Users } from '@/components/icons'; import { Users } from '@/components/icons';
import { useMessages, useLocale, useResultQuery } from '@/components/hooks'; import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks';
import { formatDate } from '@/lib/date'; import { formatDate } from '@/lib/date';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
@ -19,8 +19,10 @@ export interface RetentionProps {
export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) { export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const { timezone } = useTimezone();
const { data, error, isLoading } = useResultQuery<any>('retention', { const { data, error, isLoading } = useResultQuery<any>('retention', {
websiteId, websiteId,
timezone,
dateRange: { dateRange: {
startDate, startDate,
endDate, endDate,
@ -51,54 +53,56 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
return ( return (
<LoadingPanel data={data} isLoading={isLoading} error={error}> <LoadingPanel data={data} isLoading={isLoading} error={error}>
<Panel allowFullscreen height="900px"> {data && (
<Column gap="1" width="100%" overflow="auto"> <Panel allowFullscreen height="900px">
<Grid <Column gap="1" width="100%" overflow="auto">
columns="120px repeat(10, 100px)" <Grid
alignItems="center" columns="120px repeat(10, 100px)"
gap="1" alignItems="center"
height="50px" gap="1"
autoFlow="column" height="50px"
> autoFlow="column"
<Column> >
<Text weight="bold" align="center"> <Column>
{formatMessage(labels.cohort)} <Text weight="bold" align="center">
</Text> {formatMessage(labels.cohort)}
</Column>
{days.map(n => (
<Column key={n}>
<Text weight="bold" align="center" wrap="nowrap">
{formatMessage(labels.day)} {n}
</Text> </Text>
</Column> </Column>
))} {days.map(n => (
</Grid> <Column key={n}>
{rows.map(({ date, visitors, records }: any, rowIndex: number) => { <Text weight="bold" align="center" wrap="nowrap">
return ( {formatMessage(labels.day)} {n}
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column"> </Text>
<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> </Column>
{days.map(day => { ))}
if (totalDays - rowIndex < day) { </Grid>
return null; {rows.map(({ date, visitors, records }: any, rowIndex: number) => {
} return (
const percentage = records.filter(a => a.day === day)[0]?.percentage; <Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
return ( <Column justifyContent="center" gap="1">
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell> <Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
); <Row alignItems="center" gap>
})} <Icon>
</Grid> <Users />
); </Icon>
})} <Text>{formatLongNumber(visitors)}</Text>
</Column> </Row>
</Panel> </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> </LoadingPanel>
); );
} }

View file

@ -3,16 +3,20 @@ import { Column } from '@umami/react-zen';
import { Retention } from './Retention'; import { Retention } from './Retention';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { useDateRange } from '@/components/hooks'; import { useDateRange } from '@/components/hooks';
import { endOfMonth, startOfMonth } from 'date-fns';
export function RetentionPage({ websiteId }: { websiteId: string }) { export function RetentionPage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate },
} = useDateRange(websiteId); } = useDateRange(websiteId);
const monthStartDate = startOfMonth(startDate);
const monthEndDate = endOfMonth(startDate);
return ( return (
<Column gap> <Column gap>
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} allowDateFilter={false} allowMonthFilter />
<Retention websiteId={websiteId} startDate={startDate} endDate={endDate} /> <Retention websiteId={websiteId} startDate={monthStartDate} endDate={monthEndDate} />
</Column> </Column>
); );
} }

View file

@ -11,28 +11,30 @@ export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
return ( return (
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}> <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<MetricsBar> {data && (
<MetricCard <MetricsBar>
value={data?.visitors?.value} <MetricCard
label={formatMessage(labels.visitors)} value={data?.visitors?.value}
formatValue={formatLongNumber} label={formatMessage(labels.visitors)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.visits?.value} <MetricCard
label={formatMessage(labels.visits)} value={data?.visits?.value}
formatValue={formatLongNumber} label={formatMessage(labels.visits)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.pageviews?.value} <MetricCard
label={formatMessage(labels.views)} value={data?.pageviews?.value}
formatValue={formatLongNumber} label={formatMessage(labels.views)}
/> formatValue={formatLongNumber}
<MetricCard />
value={data?.countries?.value} <MetricCard
label={formatMessage(labels.countries)} value={data?.countries?.value}
formatValue={formatLongNumber} label={formatMessage(labels.countries)}
/> formatValue={formatLongNumber}
</MetricsBar> />
</MetricsBar>
)}
</LoadingPanel> </LoadingPanel>
); );
} }

View file

@ -14,6 +14,7 @@ export async function POST(request: Request) {
const { const {
websiteId, websiteId,
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
timezone,
} = body; } = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
@ -23,6 +24,7 @@ export async function POST(request: Request) {
const data = await getRetention(websiteId, { const data = await getRetention(websiteId, {
startDate: new Date(startDate), startDate: new Date(startDate),
endDate: new Date(endDate), endDate: new Date(endDate),
timezone,
}); });
return json(data); return json(data);

View file

@ -62,7 +62,7 @@ export function Panel({
<Button variant="quiet" onPress={handleFullscreen}> <Button variant="quiet" onPress={handleFullscreen}>
<Icon>{isFullscreen ? <Close /> : <Maximize />}</Icon> <Icon>{isFullscreen ? <Close /> : <Maximize />}</Icon>
</Button> </Button>
<Tooltip>{formatMessage(labels.expand)}</Tooltip> <Tooltip>{formatMessage(labels.maximize)}</Tooltip>
</TooltipTrigger> </TooltipTrigger>
</Row> </Row>
)} )}

View file

@ -22,7 +22,7 @@ export function useDateRange(websiteId?: string) {
); );
const dateRange = useMemo( const dateRange = useMemo(
() => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject), () => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject),
[date, offset], [date, offset, websiteConfig],
); );
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE); const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);

View file

@ -8,7 +8,7 @@ export function useFilterParams(websiteId: string) {
const { timezone, toUtc } = useTimezone(); const { timezone, toUtc } = useTimezone();
const { const {
query: { query: {
url, path,
referrer, referrer,
title, title,
query, query,
@ -29,7 +29,7 @@ export function useFilterParams(websiteId: string) {
endAt: +toUtc(endDate), endAt: +toUtc(endDate),
unit, unit,
timezone, timezone,
url, path,
referrer, referrer,
title, title,
query, query,

View file

@ -99,7 +99,7 @@ export function DateFilter({
}; };
return ( return (
<Row width="280px"> <Row minWidth="200px">
<Select <Select
value={value} value={value}
placeholder={formatMessage(labels.selectDate)} placeholder={formatMessage(labels.selectDate)}

View file

@ -1,70 +1,47 @@
import { Row, Text, Icon, Button, MenuTrigger, Popover, Menu, MenuItem } from '@umami/react-zen'; import { Row, Select, ListItem } from '@umami/react-zen';
import { startOfMonth, endOfMonth, startOfYear, addMonths, subYears } from 'date-fns';
import { Chevron } from '@/components/icons';
import { useLocale } from '@/components/hooks'; import { useLocale } from '@/components/hooks';
import { formatDate } from '@/lib/date'; import { formatDate } from '@/lib/date';
export function MonthSelect({ date = new Date(), onChange }) { export function MonthSelect({ date = new Date(), onChange }) {
const { locale } = useLocale(); const { locale } = useLocale();
const month = formatDate(date, 'MMMM', locale); const month = date.getMonth();
const year = date.getFullYear(); const year = date.getFullYear();
const currentYear = new Date().getFullYear();
// eslint-disable-next-line const months = [...Array(12)].map((_, i) => i);
const handleChange = (close: () => void, date: Date) => { const years = [...Array(10)].map((_, i) => currentYear - i);
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
close(); const handleMonthChange = (month: number) => {
const d = new Date(date);
d.setMonth(month);
onChange?.(d);
};
const handleYearChange = (year: number) => {
const d = new Date(date);
d.setFullYear(year);
onChange?.(d);
}; };
const start = startOfYear(date);
const months: Date[] = [];
for (let i = 0; i < 12; i++) {
months.push(addMonths(start, i));
}
const years: number[] = [];
for (let i = 0; i < 10; i++) {
years.push(subYears(start, 10 - i).getFullYear());
}
return ( return (
<Row> <Row gap>
<MenuTrigger> <Select value={month} onChange={handleMonthChange}>
<Button variant="quiet"> {months.map(m => {
<Text>{month}</Text> return (
<Icon size="sm"> <ListItem id={m} key={m}>
<Chevron /> {formatDate(new Date(year, m, 1), 'MMMM', locale)}
</Icon> </ListItem>
</Button> );
<Popover> })}
<Menu> </Select>
{months.map(month => { <Select value={year} onChange={handleYearChange}>
return ( {years.map(y => {
<MenuItem key={month.toString()} id={month.toString()}> return (
{month.getDay()} <ListItem id={y} key={y}>
</MenuItem> {y}
); </ListItem>
})} );
</Menu> })}
</Popover> </Select>
</MenuTrigger>
<MenuTrigger>
<Button variant="quiet">
<Text>{year}</Text>
<Icon size="sm">
<Chevron />
</Icon>
</Button>
<Popover>
<Menu>
{years.map(year => {
return (
<MenuItem key={year} id={year}>
{year}
</MenuItem>
);
})}
</Menu>
</Popover>
</MenuTrigger>
</Row> </Row>
); );
} }

View file

@ -55,7 +55,7 @@ export function WebsiteDateFilter({
}; };
return ( return (
<Row gap="3"> <Row gap>
{showButtons && !isAllTime && !isCustomRange && ( {showButtons && !isAllTime && !isCustomRange && (
<Row gap="1"> <Row gap="1">
<Button onPress={() => handleIncrement(-1)} variant="outline"> <Button onPress={() => handleIncrement(-1)} variant="outline">

View file

@ -0,0 +1,17 @@
import { useDateRange } from '@/components/hooks';
import { dateToRangeValue } from '@/lib/date';
import { MonthSelect } from './MonthSelect';
export function WebsiteMonthSelect({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate },
saveDateRange,
} = useDateRange(websiteId);
const handleMonthSelect = (date: Date) => {
const range = dateToRangeValue(date);
saveDateRange(range);
};
return <MonthSelect date={startDate} onChange={handleMonthSelect} />;
}

View file

@ -271,7 +271,7 @@ export const labels = defineMessages({
defaultMessage: 'Track your campaigns through UTM parameters.', defaultMessage: 'Track your campaigns through UTM parameters.',
}, },
conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion step' }, conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion step' },
conversionRate: { id: 'label.conversion-ratep', defaultMessage: 'Conversion rate' }, conversionRate: { id: 'label.conversion-rate', defaultMessage: 'Conversion rate' },
steps: { id: 'label.steps', defaultMessage: 'Steps' }, steps: { id: 'label.steps', defaultMessage: 'Steps' },
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' }, startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
endStep: { id: 'label.end-step', defaultMessage: 'End Step' }, endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
@ -325,7 +325,7 @@ export const labels = defineMessages({
pixels: { id: 'label.pixels', defaultMessage: 'Pixels' }, pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
addBoard: { id: 'label.add-board', defaultMessage: 'Add board' }, addBoard: { id: 'label.add-board', defaultMessage: 'Add board' },
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' }, cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
expand: { id: 'label.expand', defaultMessage: 'Expand' }, maximize: { id: 'label.maximize', defaultMessage: 'Maximize' },
remaining: { id: 'label.remaining', defaultMessage: 'Remaining' }, remaining: { id: 'label.remaining', defaultMessage: 'Remaining' },
conversion: { id: 'label.conversion', defaultMessage: 'Conversion' }, conversion: { id: 'label.conversion', defaultMessage: 'Conversion' },
firstClick: { id: 'label.first-click', defaultMessage: 'First click' }, firstClick: { id: 'label.first-click', defaultMessage: 'First click' },

View file

@ -349,3 +349,7 @@ export function generateTimeSeries(
return { x: t, d: x, y: y ?? null }; return { x: t, d: x, y: y ?? null };
}); });
} }
export function dateToRangeValue(date: Date) {
return `range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`;
}

View file

@ -5,6 +5,15 @@ import prisma from '@/lib/prisma';
export interface RetentionCriteria { export interface RetentionCriteria {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
timezone?: string;
}
export interface RetentionResult {
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
} }
export async function getRetention(...args: [websiteId: string, criteria: RetentionCriteria]) { export async function getRetention(...args: [websiteId: string, criteria: RetentionCriteria]) {
@ -17,16 +26,8 @@ export async function getRetention(...args: [websiteId: string, criteria: Retent
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: RetentionCriteria, criteria: RetentionCriteria,
): Promise< ): Promise<RetentionResult[]> {
{ const { startDate, endDate, timezone } = criteria;
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate } = criteria;
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
const unit = 'day'; const unit = 'day';
@ -42,7 +43,7 @@ async function relationalQuery(
user_activities AS ( user_activities AS (
select distinct select distinct
w.session_id, w.session_id,
${getDayDiffQuery(getDateSQL('created_at', unit), 'c.cohort_date')} as day_number ${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number
from website_event w from website_event w
join cohort_items c join cohort_items c
on w.session_id = c.session_id on w.session_id = c.session_id
@ -88,16 +89,8 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: RetentionCriteria, criteria: RetentionCriteria,
): Promise< ): Promise<RetentionResult[]> {
{ const { startDate, endDate, timezone } = criteria;
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate } = criteria;
const { getDateSQL, rawQuery } = clickhouse; const { getDateSQL, rawQuery } = clickhouse;
const unit = 'day'; const unit = 'day';
@ -105,7 +98,7 @@ async function clickhouseQuery(
` `
WITH cohort_items AS ( WITH cohort_items AS (
select select
min(${getDateSQL('created_at', unit)}) as cohort_date, min(${getDateSQL('created_at', unit, timezone)}) as cohort_date,
session_id session_id
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}