mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
finish clickhouse journeys query
This commit is contained in:
parent
3a6971e173
commit
0333bec986
@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import FunnelReport from '../funnel/FunnelReport';
|
import { useReport } from 'components/hooks';
|
||||||
import EventDataReport from '../event-data/EventDataReport';
|
import EventDataReport from '../event-data/EventDataReport';
|
||||||
|
import FunnelReport from '../funnel/FunnelReport';
|
||||||
|
import GoalReport from '../goals/GoalsReport';
|
||||||
import InsightsReport from '../insights/InsightsReport';
|
import InsightsReport from '../insights/InsightsReport';
|
||||||
|
import JourneyReport from '../journey/JourneyReport';
|
||||||
import RetentionReport from '../retention/RetentionReport';
|
import RetentionReport from '../retention/RetentionReport';
|
||||||
import UTMReport from '../utm/UTMReport';
|
import UTMReport from '../utm/UTMReport';
|
||||||
import GoalReport from '../goals/GoalsReport';
|
|
||||||
import { useReport } from 'components/hooks';
|
|
||||||
|
|
||||||
const reports = {
|
const reports = {
|
||||||
funnel: FunnelReport,
|
funnel: FunnelReport,
|
||||||
@ -14,6 +15,7 @@ const reports = {
|
|||||||
retention: RetentionReport,
|
retention: RetentionReport,
|
||||||
utm: UTMReport,
|
utm: UTMReport,
|
||||||
goals: GoalReport,
|
goals: GoalReport,
|
||||||
|
journey: JourneyReport,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ReportPage({ reportId }: { reportId: string }) {
|
export default function ReportPage({ reportId }: { reportId: string }) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { Form, FormButtons, SubmitButton } from 'react-basics';
|
import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics';
|
||||||
import { ReportContext } from '../[reportId]/Report';
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
import BaseParameters from '../[reportId]/BaseParameters';
|
import BaseParameters from '../[reportId]/BaseParameters';
|
||||||
|
|
||||||
@ -9,8 +9,8 @@ export function JourneyParameters() {
|
|||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const { id, parameters } = report || {};
|
const { id, parameters } = report || {};
|
||||||
const { websiteId, dateRange } = parameters || {};
|
const { websiteId, dateRange, steps } = parameters || {};
|
||||||
const queryDisabled = !websiteId || !dateRange;
|
const queryDisabled = !websiteId || !dateRange || !steps;
|
||||||
|
|
||||||
const handleSubmit = (data: any, e: any) => {
|
const handleSubmit = (data: any, e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -24,6 +24,24 @@ export function JourneyParameters() {
|
|||||||
return (
|
return (
|
||||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||||
|
<FormRow label={`${formatMessage(labels.steps)} (3 to 7)`}>
|
||||||
|
<FormInput
|
||||||
|
name="steps"
|
||||||
|
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/, min: 3, max: 7 }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow label={formatMessage(labels.startStep)}>
|
||||||
|
<FormInput name="startStep">
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow label={formatMessage(labels.endStep)}>
|
||||||
|
<FormInput name="endStep">
|
||||||
|
<TextField autoComplete="off" />
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||||
{formatMessage(labels.runQuery)}
|
{formatMessage(labels.runQuery)}
|
||||||
|
@ -10,7 +10,7 @@ import { REPORT_TYPES } from 'lib/constants';
|
|||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: REPORT_TYPES.journey,
|
type: REPORT_TYPES.journey,
|
||||||
parameters: {},
|
parameters: { steps: 5 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function JourneyReport({ reportId }: { reportId?: string }) {
|
export default function JourneyReport({ reportId }: { reportId?: string }) {
|
||||||
|
@ -8,14 +8,13 @@ import { useEscapeKey } from 'components/hooks';
|
|||||||
export default function JourneyView() {
|
export default function JourneyView() {
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
const { data } = report || {};
|
const { data, parameters } = report || {};
|
||||||
useEscapeKey(() => setSelected(null));
|
useEscapeKey(() => setSelected(null));
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Array(data[0].items.length)
|
return Array(Number(parameters.steps))
|
||||||
.fill(undefined)
|
.fill(undefined)
|
||||||
.map((col = {}, index) => {
|
.map((col = {}, index) => {
|
||||||
data.forEach(({ items, count }) => {
|
data.forEach(({ items, count }) => {
|
||||||
|
@ -240,6 +240,8 @@ export const labels = defineMessages({
|
|||||||
defaultMessage: 'Track your campaigns through UTM parameters.',
|
defaultMessage: 'Track your campaigns through UTM parameters.',
|
||||||
},
|
},
|
||||||
steps: { id: 'label.steps', defaultMessage: 'Steps' },
|
steps: { id: 'label.steps', defaultMessage: 'Steps' },
|
||||||
|
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
|
||||||
|
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
|
||||||
addStep: { id: 'label.add-step', defaultMessage: 'Add step' },
|
addStep: { id: 'label.add-step', defaultMessage: 'Add step' },
|
||||||
goal: { id: 'label.goal', defaultMessage: 'Goal' },
|
goal: { id: 'label.goal', defaultMessage: 'Goal' },
|
||||||
goals: { id: 'label.goals', defaultMessage: 'Goals' },
|
goals: { id: 'label.goals', defaultMessage: 'Goals' },
|
||||||
|
@ -27,7 +27,7 @@ const schema = {
|
|||||||
name: yup.string().max(200).required(),
|
name: yup.string().max(200).required(),
|
||||||
type: yup
|
type: yup
|
||||||
.string()
|
.string()
|
||||||
.matches(/funnel|insights|retention|utm|goals/i)
|
.matches(/funnel|insights|retention|utm|goals|journey/i)
|
||||||
.required(),
|
.required(),
|
||||||
description: yup.string().max(500),
|
description: yup.string().max(500),
|
||||||
parameters: yup
|
parameters: yup
|
||||||
|
@ -9,6 +9,9 @@ import * as yup from 'yup';
|
|||||||
export interface RetentionRequestBody {
|
export interface RetentionRequestBody {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
dateRange: { startDate: string; endDate: string };
|
dateRange: { startDate: string; endDate: string };
|
||||||
|
steps: number;
|
||||||
|
startStep?: string;
|
||||||
|
endStep?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -21,6 +24,9 @@ const schema = {
|
|||||||
endDate: yup.date().required(),
|
endDate: yup.date().required(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
|
steps: yup.number().min(3).max(7).required(),
|
||||||
|
startStep: yup.string(),
|
||||||
|
endStep: yup.string(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,6 +42,9 @@ export default async (
|
|||||||
const {
|
const {
|
||||||
websiteId,
|
websiteId,
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
|
steps,
|
||||||
|
startStep,
|
||||||
|
endStep,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
@ -45,6 +54,9 @@ export default async (
|
|||||||
const data = await getJourney(websiteId, {
|
const data = await getJourney(websiteId, {
|
||||||
startDate: new Date(startDate),
|
startDate: new Date(startDate),
|
||||||
endDate: new Date(endDate),
|
endDate: new Date(endDate),
|
||||||
|
steps,
|
||||||
|
startStep,
|
||||||
|
endStep,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
|
@ -8,6 +8,8 @@ interface JourneyResult {
|
|||||||
e3: string;
|
e3: string;
|
||||||
e4: string;
|
e4: string;
|
||||||
e5: string;
|
e5: string;
|
||||||
|
e6: string;
|
||||||
|
e7: string;
|
||||||
count: string;
|
count: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +19,9 @@ export async function getJourney(
|
|||||||
filters: {
|
filters: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
steps: number;
|
||||||
|
startStep?: string;
|
||||||
|
endStep?: string;
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
@ -49,14 +54,14 @@ async function relationalQuery(
|
|||||||
and created_at between {{startDate}} and {{endDate}}
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
and referrer_path != url_path),
|
and referrer_path != url_path),
|
||||||
sequences as (
|
sequences as (
|
||||||
SELECT s.e1,
|
select s.e1,
|
||||||
s.e2,
|
s.e2,
|
||||||
s.e3,
|
s.e3,
|
||||||
s.e4,
|
s.e4,
|
||||||
s.e5,
|
s.e5,
|
||||||
count(*) count
|
count(*) count
|
||||||
FROM (
|
FROM (
|
||||||
SELECT session_id,
|
select session_id,
|
||||||
MAX(CASE WHEN event_number = 1 THEN event ELSE NULL END) AS e1,
|
MAX(CASE WHEN event_number = 1 THEN event ELSE NULL END) AS e1,
|
||||||
MAX(CASE WHEN event_number = 2 THEN event ELSE NULL END) AS e2,
|
MAX(CASE WHEN event_number = 2 THEN event ELSE NULL END) AS e2,
|
||||||
MAX(CASE WHEN event_number = 3 THEN event ELSE NULL END) AS e3,
|
MAX(CASE WHEN event_number = 3 THEN event ELSE NULL END) AS e3,
|
||||||
@ -87,46 +92,99 @@ async function clickhouseQuery(
|
|||||||
filters: {
|
filters: {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
|
steps: number;
|
||||||
|
startStep?: string;
|
||||||
|
endStep?: string;
|
||||||
},
|
},
|
||||||
): Promise<JourneyResult[]> {
|
): Promise<JourneyResult[]> {
|
||||||
const { startDate, endDate } = filters;
|
const { startDate, endDate, steps, startStep, endStep } = filters;
|
||||||
const { rawQuery } = clickhouse;
|
const { rawQuery } = clickhouse;
|
||||||
|
const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery(
|
||||||
|
steps,
|
||||||
|
startStep,
|
||||||
|
endStep,
|
||||||
|
);
|
||||||
|
|
||||||
|
function getJourneyQuery(
|
||||||
|
steps: number,
|
||||||
|
startStep?: string,
|
||||||
|
endStep?: string,
|
||||||
|
): {
|
||||||
|
sequenceQuery: string;
|
||||||
|
startStepQuery: string;
|
||||||
|
endStepQuery: string;
|
||||||
|
params: { [key: string]: string };
|
||||||
|
} {
|
||||||
|
const params = {};
|
||||||
|
let sequenceQuery = '';
|
||||||
|
let startStepQuery = '';
|
||||||
|
let endStepQuery = '';
|
||||||
|
|
||||||
|
// create sequence query
|
||||||
|
let selectQuery = '';
|
||||||
|
let maxQuery = '';
|
||||||
|
let groupByQuery = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= steps; i++) {
|
||||||
|
const endQuery = i < steps ? ',' : '';
|
||||||
|
selectQuery += `s.e${i},`;
|
||||||
|
maxQuery += `\nmax(CASE WHEN event_number = ${i} THEN event ELSE NULL END) AS e${i}${endQuery}`;
|
||||||
|
groupByQuery += `s.e${i}${endQuery} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceQuery = `\nsequences as (
|
||||||
|
select ${selectQuery}
|
||||||
|
count(*) count
|
||||||
|
FROM (
|
||||||
|
select visit_id,
|
||||||
|
${maxQuery}
|
||||||
|
FROM events
|
||||||
|
group by visit_id) s
|
||||||
|
group by ${groupByQuery})
|
||||||
|
`;
|
||||||
|
|
||||||
|
// create start Step params query
|
||||||
|
if (startStep) {
|
||||||
|
startStepQuery = `and e1 = {startStep:String}`;
|
||||||
|
params['startStep'] = startStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create end Step params query
|
||||||
|
if (endStep) {
|
||||||
|
for (let i = 1; i < steps; i++) {
|
||||||
|
const startQuery = i === 1 ? 'and (' : '\nor ';
|
||||||
|
endStepQuery += `${startQuery}(e${i} = {endStep:String} and e${i + 1} is null) `;
|
||||||
|
}
|
||||||
|
endStepQuery += `\nor (e${steps} = {endStep:String}))`;
|
||||||
|
|
||||||
|
params['endStep'] = endStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sequenceQuery,
|
||||||
|
startStepQuery,
|
||||||
|
endStepQuery,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`
|
`
|
||||||
WITH events AS (
|
WITH events AS (
|
||||||
select distinct
|
select distinct
|
||||||
session_id,
|
visit_id,
|
||||||
referrer_path,
|
referrer_path,
|
||||||
coalesce(nullIf(event_name, ''), url_path) event,
|
coalesce(nullIf(event_name, ''), url_path) event,
|
||||||
row_number() OVER (PARTITION BY session_id ORDER BY created_at) AS event_number
|
row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number
|
||||||
from umami.website_event
|
from umami.website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}),
|
||||||
and referrer_path != url_path),
|
${sequenceQuery}
|
||||||
sequences as (
|
|
||||||
SELECT s.e1,
|
|
||||||
s.e2,
|
|
||||||
s.e3,
|
|
||||||
s.e4,
|
|
||||||
s.e5,
|
|
||||||
count(*) count
|
|
||||||
FROM (
|
|
||||||
SELECT session_id,
|
|
||||||
max(CASE WHEN event_number = 1 THEN event ELSE NULL END) AS e1,
|
|
||||||
max(CASE WHEN event_number = 2 THEN event ELSE NULL END) AS e2,
|
|
||||||
max(CASE WHEN event_number = 3 THEN event ELSE NULL END) AS e3,
|
|
||||||
max(CASE WHEN event_number = 4 THEN event ELSE NULL END) AS e4,
|
|
||||||
max(CASE WHEN event_number = 5 THEN event ELSE NULL END) AS e5
|
|
||||||
FROM events
|
|
||||||
group by session_id) s
|
|
||||||
group by s.e1,
|
|
||||||
s.e2,
|
|
||||||
s.e3,
|
|
||||||
s.e4,
|
|
||||||
s.e5)
|
|
||||||
select *
|
select *
|
||||||
from sequences
|
from sequences
|
||||||
|
where 1 = 1
|
||||||
|
${startStepQuery}
|
||||||
|
${endStepQuery}
|
||||||
order by count desc
|
order by count desc
|
||||||
limit 100
|
limit 100
|
||||||
`,
|
`,
|
||||||
@ -134,10 +192,28 @@ async function clickhouseQuery(
|
|||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
...params,
|
||||||
},
|
},
|
||||||
).then(parseResult);
|
).then(parseResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseResult(data: any) {
|
function combineSequentialDuplicates(array: any) {
|
||||||
return data.map(({ e1, e2, e3, e4, e5, count }) => ({ items: [e1, e2, e3, e4, e5], count }));
|
if (array.length === 0) return array;
|
||||||
|
|
||||||
|
const result = [array[0]];
|
||||||
|
|
||||||
|
for (let i = 1; i < array.length; i++) {
|
||||||
|
if (array[i] !== array[i - 1]) {
|
||||||
|
result.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResult(data: any) {
|
||||||
|
return data.map(({ e1, e2, e3, e4, e5, e6, e7, count }) => ({
|
||||||
|
items: combineSequentialDuplicates([e1, e2, e3, e4, e5, e6, e7]),
|
||||||
|
count,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user