diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js
index 7b4d8ece..d2971fa3 100644
--- a/components/pages/reports/funnel/FunnelReport.js
+++ b/components/pages/reports/funnel/FunnelReport.js
@@ -6,9 +6,10 @@ import ReportHeader from '../ReportHeader';
import ReportMenu from '../ReportMenu';
import ReportBody from '../ReportBody';
import Funnel from 'assets/funnel.svg';
+import { REPORT_TYPES } from 'lib/constants';
const defaultParameters = {
- type: 'funnel',
+ type: REPORT_TYPES.funnel,
parameters: { window: 60, urls: [] },
};
diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js
index 18eeffc3..6de4b838 100644
--- a/components/pages/reports/insights/InsightsParameters.js
+++ b/components/pages/reports/insights/InsightsParameters.js
@@ -119,7 +119,7 @@ export function InsightsParameters() {
{id === 'fields' && (
<>
-
{label}
+
{fieldOptions.find(f => f.name === name)?.label}
>
)}
{id === 'filters' && (
diff --git a/components/pages/reports/insights/InsightsReport.js b/components/pages/reports/insights/InsightsReport.js
index 88f12304..3d855d9e 100644
--- a/components/pages/reports/insights/InsightsReport.js
+++ b/components/pages/reports/insights/InsightsReport.js
@@ -5,10 +5,11 @@ import ReportBody from '../ReportBody';
import InsightsParameters from './InsightsParameters';
import InsightsTable from './InsightsTable';
import Lightbulb from 'assets/lightbulb.svg';
+import { REPORT_TYPES } from 'lib/constants';
const defaultParameters = {
- type: 'insights',
- parameters: { fields: [], filters: [], groups: [] },
+ type: REPORT_TYPES.insights,
+ parameters: { fields: [], filters: [] },
};
export default function InsightsReport({ reportId }) {
diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js
index f4549001..d5422c9e 100644
--- a/components/pages/reports/insights/InsightsTable.js
+++ b/components/pages/reports/insights/InsightsTable.js
@@ -31,10 +31,15 @@ export function InsightsTable() {
);
})}
-
+
{row => row.visitors.toLocaleString()}
-
+
{row => row.views.toLocaleString()}
diff --git a/hooks/useFilters.js b/hooks/useFilters.js
index 5143fe5b..089f2ee8 100644
--- a/hooks/useFilters.js
+++ b/hooks/useFilters.js
@@ -1,32 +1,40 @@
import { useMessages } from 'hooks';
+import { OPERATORS } from 'lib/constants';
export function useFilters() {
const { formatMessage, labels } = useMessages();
const filterLabels = {
- eq: formatMessage(labels.is),
- neq: formatMessage(labels.isNot),
- s: formatMessage(labels.isSet),
- ns: formatMessage(labels.isNotSet),
- c: formatMessage(labels.contains),
- dnc: formatMessage(labels.doesNotContain),
- t: formatMessage(labels.true),
- f: formatMessage(labels.false),
- gt: formatMessage(labels.greaterThan),
- lt: formatMessage(labels.lessThan),
- gte: formatMessage(labels.greaterThanEquals),
- lte: formatMessage(labels.lessThanEquals),
- be: formatMessage(labels.before),
- af: formatMessage(labels.after),
+ [OPERATORS.equals]: formatMessage(labels.is),
+ [OPERATORS.notEquals]: formatMessage(labels.isNot),
+ [OPERATORS.set]: formatMessage(labels.isSet),
+ [OPERATORS.notSet]: formatMessage(labels.isNotSet),
+ [OPERATORS.contains]: formatMessage(labels.contains),
+ [OPERATORS.doesNotContain]: formatMessage(labels.doesNotContain),
+ [OPERATORS.true]: formatMessage(labels.true),
+ [OPERATORS.false]: formatMessage(labels.false),
+ [OPERATORS.greaterThan]: formatMessage(labels.greaterThan),
+ [OPERATORS.lessThan]: formatMessage(labels.lessThan),
+ [OPERATORS.greaterThanEquals]: formatMessage(labels.greaterThanEquals),
+ [OPERATORS.lessThanEquals]: formatMessage(labels.lessThanEquals),
+ [OPERATORS.before]: formatMessage(labels.before),
+ [OPERATORS.after]: formatMessage(labels.after),
};
const typeFilters = {
- string: ['eq', 'neq'],
- array: ['c', 'dnc'],
- boolean: ['t', 'f'],
- number: ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'],
- date: ['be', 'af'],
- uuid: ['eq'],
+ string: [OPERATORS.equals, OPERATORS.notEquals],
+ array: [OPERATORS.contains, OPERATORS.doesNotContain],
+ boolean: [OPERATORS.true, OPERATORS.false],
+ number: [
+ OPERATORS.equals,
+ OPERATORS.notEquals,
+ OPERATORS.greaterThan,
+ OPERATORS.lessThan,
+ OPERATORS.greaterThanEquals,
+ OPERATORS.lessThanEquals,
+ ],
+ date: [OPERATORS.before, OPERATORS.after],
+ uuid: [OPERATORS.equals],
};
const getFilters = type => {
diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts
index f7abd94f..75786850 100644
--- a/lib/clickhouse.ts
+++ b/lib/clickhouse.ts
@@ -3,7 +3,7 @@ import dateFormat from 'dateformat';
import debug from 'debug';
import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types';
-import { FILTER_COLUMNS } from './constants';
+import { FILTER_COLUMNS, OPERATORS } from './constants';
import { loadWebsite } from './load';
import { maxDate } from './date';
@@ -63,17 +63,29 @@ function getDateFormat(date) {
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
}
+function mapFilter(column, operator, name) {
+ switch (operator) {
+ case OPERATORS.equals:
+ return `${column} = {${name}:String}`;
+ case OPERATORS.notEquals:
+ return `${column} != {${name}:String}`;
+ default:
+ return '';
+ }
+}
+
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
- const query = Object.keys(filters).reduce((arr, key) => {
- const filter = filters[key];
- const column = FILTER_COLUMNS[key] ?? options?.columns?.[key];
+ const query = Object.keys(filters).reduce((arr, name) => {
+ const value = filters[name];
+ const operator = value?.filter ?? OPERATORS.equals;
+ const column = FILTER_COLUMNS[name] ?? options?.columns?.[name];
- if (filter !== undefined && column) {
- arr.push(`and ${column} = {${key}:String}`);
- }
+ if (value !== undefined && column) {
+ arr.push(`and ${mapFilter(column, operator, name)}`);
- if (key === 'referrer') {
- arr.push('and referrer_domain != {websiteDomain:String}');
+ if (name === 'referrer') {
+ arr.push('and referrer_domain != {websiteDomain:String}');
+ }
}
return arr;
@@ -82,11 +94,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n');
}
-async function parseFilters(
- websiteId: string,
- filters: QueryFilters & { [key: string]: any } = {},
- options?: QueryOptions,
-) {
+async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await loadWebsite(websiteId);
return {
diff --git a/lib/constants.ts b/lib/constants.ts
index 887f90a9..8972f81f 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -76,6 +76,23 @@ export const DATA_TYPE = {
array: 5,
} as const;
+export const OPERATORS = {
+ equals: 'eq',
+ notEquals: 'neq',
+ set: 's',
+ notSet: 'ns',
+ contains: 'c',
+ doesNotContain: 'dnc',
+ true: 't',
+ false: 'f',
+ greaterThan: 'gt',
+ lessThan: 'lt',
+ greaterThanEquals: 'gte',
+ lessThanEquals: 'lte',
+ before: 'bf',
+ after: 'af',
+} as const;
+
export const DATA_TYPES = {
[DATA_TYPE.string]: 'string',
[DATA_TYPE.number]: 'number',
@@ -84,6 +101,12 @@ export const DATA_TYPES = {
[DATA_TYPE.array]: 'array',
};
+export const REPORT_TYPES = {
+ funnel: 'funnel',
+ insights: 'insights',
+ retention: 'retention',
+} as const;
+
export const REPORT_PARAMETERS = {
fields: 'fields',
filters: 'filters',
diff --git a/lib/prisma.ts b/lib/prisma.ts
index 753f1ae4..a4993286 100644
--- a/lib/prisma.ts
+++ b/lib/prisma.ts
@@ -1,7 +1,7 @@
import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
-import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants';
+import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS } from './constants';
import { loadWebsite } from './load';
import { maxDate } from './date';
import { QueryFilters, QueryOptions } from './types';
@@ -67,15 +67,27 @@ function getTimestampIntervalQuery(field: string): string {
}
}
+function mapFilter(column, operator, name) {
+ switch (operator) {
+ case OPERATORS.equals:
+ return `${column} = {{${name}}}`;
+ case OPERATORS.notEquals:
+ return `${column} != {{${name}}}`;
+ default:
+ return '';
+ }
+}
+
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
- const query = Object.keys(filters).reduce((arr, key) => {
- const filter = filters[key];
- const column = FILTER_COLUMNS[key] ?? options?.columns?.[key];
+ const query = Object.keys(filters).reduce((arr, name) => {
+ const value = filters[name];
+ const operator = value?.filter ?? OPERATORS.equals;
+ const column = FILTER_COLUMNS[name] ?? options?.columns?.[name];
- if (filter !== undefined && column) {
- arr.push(`and ${column}={{${key}}}`);
+ if (value !== undefined && column) {
+ arr.push(`and ${mapFilter(column, operator, name)}`);
- if (key === 'referrer') {
+ if (name === 'referrer') {
arr.push(
'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)',
);
@@ -88,11 +100,17 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
return query.join('\n');
}
-async function parseFilters(
- websiteId,
- filters: QueryFilters & { [key: string]: any } = {},
- options: QueryOptions = {},
-) {
+function normalizeFilters(filters = {}) {
+ return Object.keys(filters).reduce((obj, key) => {
+ const value = filters[key];
+
+ obj[key] = value?.value ?? value;
+
+ return obj;
+ }, {});
+}
+
+async function parseFilters(websiteId, filters: QueryFilters = {}, options: QueryOptions = {}) {
const website = await loadWebsite(websiteId);
return {
@@ -102,7 +120,7 @@ async function parseFilters(
: '',
filterQuery: getFilterQuery(filters, options),
params: {
- ...filters,
+ ...normalizeFilters(filters),
websiteId,
startDate: maxDate(filters.startDate, website.resetAt),
websiteDomain: website.domain,
diff --git a/package.json b/package.json
index 647cdf41..89dc5e97 100644
--- a/package.json
+++ b/package.json
@@ -94,7 +94,7 @@
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
- "react-basics": "^0.91.0",
+ "react-basics": "^0.92.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",
diff --git a/pages/api/reports/insights.ts b/pages/api/reports/insights.ts
index decb1f81..09a07d2f 100644
--- a/pages/api/reports/insights.ts
+++ b/pages/api/reports/insights.ts
@@ -16,6 +16,14 @@ export interface InsightsRequestBody {
groups: { name: string; type: string }[];
}
+function convertFilters(filters) {
+ return filters.reduce((obj, { name, ...value }) => {
+ obj[name] = value;
+
+ return obj;
+ }, {});
+}
+
export default async (
req: NextApiRequestQueryBody,
res: NextApiResponse,
@@ -36,7 +44,7 @@ export default async (
}
const data = await getInsights(websiteId, fields, {
- ...filters,
+ ...convertFilters(filters),
startDate: new Date(startDate),
endDate: new Date(endDate),
});
diff --git a/queries/analytics/reports/getInsights.ts b/queries/analytics/reports/getInsights.ts
index 9793f258..fa54488b 100644
--- a/queries/analytics/reports/getInsights.ts
+++ b/queries/analytics/reports/getInsights.ts
@@ -1,7 +1,7 @@
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse';
-import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants';
+import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types';
export async function getInsights(
@@ -91,7 +91,7 @@ function parseFields(fields) {
(arr, field) => {
const { name } = field;
- return arr.concat(name);
+ return arr.concat(`${FILTER_COLUMNS[name]} as "${name}"`);
},
['count(*) as views', 'count(distinct website_event.session_id) as visitors'],
);
@@ -103,5 +103,5 @@ function parseGroupBy(fields) {
if (!fields.length) {
return '';
}
- return `group by ${fields.map(({ name }) => name).join(',')}`;
+ return `group by ${fields.map(({ name }) => FILTER_COLUMNS[name]).join(',')}`;
}
diff --git a/yarn.lock b/yarn.lock
index d9224c2a..115e3cc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7557,10 +7557,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-basics@^0.91.0:
- version "0.91.0"
- resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0"
- integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw==
+react-basics@^0.92.0:
+ version "0.92.0"
+ resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.92.0.tgz#02bc6e88bdaf189c30cc6cbd8bbb1c9d12cd089b"
+ integrity sha512-BVUWg5a7R88konA9NedYMBx1hl50d6h/MD7qlKOEO/Cnm8cOC7AYTRKAKhO6kHMWjY4ZpUuvlg0UcF+SJP/uXA==
dependencies:
classnames "^2.3.1"
date-fns "^2.29.3"