mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-01 12:29:35 +01:00
UTM report.
This commit is contained in:
parent
e602aedd21
commit
bca9c87021
@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
@ -10,6 +11,7 @@ export default function ReportsPage({ teamId }: { teamId: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports',
|
||||
};
|
||||
|
@ -1,28 +0,0 @@
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
import RetentionReport from '../retention/RetentionReport';
|
||||
import { useApi } from 'components/hooks';
|
||||
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
};
|
||||
|
||||
export default function ReportDetails({ reportId }: { reportId: string }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: report } = useQuery({
|
||||
queryKey: ['reports', reportId],
|
||||
queryFn: () => get(`/reports/${reportId}`),
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReportComponent = reports[report.type];
|
||||
|
||||
return <ReportComponent reportId={reportId} />;
|
||||
}
|
@ -1,6 +1,27 @@
|
||||
'use client';
|
||||
import ReportDetails from './ReportDetails';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
import RetentionReport from '../retention/RetentionReport';
|
||||
import UTMReport from '../utm/UTMReport';
|
||||
import { useReport } from 'components/hooks';
|
||||
|
||||
export default function ReportPage({ reportId }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
utm: UTMReport,
|
||||
};
|
||||
|
||||
export default function ReportPage({ reportId }: { reportId: string }) {
|
||||
const { report } = useReport(reportId);
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReportComponent = reports[report.type];
|
||||
|
||||
return <ReportComponent reportId={reportId} />;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import Tag from 'assets/tag.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
|
||||
@ -30,6 +31,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
||||
url: renderTeamUrl('/reports/retention'),
|
||||
icon: <Magnet />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.utm),
|
||||
description: formatMessage(labels.utmDescription),
|
||||
url: renderTeamUrl('/reports/utm'),
|
||||
icon: <Tag />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -53,11 +53,11 @@ export function InsightsParameters() {
|
||||
filters,
|
||||
};
|
||||
|
||||
const handleSubmit = values => {
|
||||
const handleSubmit = (values: any) => {
|
||||
runReport(values);
|
||||
};
|
||||
|
||||
const handleAdd = (id, value) => {
|
||||
const handleAdd = (id: string | number, value: { name: any }) => {
|
||||
const data = parameterData[id];
|
||||
|
||||
if (!data.find(({ name }) => name === value.name)) {
|
||||
@ -65,7 +65,7 @@ export function InsightsParameters() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (id, index) => {
|
||||
const handleRemove = (id: string, index: number) => {
|
||||
const data = [...parameterData[id]];
|
||||
data.splice(index, 1);
|
||||
updateReport({ parameters: { [id]: data } });
|
||||
|
@ -1,6 +1,11 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Retention Report',
|
||||
};
|
||||
|
36
src/app/(main)/reports/utm/UTMParameters.tsx
Normal file
36
src/app/(main)/reports/utm/UTMParameters.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useContext } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Form, FormButtons, SubmitButton } from 'react-basics';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
|
||||
export function UTMParameters() {
|
||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
|
||||
const handleSubmit = (data: any, e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UTMParameters;
|
28
src/app/(main)/reports/utm/UTMReport.tsx
Normal file
28
src/app/(main)/reports/utm/UTMReport.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import UTMParameters from './UTMParameters';
|
||||
import UTMView from './UTMView';
|
||||
import Tag from 'assets/tag.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
const defaultParameters = {
|
||||
type: REPORT_TYPES.utm,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export default function UTMReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Tag />} />
|
||||
<ReportMenu>
|
||||
<UTMParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<UTMView />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
}
|
5
src/app/(main)/reports/utm/UTMReportPage.tsx
Normal file
5
src/app/(main)/reports/utm/UTMReportPage.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import UTMReport from './UTMReport';
|
||||
|
||||
export default function UTMReportPage() {
|
||||
return <UTMReport />;
|
||||
}
|
24
src/app/(main)/reports/utm/UTMView.module.css
Normal file
24
src/app/(main)/reports/utm/UTMView.module.css
Normal file
@ -0,0 +1,24 @@
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
43
src/app/(main)/reports/utm/UTMView.tsx
Normal file
43
src/app/(main)/reports/utm/UTMView.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useContext } from 'react';
|
||||
import { firstBy } from 'thenby';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './UTMView.module.css';
|
||||
|
||||
function toArray(data: { [key: string]: number }) {
|
||||
return Object.keys(data)
|
||||
.map(key => {
|
||||
return { name: key, value: data[key] };
|
||||
})
|
||||
.sort(firstBy('value', -1));
|
||||
}
|
||||
|
||||
export default function UTMView() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { data } = report || {};
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(data).map(key => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className={styles.title}>{key}</div>
|
||||
<div className={styles.params}>
|
||||
{toArray(data[key]).map(({ name, value }) => {
|
||||
return (
|
||||
<div key={name} className={styles.row}>
|
||||
<div className={styles.label}>{name}</div>
|
||||
<div className={styles.value}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
10
src/app/(main)/reports/utm/page.tsx
Normal file
10
src/app/(main)/reports/utm/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import UTMReportPage from './UTMReportPage';
|
||||
|
||||
export default function () {
|
||||
return <UTMReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'UTM Report',
|
||||
};
|
1
src/assets/bookmark.svg
Normal file
1
src/assets/bookmark.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M3.515 22.875a1 1 0 0 0 1.015-.027L12 18.179l7.47 4.669A1 1 0 0 0 21 22V4a3 3 0 0 0-3-3H6a3 3 0 0 0-3 3v18a1 1 0 0 0 .515.875zM5 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v16.2l-6.47-4.044a1 1 0 0 0-1.06 0L5 20.2z"/></svg>
|
After Width: | Height: | Size: 306 B |
1
src/assets/flag.svg
Normal file
1
src/assets/flag.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" viewBox="0 0 510 510" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m393.159 121.41 69.152-86.44c-16.753-2.022-149.599-37.363-282.234-8.913V0h-30v361.898c-25.85 6.678-45 30.195-45 58.102v1.509c-34.191 6.969-60 37.272-60 73.491v15h240v-15c0-36.22-25.809-66.522-60-73.491V420c0-27.906-19.15-51.424-45-58.102V237.165c153.335-30.989 264.132 7.082 284.847 9.834zM252.506 480H77.647c6.19-17.461 22.873-30 42.43-30h90c19.556 0 36.238 12.539 42.429 30zm-57.429-60h-60c0-16.542 13.458-30 30-30s30 13.458 30 30zm-15-213.427V56.771c66.329-15.269 141.099-15.756 227.537-1.455l-50.619 63.274 48.8 85.4c-75.047-12.702-150.759-11.841-225.718 2.583z"/></svg>
|
After Width: | Height: | Size: 670 B |
1
src/assets/speaker.svg
Normal file
1
src/assets/speaker.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M232.011 88.828c-5.664-5.664-13.217-8.784-21.269-8.784s-15.605 3.12-21.269 8.783c-9.917 9.917-11.446 25.09-4.593 36.632-23.293 86.372-34.167 96.094-78.604 135.776-15.831 14.138-35.533 31.731-61.302 57.5-5.434 5.434-8.426 12.673-8.426 20.383s2.993 14.949 8.426 20.383l70.981 70.98c5.434 5.435 12.672 8.427 20.382 8.427a28.7 28.7 0 0 0 14.046-3.637l72.768 72.768c2.574 2.574 6.09 3.962 9.896 3.961.789 0 1.59-.06 2.398-.181 3.883-.581 7.662-2.543 10.641-5.521l25.329-25.329c6.918-6.919 7.684-16.993 1.741-22.936l-39.164-39.164c11.586-20.762 9.203-46.431-6.187-64.762 29.684-32.251 46.532-43.128 122.192-63.532a30.076 30.076 0 0 0 15.361 4.203c7.703 0 15.405-2.933 21.269-8.796 11.728-11.729 11.728-30.811 0-42.539zM127.268 419.167l-70.981-70.981c-2.412-2.411-3.74-5.632-3.74-9.068s1.328-6.657 3.74-9.068c17.786-17.786 32.665-31.645 45.371-43.163l86.911 86.911c-11.519 12.706-25.378 27.585-43.164 45.371-2.412 2.411-5.632 3.74-9.068 3.74-3.437-.001-6.657-1.33-9.069-3.742zM260.1 469.653l-25.33 25.33a4.096 4.096 0 0 1-1.197.85L162.45 424.71a1243.745 1243.745 0 0 0 26.786-27.968l71.714 71.713a4.047 4.047 0 0 1-.85 1.198zm-38.055-62.731-21.982-21.981a2607.916 2607.916 0 0 0 14.157-15.763l2.712-3.035c8.895 11.831 10.752 27.329 5.113 40.779zm-19.759-48.401-3.004 3.362-85.711-85.711 3.361-3.003c44.419-39.665 57.85-51.661 80.687-133.656l138.322 138.322c-81.993 22.837-93.99 36.268-133.655 80.686zm173.027-83.854c-5.489 5.49-14.422 5.49-19.911 0L200.786 120.052c-5.489-5.489-5.489-14.421 0-19.91 2.642-2.643 6.178-4.098 9.956-4.098s7.313 1.455 9.955 4.098l154.616 154.615c5.489 5.489 5.489 14.421 0 19.91zm-22.558-151.968a8 8 0 0 1 0-11.314l43.904-43.904a8 8 0 0 1 11.313 11.314l-43.904 43.904c-1.562 1.562-3.609 2.343-5.657 2.343s-4.094-.781-5.656-2.343zm122.699 107.695a8 8 0 0 1-8 8h-62.09a8 8 0 0 1 0-16h62.09a8 8 0 0 1 8 8zM237.061 70.09V8a8 8 0 0 1 16 0v62.09a8 8 0 0 1-16 0z"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/tag.svg
Normal file
1
src/assets/tag.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="437pt" viewBox="0 0 437.004 437" width="437pt" xmlns="http://www.w3.org/2000/svg"><path d="M229 14.645A50.173 50.173 0 0 0 192.371.015L52.293 3.586C25.672 4.25 4.246 25.673 3.582 52.298L.016 192.37a50.215 50.215 0 0 0 14.625 36.633l193.367 193.36c19.539 19.495 51.168 19.495 70.707 0l143.644-143.645c19.528-19.524 19.528-51.184 0-70.711zm179.219 249.933-143.645 143.64c-11.722 11.7-30.703 11.7-42.426 0L28.785 214.86a30.131 30.131 0 0 1-8.777-21.98l3.566-140.074c.403-15.973 13.254-28.828 29.227-29.227l140.074-3.57c.254-.004.5-.008.754-.008a30.129 30.129 0 0 1 21.223 8.79l193.367 193.362c11.695 11.723 11.695 30.703 0 42.426zm0 0"/><path d="M130.719 82.574c-26.59 0-48.145 21.555-48.149 48.145 0 26.59 21.559 48.144 48.145 48.144 26.59 0 48.144-21.554 48.144-48.144-.03-26.574-21.566-48.114-48.14-48.145zm0 76.29c-15.547 0-28.145-12.602-28.149-28.145 0-15.543 12.602-28.145 28.145-28.145s28.144 12.602 28.144 28.145c-.015 15.535-12.605 28.125-28.14 28.144zm0 0"/></svg>
|
After Width: | Height: | Size: 984 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M256 0c-74.439 0-135 60.561-135 135s60.561 135 135 135 135-60.561 135-135S330.439 0 256 0zM423.966 358.195C387.006 320.667 338.009 300 286 300h-60c-52.008 0-101.006 20.667-137.966 58.195C51.255 395.539 31 444.833 31 497c0 8.284 6.716 15 15 15h420c8.284 0 15-6.716 15-15 0-52.167-20.255-101.461-57.034-138.805z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M256 0c-74.439 0-135 60.561-135 135s60.561 135 135 135 135-60.561 135-135S330.439 0 256 0zm167.966 358.195C387.006 320.667 338.009 300 286 300h-60c-52.008 0-101.006 20.667-137.966 58.195C51.255 395.539 31 444.833 31 497c0 8.284 6.716 15 15 15h420c8.284 0 15-6.716 15-15 0-52.167-20.255-101.461-57.034-138.805z"/></svg>
|
Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 452 B |
@ -4,7 +4,7 @@ import { useApi } from './useApi';
|
||||
import { useTimezone } from '../useTimezone';
|
||||
import { useMessages } from '../useMessages';
|
||||
|
||||
export function useReport(reportId: string, defaultParameters: { [key: string]: any }) {
|
||||
export function useReport(reportId: string, defaultParameters: { [key: string]: any } = {}) {
|
||||
const [report, setReport] = useState(null);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const { get, post } = useApi();
|
||||
|
@ -222,6 +222,11 @@ export const labels = defineMessages({
|
||||
id: 'message.visitors-dropped-off',
|
||||
defaultMessage: 'Visitors dropped off',
|
||||
},
|
||||
utm: { id: 'label.utm', defaultMessage: 'UTM' },
|
||||
utmDescription: {
|
||||
id: 'label.utm-description',
|
||||
defaultMessage: 'Track your campaigns through UTM parameters.',
|
||||
},
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
@ -106,6 +106,7 @@ export const REPORT_TYPES = {
|
||||
funnel: 'funnel',
|
||||
insights: 'insights',
|
||||
retention: 'retention',
|
||||
utm: 'utm',
|
||||
} as const;
|
||||
|
||||
export const REPORT_PARAMETERS = {
|
||||
@ -227,6 +228,8 @@ export const URL_LENGTH = 500;
|
||||
export const PAGE_TITLE_LENGTH = 500;
|
||||
export const EVENT_NAME_LENGTH = 50;
|
||||
|
||||
export const UTM_PARAMS = ['source', 'medium', 'campaign', 'term', 'content'];
|
||||
|
||||
export const DESKTOP_OS = [
|
||||
'BeOS',
|
||||
'Chrome OS',
|
||||
|
@ -27,7 +27,7 @@ const schema: YupRequest = {
|
||||
websiteId: yup.string().uuid().required(),
|
||||
type: yup
|
||||
.string()
|
||||
.matches(/funnel|insights|retention/i)
|
||||
.matches(/funnel|insights|retention|utm/i)
|
||||
.required(),
|
||||
name: yup.string().max(200).required(),
|
||||
description: yup.string().max(500),
|
||||
|
@ -27,7 +27,7 @@ const schema = {
|
||||
name: yup.string().max(200).required(),
|
||||
type: yup
|
||||
.string()
|
||||
.matches(/funnel|insights|retention/i)
|
||||
.matches(/funnel|insights|retention|utm/i)
|
||||
.required(),
|
||||
description: yup.string().max(500),
|
||||
parameters: yup
|
||||
|
54
src/pages/api/reports/utm.ts
Normal file
54
src/pages/api/reports/utm.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { TimezoneTest } from 'lib/yup';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getUTM } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface UTMRequestBody {
|
||||
websiteId: string;
|
||||
dateRange: { startDate: string; endDate: string; timezone: string };
|
||||
}
|
||||
|
||||
const schema = {
|
||||
POST: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
dateRange: yup
|
||||
.object()
|
||||
.shape({
|
||||
startDate: yup.date().required(),
|
||||
endDate: yup.date().required(),
|
||||
timezone: TimezoneTest,
|
||||
})
|
||||
.required(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default async (req: NextApiRequestQueryBody<any, UTMRequestBody>, res: NextApiResponse) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate, timezone },
|
||||
} = req.body;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getUTM(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
timezone,
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
120
src/queries/analytics/reports/getUTM.ts
Normal file
120
src/queries/analytics/reports/getUTM.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getUTM(
|
||||
...args: [
|
||||
websiteId: string,
|
||||
filters: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
timezone?: string;
|
||||
},
|
||||
]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
filters: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
timezone?: string;
|
||||
},
|
||||
): Promise<
|
||||
{
|
||||
date: string;
|
||||
day: number;
|
||||
visitors: number;
|
||||
returnVisitors: number;
|
||||
percentage: number;
|
||||
}[]
|
||||
> {
|
||||
const { startDate, endDate } = filters;
|
||||
const { rawQuery } = prisma;
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select url_query, count(*) as "num"
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and url_query is not null
|
||||
group by 1
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
).then(results => {
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
filters: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
timezone?: string;
|
||||
},
|
||||
): Promise<
|
||||
{
|
||||
date: string;
|
||||
day: number;
|
||||
visitors: number;
|
||||
returnVisitors: number;
|
||||
percentage: number;
|
||||
}[]
|
||||
> {
|
||||
const { startDate, endDate } = filters;
|
||||
const { rawQuery } = clickhouse;
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select url_query, count(*) as "num"
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and url_query != ''
|
||||
group by 1
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
).then(result => parseParameters(result as any[]));
|
||||
}
|
||||
|
||||
function parseParameters(result: any[]) {
|
||||
return Object.values(result).reduce((data, { url_query, num }) => {
|
||||
const params = url_query.split('&').map(n => decodeURIComponent(n));
|
||||
|
||||
for (const param of params) {
|
||||
const [key, value] = param.split('=');
|
||||
|
||||
const match = key.match(/^utm_(\w+)$/);
|
||||
|
||||
if (match) {
|
||||
const group = match[1];
|
||||
const name = decodeURIComponent(value);
|
||||
|
||||
if (!data[group]) {
|
||||
data[group] = { [name]: +num };
|
||||
} else if (!data[group][name]) {
|
||||
data[group][name] = +num;
|
||||
} else {
|
||||
data[group][name] += +num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}, {});
|
||||
}
|
@ -14,6 +14,7 @@ export * from './analytics/events/saveEvent';
|
||||
export * from './analytics/reports/getFunnel';
|
||||
export * from './analytics/reports/getRetention';
|
||||
export * from './analytics/reports/getInsights';
|
||||
export * from './analytics/reports/getUTM';
|
||||
export * from './analytics/pageviews/getPageviewMetrics';
|
||||
export * from './analytics/pageviews/getPageviewStats';
|
||||
export * from './analytics/sessions/createSession';
|
||||
|
Loading…
Reference in New Issue
Block a user