mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
add psql query for retention
This commit is contained in:
parent
ff5884237c
commit
13530c9cdc
@ -4,6 +4,11 @@ import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from '
|
|||||||
import { ReportContext } from 'components/pages/reports/Report';
|
import { ReportContext } from 'components/pages/reports/Report';
|
||||||
import BaseParameters from '../BaseParameters';
|
import BaseParameters from '../BaseParameters';
|
||||||
|
|
||||||
|
const fieldOptions = [
|
||||||
|
{ name: 'daily', type: 'string' },
|
||||||
|
{ name: 'weekly', type: 'string' },
|
||||||
|
];
|
||||||
|
|
||||||
export function RetentionParameters() {
|
export function RetentionParameters() {
|
||||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
@ -24,14 +29,7 @@ export function RetentionParameters() {
|
|||||||
return (
|
return (
|
||||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
<BaseParameters />
|
<BaseParameters />
|
||||||
<FormRow label={formatMessage(labels.window)}>
|
<FormRow label={formatMessage(labels.window)} />
|
||||||
<FormInput
|
|
||||||
name="window"
|
|
||||||
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/ }}
|
|
||||||
>
|
|
||||||
<TextField autoComplete="off" />
|
|
||||||
</FormInput>
|
|
||||||
</FormRow>
|
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||||
{formatMessage(labels.runQuery)}
|
{formatMessage(labels.runQuery)}
|
||||||
|
@ -8,8 +8,8 @@ import ReportBody from '../ReportBody';
|
|||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: 'Retention',
|
type: 'retention',
|
||||||
parameters: { window: 60, urls: [] },
|
parameters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RetentionReport({ reportId }) {
|
export default function RetentionReport({ reportId }) {
|
||||||
@ -20,7 +20,7 @@ export default function RetentionReport({ reportId }) {
|
|||||||
<RetentionParameters />
|
<RetentionParameters />
|
||||||
</ReportMenu>
|
</ReportMenu>
|
||||||
<ReportBody>
|
<ReportBody>
|
||||||
<RetentionChart />
|
{/* <RetentionChart /> */}
|
||||||
<RetentionTable />
|
<RetentionTable />
|
||||||
</ReportBody>
|
</ReportBody>
|
||||||
</Report>
|
</Report>
|
||||||
|
@ -1,18 +1,29 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import DataTable from 'components/metrics/DataTable';
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
import { ReportContext } from '../Report';
|
import { ReportContext } from '../Report';
|
||||||
|
|
||||||
export function RetentionTable() {
|
export function RetentionTable() {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { fields = [] } = report?.parameters || {};
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <GridTable data={report?.data || []}>
|
||||||
|
// {fields.map(({ name }) => {
|
||||||
|
// return <GridColumn key={name} name={name} label={name} />;
|
||||||
|
// })}
|
||||||
|
// <GridColumn name="total" label={formatMessage(labels.total)} />
|
||||||
|
// </GridTable>
|
||||||
|
// );
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<GridTable data={report?.data || []}>
|
||||||
data={report?.data}
|
<GridColumn name="cohortDate">{row => row.cohortDate}</GridColumn>
|
||||||
title={formatMessage(labels.url)}
|
<GridColumn name="dateNumber">{row => row.date_number}</GridColumn>
|
||||||
metric={formatMessage(labels.visitors)}
|
<GridColumn name="visitors" label={formatMessage(labels.visitors)}>
|
||||||
showPercentage={true}
|
{row => row.date_number}
|
||||||
/>
|
</GridColumn>
|
||||||
|
</GridTable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,17 +7,12 @@ import { getRetention } from 'queries';
|
|||||||
|
|
||||||
export interface RetentionRequestBody {
|
export interface RetentionRequestBody {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
urls: string[];
|
window: string;
|
||||||
window: number;
|
dateRange: { window; startDate: string; endDate: string };
|
||||||
dateRange: {
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RetentionResponse {
|
export interface RetentionResponse {
|
||||||
urls: string[];
|
window: string;
|
||||||
window: number;
|
|
||||||
startAt: number;
|
startAt: number;
|
||||||
endAt: number;
|
endAt: number;
|
||||||
}
|
}
|
||||||
@ -32,7 +27,6 @@ export default async (
|
|||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const {
|
const {
|
||||||
websiteId,
|
websiteId,
|
||||||
urls,
|
|
||||||
window,
|
window,
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = req.body;
|
} = req.body;
|
||||||
@ -44,8 +38,7 @@ export default async (
|
|||||||
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),
|
||||||
urls,
|
window: window,
|
||||||
windowMinutes: +window,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
|
@ -6,204 +6,200 @@ export async function getRetention(
|
|||||||
...args: [
|
...args: [
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
criteria: {
|
criteria: {
|
||||||
windowMinutes: number;
|
window: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
urls: string[];
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
// [CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function relationalQuery(
|
async function relationalQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
criteria: {
|
criteria: {
|
||||||
windowMinutes: number;
|
window: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
urls: string[];
|
|
||||||
},
|
},
|
||||||
): Promise<
|
): Promise<
|
||||||
{
|
{
|
||||||
x: string;
|
date: Date;
|
||||||
y: number;
|
visitors: number;
|
||||||
z: number;
|
day: number;
|
||||||
|
percentage: number;
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const { windowMinutes, startDate, endDate, urls } = criteria;
|
const { window, startDate, endDate } = criteria;
|
||||||
const { rawQuery, getAddMinutesQuery } = prisma;
|
const { rawQuery } = prisma;
|
||||||
const { levelQuery, sumQuery } = getRetentionQuery(urls, windowMinutes);
|
|
||||||
|
|
||||||
function getRetentionQuery(
|
|
||||||
urls: string[],
|
|
||||||
windowMinutes: number,
|
|
||||||
): {
|
|
||||||
levelQuery: string;
|
|
||||||
sumQuery: string;
|
|
||||||
} {
|
|
||||||
return urls.reduce(
|
|
||||||
(pv, cv, i) => {
|
|
||||||
const levelNumber = i + 1;
|
|
||||||
const startSum = i > 0 ? 'union ' : '';
|
|
||||||
|
|
||||||
if (levelNumber >= 2) {
|
|
||||||
pv.levelQuery += `
|
|
||||||
, level${levelNumber} AS (
|
|
||||||
select distinct we.session_id, we.created_at
|
|
||||||
from level${i} l
|
|
||||||
join website_event we
|
|
||||||
on l.session_id = we.session_id
|
|
||||||
where we.created_at between l.created_at
|
|
||||||
and ${getAddMinutesQuery(`l.created_at `, windowMinutes)}
|
|
||||||
and we.referrer_path = {{${i - 1}}}
|
|
||||||
and we.url_path = {{${i}}}
|
|
||||||
and we.created_at <= {{endDate}}
|
|
||||||
and we.website_id = {{websiteId::uuid}}
|
|
||||||
)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
|
|
||||||
|
|
||||||
return pv;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
levelQuery: '',
|
|
||||||
sumQuery: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`
|
`
|
||||||
WITH level1 AS (
|
WITH cohort_items AS (
|
||||||
select distinct session_id, created_at
|
select
|
||||||
from website_event
|
date_trunc('week', created_at)::date as cohort_date,
|
||||||
|
session_id
|
||||||
|
from session
|
||||||
where website_id = {{websiteId::uuid}}
|
where website_id = {{websiteId::uuid}}
|
||||||
and created_at between {{startDate}} and {{endDate}}
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
and url_path = {{0}}
|
order by 1, 2
|
||||||
)
|
|
||||||
${levelQuery}
|
|
||||||
${sumQuery}
|
|
||||||
ORDER BY level;
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
websiteId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
...urls,
|
|
||||||
},
|
|
||||||
).then(results => {
|
|
||||||
return urls.map((a, i) => ({
|
|
||||||
x: a,
|
|
||||||
y: results[i]?.count || 0,
|
|
||||||
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickhouseQuery(
|
|
||||||
websiteId: string,
|
|
||||||
criteria: {
|
|
||||||
windowMinutes: number;
|
|
||||||
startDate: Date;
|
|
||||||
endDate: Date;
|
|
||||||
urls: string[];
|
|
||||||
},
|
|
||||||
): Promise<
|
|
||||||
{
|
|
||||||
x: string;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
}[]
|
|
||||||
> {
|
|
||||||
const { windowMinutes, startDate, endDate, urls } = criteria;
|
|
||||||
const { rawQuery } = clickhouse;
|
|
||||||
const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery(
|
|
||||||
urls,
|
|
||||||
windowMinutes,
|
|
||||||
);
|
|
||||||
|
|
||||||
function getRetentionQuery(
|
|
||||||
urls: string[],
|
|
||||||
windowMinutes: number,
|
|
||||||
): {
|
|
||||||
levelQuery: string;
|
|
||||||
sumQuery: string;
|
|
||||||
urlFilterQuery: string;
|
|
||||||
urlParams: { [key: string]: string };
|
|
||||||
} {
|
|
||||||
return urls.reduce(
|
|
||||||
(pv, cv, i) => {
|
|
||||||
const levelNumber = i + 1;
|
|
||||||
const startSum = i > 0 ? 'union all ' : '';
|
|
||||||
const startFilter = i > 0 ? ', ' : '';
|
|
||||||
|
|
||||||
if (levelNumber >= 2) {
|
|
||||||
pv.levelQuery += `\n
|
|
||||||
, level${levelNumber} AS (
|
|
||||||
select distinct y.session_id as session_id,
|
|
||||||
y.url_path as url_path,
|
|
||||||
y.referrer_path as referrer_path,
|
|
||||||
y.created_at as created_at
|
|
||||||
from level${i} x
|
|
||||||
join level0 y
|
|
||||||
on x.session_id = y.session_id
|
|
||||||
where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute
|
|
||||||
and y.referrer_path = {url${i - 1}:String}
|
|
||||||
and y.url_path = {url${i}:String}
|
|
||||||
)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
|
|
||||||
pv.urlFilterQuery += `${startFilter}{url${i}:String} `;
|
|
||||||
pv.urlParams[`url${i}`] = cv;
|
|
||||||
|
|
||||||
return pv;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
levelQuery: '',
|
|
||||||
sumQuery: '',
|
|
||||||
urlFilterQuery: '',
|
|
||||||
urlParams: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawQuery<{ level: number; count: number }[]>(
|
|
||||||
`
|
|
||||||
WITH level0 AS (
|
|
||||||
select distinct session_id, url_path, referrer_path, created_at
|
|
||||||
from umami.website_event
|
|
||||||
where url_path in (${urlFilterQuery})
|
|
||||||
and website_id = {websiteId:UUID}
|
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
|
||||||
),
|
),
|
||||||
level1 AS (
|
user_activities AS (
|
||||||
select *
|
select distinct
|
||||||
from level0
|
w.session_id,
|
||||||
where url_path = {url0:String}
|
(date_trunc('week', w.created_at)::date - c.cohort_date::date) / 7 as date_number
|
||||||
|
from website_event w
|
||||||
|
left join cohort_items c
|
||||||
|
on w.session_id = c.session_id
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
),
|
||||||
|
cohort_size as (
|
||||||
|
select cohort_date,
|
||||||
|
count(*) as visitors
|
||||||
|
from cohort_items
|
||||||
|
group by 1
|
||||||
|
order by 1
|
||||||
|
),
|
||||||
|
cohort_date as (
|
||||||
|
select
|
||||||
|
c.cohort_date,
|
||||||
|
a.date_number,
|
||||||
|
count(*) as visitors
|
||||||
|
from user_activities a
|
||||||
|
left join cohort_items c
|
||||||
|
on a.session_id = c.session_id
|
||||||
|
group by 1, 2
|
||||||
)
|
)
|
||||||
${levelQuery}
|
select
|
||||||
select *
|
c.cohort_date,
|
||||||
from (
|
c.date_number,
|
||||||
${sumQuery}
|
s.visitors,
|
||||||
) ORDER BY level;
|
c.visitors,
|
||||||
`,
|
c.visitors::float * 100 / s.visitors as percentage
|
||||||
|
from cohort_date c
|
||||||
|
left join cohort_size s
|
||||||
|
on c.cohort_date = s.cohort_date
|
||||||
|
where c.cohort_date IS NOT NULL
|
||||||
|
order by 1, 2`,
|
||||||
{
|
{
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
...urlParams,
|
window,
|
||||||
},
|
},
|
||||||
).then(results => {
|
).then(results => {
|
||||||
return urls.map((a, i) => ({
|
return results;
|
||||||
x: a,
|
// return results.map((a, i) => ({
|
||||||
y: results[i]?.count || 0,
|
// x: a,
|
||||||
z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
|
// y: results[i]?.count || 0,
|
||||||
}));
|
// z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
|
||||||
|
// }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// async function clickhouseQuery(
|
||||||
|
// websiteId: string,
|
||||||
|
// criteria: {
|
||||||
|
// windowMinutes: number;
|
||||||
|
// startDate: Date;
|
||||||
|
// endDate: Date;
|
||||||
|
// urls: string[];
|
||||||
|
// },
|
||||||
|
// ): Promise<
|
||||||
|
// {
|
||||||
|
// x: string;
|
||||||
|
// y: number;
|
||||||
|
// z: number;
|
||||||
|
// }[]
|
||||||
|
// > {
|
||||||
|
// const { windowMinutes, startDate, endDate, urls } = criteria;
|
||||||
|
// const { rawQuery } = clickhouse;
|
||||||
|
// const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getRetentionQuery(
|
||||||
|
// urls,
|
||||||
|
// windowMinutes,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// function getRetentionQuery(
|
||||||
|
// urls: string[],
|
||||||
|
// windowMinutes: number,
|
||||||
|
// ): {
|
||||||
|
// levelQuery: string;
|
||||||
|
// sumQuery: string;
|
||||||
|
// urlFilterQuery: string;
|
||||||
|
// urlParams: { [key: string]: string };
|
||||||
|
// } {
|
||||||
|
// return urls.reduce(
|
||||||
|
// (pv, cv, i) => {
|
||||||
|
// const levelNumber = i + 1;
|
||||||
|
// const startSum = i > 0 ? 'union all ' : '';
|
||||||
|
// const startFilter = i > 0 ? ', ' : '';
|
||||||
|
|
||||||
|
// if (levelNumber >= 2) {
|
||||||
|
// pv.levelQuery += `\n
|
||||||
|
// , level${levelNumber} AS (
|
||||||
|
// select distinct y.session_id as session_id,
|
||||||
|
// y.url_path as url_path,
|
||||||
|
// y.referrer_path as referrer_path,
|
||||||
|
// y.created_at as created_at
|
||||||
|
// from level${i} x
|
||||||
|
// join level0 y
|
||||||
|
// on x.session_id = y.session_id
|
||||||
|
// where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute
|
||||||
|
// and y.referrer_path = {url${i - 1}:String}
|
||||||
|
// and y.url_path = {url${i}:String}
|
||||||
|
// )`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
|
||||||
|
// pv.urlFilterQuery += `${startFilter}{url${i}:String} `;
|
||||||
|
// pv.urlParams[`url${i}`] = cv;
|
||||||
|
|
||||||
|
// return pv;
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// levelQuery: '',
|
||||||
|
// sumQuery: '',
|
||||||
|
// urlFilterQuery: '',
|
||||||
|
// urlParams: {},
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return rawQuery<{ level: number; count: number }[]>(
|
||||||
|
// `
|
||||||
|
// WITH level0 AS (
|
||||||
|
// select distinct session_id, url_path, referrer_path, created_at
|
||||||
|
// from umami.website_event
|
||||||
|
// where url_path in (${urlFilterQuery})
|
||||||
|
// and website_id = {websiteId:UUID}
|
||||||
|
// and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
// ),
|
||||||
|
// level1 AS (
|
||||||
|
// select *
|
||||||
|
// from level0
|
||||||
|
// where url_path = {url0:String}
|
||||||
|
// )
|
||||||
|
// ${levelQuery}
|
||||||
|
// select *
|
||||||
|
// from (
|
||||||
|
// ${sumQuery}
|
||||||
|
// ) ORDER BY level;
|
||||||
|
// `,
|
||||||
|
// {
|
||||||
|
// websiteId,
|
||||||
|
// startDate,
|
||||||
|
// endDate,
|
||||||
|
// ...urlParams,
|
||||||
|
// },
|
||||||
|
// ).then(results => {
|
||||||
|
// return urls.map((a, i) => ({
|
||||||
|
// x: a,
|
||||||
|
// y: results[i]?.count || 0,
|
||||||
|
// z: (1 - Number(results[i]?.count) / Number(results[i - 1]?.count)) * 100 || 0, // drop off
|
||||||
|
// }));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user