mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-01 20:39:44 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into feat/um-376-retention-report
This commit is contained in:
commit
fc5a1f458b
@ -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({
|
||||||
|
@ -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)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
13
components/pages/reports/FilterSelectForm.js
Normal file
13
components/pages/reports/FilterSelectForm.js
Normal 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} />;
|
||||||
|
}
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
@ -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']),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
15
lib/types.ts
15
lib/types.ts
@ -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 };
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user