Merge branch 'dev' of https://github.com/umami-software/umami into feat/um-376-retention-report

This commit is contained in:
Francis Cao 2023-08-07 14:04:05 -07:00
commit fc5a1f458b
23 changed files with 156 additions and 168 deletions

View File

@ -163,6 +163,13 @@ export const labels = defineMessages({
insights: { id: 'label.insights', defaultMessage: 'Insights' }, insights: { id: 'label.insights', defaultMessage: 'Insights' },
retention: { id: 'label.retention', defaultMessage: 'Retention' }, retention: { id: 'label.retention', defaultMessage: 'Retention' },
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' }, dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
referrer: { id: 'label.referrer', defaultMessage: 'Referrer' },
country: { id: 'label.country', defaultMessage: 'Country' },
region: { id: 'label.region', defaultMessage: 'Region' },
city: { id: 'label.city', defaultMessage: 'City' },
browser: { id: 'label.browser', defaultMessage: 'Browser' },
device: { id: 'label.device', defaultMessage: 'Device' },
pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View File

@ -28,6 +28,11 @@ export function EventDataMetricsBar({ websiteId }) {
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}> <MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{!error && isFetched && ( {!error && isFetched && (
<> <>
<MetricCard
className={styles.card}
label={formatMessage(labels.events)}
value={data?.events}
/>
<MetricCard <MetricCard
className={styles.card} className={styles.card}
label={formatMessage(labels.fields)} label={formatMessage(labels.fields)}

View File

@ -16,7 +16,7 @@ export function EventDataTable({ data = [] }) {
<GridTable data={data}> <GridTable data={data}>
<GridColumn name="eventName" label={formatMessage(labels.event)}> <GridColumn name="eventName" label={formatMessage(labels.event)}>
{row => ( {row => (
<Link href={resolveUrl({ eventName: row.eventName })} shallow={true}> <Link href={resolveUrl({ event: row.eventName })} shallow={true}>
{row.eventName} {row.eventName}
</Link> </Link>
)} )}

View File

@ -6,14 +6,14 @@ import PageHeader from 'components/layout/PageHeader';
import Empty from 'components/common/Empty'; import Empty from 'components/common/Empty';
import { DATA_TYPES } from 'lib/constants'; import { DATA_TYPES } from 'lib/constants';
export function EventDataValueTable({ data = [], eventName }) { export function EventDataValueTable({ data = [], event }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { resolveUrl } = usePageQuery(); const { resolveUrl } = usePageQuery();
const Title = () => { const Title = () => {
return ( return (
<> <>
<Link href={resolveUrl({ eventName: undefined })}> <Link href={resolveUrl({ event: undefined })}>
<Button> <Button>
<Icon rotate={180}> <Icon rotate={180}>
<Icons.ArrowRight /> <Icons.ArrowRight />
@ -21,7 +21,7 @@ export function EventDataValueTable({ data = [], eventName }) {
<Text>{formatMessage(labels.back)}</Text> <Text>{formatMessage(labels.back)}</Text>
</Button> </Button>
</Link> </Link>
<Text>{eventName}</Text> <Text>{event}</Text>
</> </>
); );
}; };

View File

@ -0,0 +1,13 @@
import { useState } from 'react';
import FieldSelectForm from './FieldSelectForm';
import FieldFilterForm from './FieldFilterForm';
export default function FilterSelectForm({ fields, onSelect }) {
const [field, setField] = useState();
if (!field) {
return <FieldSelectForm fields={fields} onSelect={setField} />;
}
return <FieldFilterForm name={field.name} type={field.type} onSelect={onSelect} />;
}

View File

@ -1,10 +1,12 @@
import FunnelReport from './funnel/FunnelReport'; import FunnelReport from './funnel/FunnelReport';
import EventDataReport from './event-data/EventDataReport'; import EventDataReport from './event-data/EventDataReport';
import InsightsReport from './insights/InsightsReport';
import RetentionReport from './retention/RetentionReport'; import RetentionReport from './retention/RetentionReport';
const reports = { const reports = {
funnel: FunnelReport, funnel: FunnelReport,
'event-data': EventDataReport, 'event-data': EventDataReport,
insights: InsightsReport,
retention: RetentionReport, retention: RetentionReport,
}; };

View File

@ -7,40 +7,38 @@ import Icons from 'components/icons';
import BaseParameters from '../BaseParameters'; import BaseParameters from '../BaseParameters';
import ParameterList from '../ParameterList'; import ParameterList from '../ParameterList';
import styles from './InsightsParameters.module.css'; import styles from './InsightsParameters.module.css';
import FieldSelectForm from '../FieldSelectForm';
import PopupForm from '../PopupForm'; import PopupForm from '../PopupForm';
import FieldFilterForm from '../FieldFilterForm'; import FilterSelectForm from '../FilterSelectForm';
import FieldSelectForm from '../FieldSelectForm';
const fieldOptions = [
{ name: 'url', type: 'string' },
{ name: 'title', type: 'string' },
{ name: 'referrer', type: 'string' },
{ name: 'query', type: 'string' },
{ name: 'browser', type: 'string' },
{ name: 'os', type: 'string' },
{ name: 'device', type: 'string' },
{ name: 'country', type: 'string' },
{ name: 'region', type: 'string' },
{ name: 'city', type: 'string' },
{ name: 'language', type: 'string' },
];
export function InsightsParameters() { export function InsightsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const ref = useRef(null); const ref = useRef(null);
const { parameters } = report || {}; const { parameters } = report || {};
const { websiteId, dateRange, fields, filters, groups } = parameters || {}; const { websiteId, dateRange, filters, groups } = parameters || {};
const queryEnabled = websiteId && dateRange && fields?.length; const queryEnabled = websiteId && dateRange && (filters?.length || groups?.length);
const fieldOptions = [
{ name: 'url_path', type: 'string', label: formatMessage(labels.url) },
{ name: 'page_title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer_domain', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'url_query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
{ name: 'language', type: 'string', label: formatMessage(labels.language) },
];
const parameterGroups = [ const parameterGroups = [
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
]; ];
const parameterData = { const parameterData = {
fields,
filters, filters,
groups, groups,
}; };
@ -73,11 +71,11 @@ export function InsightsParameters() {
{(close, element) => { {(close, element) => {
return ( return (
<PopupForm element={element} onClose={close}> <PopupForm element={element} onClose={close}>
{group === REPORT_PARAMETERS.fields && ( {group === REPORT_PARAMETERS.groups && (
<FieldSelectForm fields={fieldOptions} onSelect={handleAdd.bind(null, group)} /> <FieldSelectForm fields={fieldOptions} onSelect={handleAdd.bind(null, group)} />
)} )}
{group === REPORT_PARAMETERS.filters && ( {group === REPORT_PARAMETERS.filters && (
<FieldFilterForm fields={fieldOptions} onSelect={handleAdd.bind(null, group)} /> <FilterSelectForm fields={fieldOptions} onSelect={handleAdd.bind(null, group)} />
)} )}
</PopupForm> </PopupForm>
); );
@ -97,27 +95,21 @@ export function InsightsParameters() {
items={parameterData[group]} items={parameterData[group]}
onRemove={index => handleRemove(group, index)} onRemove={index => handleRemove(group, index)}
> >
{({ name, value }) => { {({ value, label }) => {
return ( return (
<div className={styles.parameter}> <div className={styles.parameter}>
{group === REPORT_PARAMETERS.fields && ( {group === REPORT_PARAMETERS.groups && (
<> <>
<div>{name}</div> <div>{label}</div>
<div className={styles.op}>{value}</div>
</> </>
)} )}
{group === REPORT_PARAMETERS.filters && ( {group === REPORT_PARAMETERS.filters && (
<> <>
<div>{name}</div> <div>{label}</div>
<div className={styles.op}>{value[0]}</div> <div className={styles.op}>{value[0]}</div>
<div>{value[1]}</div> <div>{value[1]}</div>
</> </>
)} )}
{group === REPORT_PARAMETERS.groups && (
<>
<div>{name}</div>
</>
)}
</div> </div>
); );
}} }}

View File

@ -6,14 +6,15 @@ import { ReportContext } from '../Report';
export function InsightsTable() { export function InsightsTable() {
const { report } = useContext(ReportContext); const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { fields = [] } = report?.parameters || {}; const { groups = [] } = report?.parameters || {};
return ( return (
<GridTable data={report?.data || []}> <GridTable data={report?.data || []}>
{fields.map(({ name }) => { {groups.map(({ name, label }) => {
return <GridColumn key={name} name={name} label={name} />; return <GridColumn key={name} name={name} label={label} />;
})} })}
<GridColumn name="total" label={formatMessage(labels.total)} /> <GridColumn name="views" label={formatMessage(labels.views)} width="100px" />
<GridColumn name="visitors" label={formatMessage(labels.visitors)} width="100px" />
</GridTable> </GridTable>
); );
} }

View File

@ -26,15 +26,15 @@ function useData(websiteId, eventName) {
export default function WebsiteEventData({ websiteId }) { export default function WebsiteEventData({ websiteId }) {
const { const {
query: { eventName }, query: { event },
} = usePageQuery(); } = usePageQuery();
const { data } = useData(websiteId, eventName); const { data } = useData(websiteId, event);
return ( return (
<Flexbox className={styles.container} direction="column" gap={20}> <Flexbox className={styles.container} direction="column" gap={20}>
<EventDataMetricsBar websiteId={websiteId} /> <EventDataMetricsBar websiteId={websiteId} />
{!eventName && <EventDataTable data={data} />} {!event && <EventDataTable data={data} />}
{eventName && <EventDataValueTable eventName={eventName} data={data} />} {event && <EventDataValueTable event={event} data={data} />}
</Flexbox> </Flexbox>
); );
} }

View File

@ -2,8 +2,8 @@ import { ClickHouse } from 'clickhouse';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import debug from 'debug'; import debug from 'debug';
import { CLICKHOUSE } from 'lib/db'; import { CLICKHOUSE } from 'lib/db';
import { QueryFilters } from './types'; import { QueryFilters, QueryOptions } from './types';
import { FILTER_COLUMNS, IGNORED_FILTERS } from './constants'; import { FILTER_COLUMNS } from './constants';
import { loadWebsite } from './load'; import { loadWebsite } from './load';
import { maxDate } from './date'; import { maxDate } from './date';
@ -63,12 +63,12 @@ function getDateFormat(date) {
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
} }
function getFilterQuery(filters = {}) { function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
const query = Object.keys(filters).reduce((arr, key) => { const query = Object.keys(filters).reduce((arr, key) => {
const filter = filters[key]; const filter = filters[key];
const column = FILTER_COLUMNS[key] ?? options?.columns?.[key];
if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { if (filter !== undefined && column) {
const column = FILTER_COLUMNS[key] || key;
arr.push(`and ${column} = {${key}:String}`); arr.push(`and ${column} = {${key}:String}`);
} }
@ -85,11 +85,12 @@ function getFilterQuery(filters = {}) {
async function parseFilters( async function parseFilters(
websiteId: string, websiteId: string,
filters: QueryFilters & { [key: string]: any } = {}, filters: QueryFilters & { [key: string]: any } = {},
options?: QueryOptions,
) { ) {
const website = await loadWebsite(websiteId); const website = await loadWebsite(websiteId);
return { return {
filterQuery: getFilterQuery(filters), filterQuery: getFilterQuery(filters, options),
params: { params: {
...filters, ...filters,
websiteId, websiteId,

View File

@ -48,13 +48,16 @@ export const FILTER_COLUMNS = {
referrer: 'referrer_domain', referrer: 'referrer_domain',
title: 'page_title', title: 'page_title',
query: 'url_query', query: 'url_query',
os: 'os',
browser: 'browser',
device: 'device',
country: 'country',
region: 'subdivision1', region: 'subdivision1',
eventType: 'event_type', city: 'city',
eventName: 'event_name', language: 'language',
event: 'event_name',
}; };
export const IGNORED_FILTERS = ['startDate', 'endDate', 'timezone', 'unit'];
export const COLLECTION_TYPE = { export const COLLECTION_TYPE = {
event: 'event', event: 'event',
identify: 'identify', identify: 'identify',

View File

@ -3,6 +3,7 @@ import { getClientIp } from 'request-ip';
import { browserName, detectOS } from 'detect-browser'; import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip'; import isLocalhost from 'is-localhost-ip';
import maxmind from 'maxmind'; import maxmind from 'maxmind';
import { safeDecodeURIComponent } from 'next-basics';
import { import {
DESKTOP_OS, DESKTOP_OS,
@ -65,20 +66,18 @@ export async function getLocation(ip, req) {
// Cloudflare headers // Cloudflare headers
if (req.headers['cf-ipcountry']) { if (req.headers['cf-ipcountry']) {
return { return {
country: req.headers['cf-ipcountry'], country: safeDecodeURIComponent(req.headers['cf-ipcountry']),
subdivision1: safeDecodeURIComponent(req.headers['cf-region-code']),
city: safeDecodeURIComponent(req.headers['cf-ipcity']),
}; };
} }
// Vercel headers // Vercel headers
if (req.headers['x-vercel-ip-country']) { if (req.headers['x-vercel-ip-country']) {
const country = req.headers['x-vercel-ip-country'];
const region = req.headers['x-vercel-ip-country-region'];
const city = req.headers['x-vercel-ip-city'];
return { return {
country, country: safeDecodeURIComponent(req.headers['x-vercel-ip-country']),
subdivision1: region, subdivision1: safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']),
city: city ? decodeURIComponent(city) : undefined, city: safeDecodeURIComponent(req.headers['x-vercel-ip-city']),
}; };
} }

View File

@ -1,7 +1,7 @@
import prisma from '@umami/prisma-client'; import prisma from '@umami/prisma-client';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { FILTER_COLUMNS, IGNORED_FILTERS, SESSION_COLUMNS } from './constants'; import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants';
import { loadWebsite } from './load'; import { loadWebsite } from './load';
import { maxDate } from './date'; import { maxDate } from './date';
import { QueryFilters, QueryOptions } from './types'; import { QueryFilters, QueryOptions } from './types';
@ -67,12 +67,12 @@ function getTimestampIntervalQuery(field: string): string {
} }
} }
function getFilterQuery(filters = {}): string { function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
const query = Object.keys(filters).reduce((arr, key) => { const query = Object.keys(filters).reduce((arr, key) => {
const filter = filters[key]; const filter = filters[key];
const column = FILTER_COLUMNS[key] ?? options?.columns?.[key];
if (filter !== undefined && !IGNORED_FILTERS.includes(key)) { if (filter !== undefined && column) {
const column = FILTER_COLUMNS[key] || key;
arr.push(`and ${column}={{${key}}}`); arr.push(`and ${column}={{${key}}}`);
if (key === 'referrer') { if (key === 'referrer') {
@ -100,7 +100,7 @@ async function parseFilters(
options?.joinSession || Object.keys(filters).find(key => SESSION_COLUMNS.includes(key)) options?.joinSession || Object.keys(filters).find(key => SESSION_COLUMNS.includes(key))
? `inner join session on website_event.session_id = session.session_id` ? `inner join session on website_event.session_id = session.session_id`
: '', : '',
filterQuery: getFilterQuery(filters), filterQuery: getFilterQuery(filters, options),
params: { params: {
...filters, ...filters,
websiteId, websiteId,

View File

@ -80,15 +80,15 @@ export interface WebsiteEventMetric {
} }
export interface WebsiteEventDataStats { export interface WebsiteEventDataStats {
field: string; fieldName: string;
type: number; dataType: number;
total: number; total: number;
} }
export interface WebsiteEventDataFields { export interface WebsiteEventDataFields {
field: string; fieldName: string;
type: number; dataType: number;
value?: string; fieldValue?: string;
total: number; total: number;
} }
@ -135,9 +135,7 @@ export interface QueryFilters {
endDate?: Date; endDate?: Date;
timezone?: string; timezone?: string;
unit?: string; unit?: string;
domain?: string;
eventType?: number; eventType?: number;
eventName?: string;
url?: string; url?: string;
referrer?: string; referrer?: string;
title?: string; title?: string;
@ -149,9 +147,10 @@ export interface QueryFilters {
region?: string; region?: string;
city?: string; city?: string;
language?: string; language?: string;
event?: string;
} }
export interface QueryOptions { export interface QueryOptions {
joinSession?: boolean; joinSession?: boolean;
ignoreFilters?: string[]; columns?: { [key: string]: string };
} }

View File

@ -90,7 +90,7 @@
"maxmind": "^4.3.6", "maxmind": "^4.3.6",
"moment-timezone": "^0.5.35", "moment-timezone": "^0.5.35",
"next": "13.3.1", "next": "13.3.1",
"next-basics": "^0.35.0", "next-basics": "^0.36.0",
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"react": "^18.2.0", "react": "^18.2.0",

View File

@ -21,7 +21,7 @@ export default async (
await useAuth(req, res); await useAuth(req, res);
if (req.method === 'GET') { if (req.method === 'GET') {
const { websiteId, startAt, endAt, eventName } = req.query; const { websiteId, startAt, endAt, event } = req.query;
if (!(await canViewWebsite(req.auth, websiteId))) { if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res); return unauthorized(res);
@ -33,7 +33,7 @@ export default async (
const data = await getEventDataEvents(websiteId, { const data = await getEventDataEvents(websiteId, {
startDate, startDate,
endDate, endDate,
eventName, event,
}); });
return ok(res, data); return ok(res, data);

View File

@ -11,6 +11,7 @@ export interface EventDataFieldsRequestBody {
startDate: string; startDate: string;
endDate: string; endDate: string;
}; };
field?: string;
} }
export default async ( export default async (
@ -27,7 +28,10 @@ export default async (
return unauthorized(res); return unauthorized(res);
} }
const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt), field); const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const data = await getEventDataFields(websiteId, { startDate, endDate, field });
return ok(res, data); return ok(res, data);
} }

View File

@ -32,16 +32,18 @@ export default async (
const endDate = new Date(+endAt); const endDate = new Date(+endAt);
const results = await getEventDataFields(websiteId, { startDate, endDate }); const results = await getEventDataFields(websiteId, { startDate, endDate });
const events = new Set();
const data = results.reduce( const data = results.reduce(
(obj, row) => { (obj, row) => {
events.add(row.fieldName);
obj.records += Number(row.total); obj.records += Number(row.total);
return obj; return obj;
}, },
{ fields: results.length, records: 0 }, { fields: results.length, records: 0 },
); );
return ok(res, data); return ok(res, { ...data, events: events.size });
} }
return methodNotAllowed(res); return methodNotAllowed(res);

View File

@ -13,7 +13,7 @@ export interface InsightsRequestBody {
}; };
fields: { name: string; type: string; value: string }[]; fields: { name: string; type: string; value: string }[];
filters: string[]; filters: string[];
groups: string[]; groups: { name: string; type: string }[];
} }
export default async ( export default async (
@ -27,21 +27,18 @@ export default async (
const { const {
websiteId, websiteId,
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
fields,
filters,
groups, groups,
filters,
} = req.body; } = req.body;
if (!(await canViewWebsite(req.auth, websiteId))) { if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
const data = await getInsights(websiteId, { const data = await getInsights(websiteId, groups, {
...filters,
startDate: new Date(startDate), startDate: new Date(startDate),
endDate: new Date(endDate), endDate: new Date(endDate),
fields,
filters,
groups,
}); });
return ok(res, data); return ok(res, data);

View File

@ -14,10 +14,10 @@ export async function getEventDataEvents(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { eventName } = filters; const { event } = filters;
const { params } = await parseFilters(websiteId, filters); const { params } = await parseFilters(websiteId, filters);
if (eventName) { if (event) {
return rawQuery( return rawQuery(
` `
select select
@ -31,7 +31,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
on website_event.event_id = event_data.website_event_id on website_event.event_id = event_data.website_event_id
where event_data.website_id = {{websiteId::uuid}} where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}} and event_data.created_at between {{startDate}} and {{endDate}}
and websit_event.event_name = {{eventName}} and website_event.event_name = {{event}}
group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value group by website_event.event_name, event_data.event_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 4 desc order by 1 asc, 2 asc, 3 asc, 4 desc
`, `,
@ -61,10 +61,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { eventName } = filters; const { event } = filters;
const { params } = await parseFilters(websiteId, filters); const { params } = await parseFilters(websiteId, filters);
if (eventName) { if (event) {
return rawQuery( return rawQuery(
` `
select select
@ -76,7 +76,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
from event_data from event_data
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime} and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_name = {eventName:String} and event_name = {event:String}
group by event_key, data_type, string_value, event_name group by event_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 4 desc order by 1 asc, 2 asc, 3 asc, 4 desc
limit 100 limit 100

View File

@ -14,39 +14,23 @@ export async function getEventDataFields(
async function relationalQuery(websiteId: string, filters: QueryFilters & { field?: string }) { async function relationalQuery(websiteId: string, filters: QueryFilters & { field?: string }) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { field } = filters; const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { params } = await parseFilters(websiteId, filters); columns: { field: 'event_key' },
});
if (field) {
return rawQuery(
`
select
event_key as field,
string_value as value,
count(*) as total
from event_data
where website_id = {{websiteId::uuid}}
and event_key = {{field}}
and created_at between {{startDate}} and {{endDate}}
group by event_key, string_value
order by 3 desc, 2 desc, 1 asc
limit 100
`,
params,
);
}
return rawQuery( return rawQuery(
` `
select select
event_key as field, event_key as fieldName,
data_type as type, data_type as dataType,
string_value as fieldValue,
count(*) as total count(*) as total
from event_data from event_data
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
group by event_key, data_type ${filterQuery}
order by 3 desc, 2 asc, 1 asc group by event_key, data_type, string_value
order by 3 desc, 2 desc, 1 asc
limit 100 limit 100
`, `,
params, params,
@ -55,39 +39,23 @@ async function relationalQuery(websiteId: string, filters: QueryFilters & { fiel
async function clickhouseQuery(websiteId: string, filters: QueryFilters & { field?: string }) { async function clickhouseQuery(websiteId: string, filters: QueryFilters & { field?: string }) {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { field } = filters; const { filterQuery, params } = await parseFilters(websiteId, filters, {
const { params } = await parseFilters(websiteId, filters); columns: { field: 'event_key' },
});
if (field) {
return rawQuery(
`
select
event_key as field,
string_value as value,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and event_key = {field:String}
and created_at between {startDate:DateTime} and {endDate:DateTime}
group by event_key, string_value
order by 3 desc, 2 desc, 1 asc
limit 100
`,
params,
);
}
return rawQuery( return rawQuery(
` `
select select
event_key as field, event_key as fieldName,
data_type as type, data_type as dataType,
string_value as fieldValue,
count(*) as total count(*) as total
from event_data from event_data
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime} and created_at between {startDate:DateTime} and {endDate:DateTime}
group by event_key, data_type ${filterQuery}
order by 3 desc, 2 asc, 1 asc group by event_key, data_type, string_value
order by 3 desc, 2 desc, 1 asc
limit 100 limit 100
`, `,
params, params,

View File

@ -4,7 +4,9 @@ import clickhouse from 'lib/clickhouse';
import { EVENT_TYPE } from 'lib/constants'; import { EVENT_TYPE } from 'lib/constants';
import { QueryFilters } from 'lib/types'; import { QueryFilters } from 'lib/types';
export async function getInsights(...args: [websiteId: string, filters: QueryFilters]) { export async function getInsights(
...args: [websiteId: string, groups: { name: string; type: string }[], filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -13,6 +15,7 @@ export async function getInsights(...args: [websiteId: string, filters: QueryFil
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
groups: { name: string; type: string }[],
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<
{ {
@ -45,6 +48,7 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
groups: { name: string; type: string }[],
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<
{ {
@ -53,7 +57,6 @@ async function clickhouseQuery(
}[] }[]
> { > {
const { parseFilters, rawQuery } = clickhouse; const { parseFilters, rawQuery } = clickhouse;
const { fields } = filters;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -62,14 +65,14 @@ async function clickhouseQuery(
return rawQuery( return rawQuery(
` `
select select
${parseFields(fields)} ${parseFields(groups)}
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime} and {endDate:DateTime} and created_at between {startDate:DateTime} and {endDate:DateTime}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
${filterQuery} ${filterQuery}
group by ${fields.map(({ name }) => name).join(',')} group by ${groups.map(({ name }) => name).join(',')}
order by total desc order by 1 desc
limit 500 limit 500
`, `,
params, params,
@ -77,22 +80,14 @@ async function clickhouseQuery(
} }
function parseFields(fields) { function parseFields(fields) {
let count = false; const query = fields.reduce(
let distinct = false; (arr, field) => {
const { name } = field;
const query = fields.reduce((arr, field) => { return arr.concat(name);
const { name, value } = field; },
['count(*) as views', 'count(distinct session_id) as visitors'],
if (!count && value === 'total') { );
count = true;
arr = arr.concat(`count(*) as views`);
} else if (!distinct && value === 'unique') {
distinct = true;
//arr = arr.concat(`count(distinct ${name})`);
}
return arr.concat(name);
}, []);
return query.join(',\n'); return query.join(',\n');
} }

View File

@ -6371,10 +6371,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next-basics@^0.35.0: next-basics@^0.36.0:
version "0.35.0" version "0.36.0"
resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.35.0.tgz#aa68fd35a0e3fbabfdaf570cd092b6a7cf8df6f5" resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.36.0.tgz#b1675c3f2b98df2fec8df605095dab7d17f9dc7b"
integrity sha512-yqXZMLe109hSJ8sebI/f2m1XNnVuQowpELOhZSGOFOmLfvUyFBAEi0ULdqX1eb8xbttLgjcrumrZfMgmEwuCPw== integrity sha512-Nwou8pCjFuoD/ZxUw9iKC7hhZeWbo/ng0ze74yck3W89MNc/CepwCDziflAHY5XcmIVNmpXOCu9OfmzTdVRPWQ==
dependencies: dependencies:
bcryptjs "^2.4.3" bcryptjs "^2.4.3"
jsonwebtoken "^9.0.0" jsonwebtoken "^9.0.0"