diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
index 96faeb9c..c1181e72 100644
--- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
+++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
@@ -211,7 +211,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
}
if (!values?.length) {
- return
poop
;
+ return null;
}
return (
diff --git a/src/app/(main)/reports/[reportId]/PopupForm.module.css b/src/app/(main)/reports/[reportId]/PopupForm.module.css
index 94d98b38..5d069dd4 100644
--- a/src/app/(main)/reports/[reportId]/PopupForm.module.css
+++ b/src/app/(main)/reports/[reportId]/PopupForm.module.css
@@ -1,5 +1,4 @@
.form {
- position: absolute;
background: var(--base50);
min-width: 300px;
padding: 20px;
diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
index 640c519b..989f4def 100644
--- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
+++ b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
@@ -35,7 +35,7 @@ export function ShareUrl({
const url = `${hostUrl || process.env.hostUrl || window?.location.origin}${
process.env.basePath
- }/share/${id}/${encodeURIComponent(domain)}`;
+ }/share/${id}/${domain}`;
const handleGenerate = () => {
setId(generateId());
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx
index c7e4cc53..6ee0cb6d 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx
@@ -2,6 +2,7 @@ import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
import PopupForm from 'app/(main)/reports/[reportId]/PopupForm';
import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm';
import { useFields, useMessages, useNavigation } from 'components/hooks';
+import { OPERATORS } from 'lib/constants';
export function WebsiteFilterButton({
websiteId,
@@ -14,8 +15,18 @@ export function WebsiteFilterButton({
const { renderUrl, router } = useNavigation();
const { fields } = useFields();
- const handleAddFilter = ({ name, value }) => {
- router.push(renderUrl({ [name]: value }));
+ const handleAddFilter = ({ name, operator, value }) => {
+ let prefix = '';
+
+ if (operator === OPERATORS.notEquals) {
+ prefix = '!';
+ } else if (operator === OPERATORS.contains) {
+ prefix = '~';
+ } else if (operator === OPERATORS.doesNotContain) {
+ prefix = '!~';
+ }
+
+ router.push(renderUrl({ [name]: prefix + value }));
};
return (
@@ -26,7 +37,7 @@ export function WebsiteFilterButton({
{formatMessage(labels.filter)}
-
+
{(close: () => void) => {
return (
@@ -37,7 +48,6 @@ export function WebsiteFilterButton({
handleAddFilter(value);
close();
}}
- allowFilterSelect={false}
/>
);
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
index c488464d..10f28895 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
@@ -47,24 +47,15 @@ export function WebsiteMetricsBar({
value={views.value}
change={views.change}
/>
-
e.value === value).label;
+ return options.find(e => e.value === value)?.label;
};
return (
diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts
index e41d987b..0eabd3d2 100644
--- a/src/lib/clickhouse.ts
+++ b/src/lib/clickhouse.ts
@@ -62,30 +62,32 @@ function getDateFormat(date: Date) {
}
function mapFilter(column: string, operator: string, name: string, type: string = 'String') {
+ const value = `{${name}:${type}}`;
+
switch (operator) {
case OPERATORS.equals:
- return `${column} = {${name}:${type}}`;
+ return `${column} = ${value}`;
case OPERATORS.notEquals:
- return `${column} != {${name}:${type}}`;
+ return `${column} != ${value}`;
case OPERATORS.contains:
- return `positionCaseInsensitive(${column}, {${name}:${type}}) > 0`;
+ return `positionCaseInsensitive(${column}, ${value}) > 0`;
case OPERATORS.doesNotContain:
- return `positionCaseInsensitive(${column}, {${name}:${type}}) = 0`;
+ return `positionCaseInsensitive(${column}, ${value}) = 0`;
default:
return '';
}
}
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
- const query = Object.keys(filters).reduce((arr, name) => {
- const value = filters[name];
- const filter = value?.filter ?? OPERATORS.equals;
- const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name];
+ const query = Object.keys(filters).reduce((arr, key) => {
+ const filter = filters[key];
+ const operator = filter?.operator ?? OPERATORS.equals;
+ const column = filter?.column ?? FILTER_COLUMNS[key] ?? options?.columns?.[key];
- if (value !== undefined && column !== undefined) {
- arr.push(`and ${mapFilter(column, filter, name)}`);
+ if (filter !== undefined && column !== undefined) {
+ arr.push(`and ${mapFilter(column, operator, key)}`);
- if (name === 'referrer') {
+ if (key === 'referrer') {
arr.push('and referrer_domain != {websiteDomain:String}');
}
}
diff --git a/src/lib/query.ts b/src/lib/query.ts
index b1dd5cbe..82514eb0 100644
--- a/src/lib/query.ts
+++ b/src/lib/query.ts
@@ -1,6 +1,13 @@
import { NextApiRequest } from 'next';
import { getAllowedUnits, getMinimumUnit } from './date';
import { getWebsiteDateRange } from '../queries';
+import { FILTER_COLUMNS, OPERATORS } from 'lib/constants';
+
+const OPERATOR_SYMBOLS = {
+ '!': 'neq',
+ '~': 'c',
+ '!~': 'dnc',
+};
export async function parseDateRangeQuery(req: NextApiRequest) {
const { websiteId, startAt, endAt, unit } = req.query;
@@ -29,3 +36,28 @@ export async function parseDateRangeQuery(req: NextApiRequest) {
unit: (getAllowedUnits(startDate, endDate).includes(unit as string) ? unit : minUnit) as string,
};
}
+
+export function getQueryFilters(req: NextApiRequest) {
+ return Object.keys(FILTER_COLUMNS).reduce((obj, key) => {
+ const value = req.query[key];
+
+ if (value) {
+ obj[key] = value;
+ }
+
+ if (typeof value === 'string') {
+ const [, prefix, paramValue] = value.match(/^(!~|!|~)?(.*)$/);
+
+ if (prefix && paramValue) {
+ obj[key] = {
+ name: key,
+ column: FILTER_COLUMNS[key],
+ operator: OPERATOR_SYMBOLS[prefix] || OPERATORS.equals,
+ value: paramValue,
+ };
+ }
+ }
+
+ return obj;
+ }, {});
+}
diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/metrics.ts
index d78c67c8..2387d9c2 100644
--- a/src/pages/api/websites/[websiteId]/metrics.ts
+++ b/src/pages/api/websites/[websiteId]/metrics.ts
@@ -5,7 +5,7 @@ import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants';
import { getPageviewMetrics, getSessionMetrics } from 'queries';
-import { parseDateRangeQuery } from 'lib/query';
+import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
import * as yup from 'yup';
export interface WebsiteMetricsRequestQuery {
@@ -62,25 +62,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
- const {
- websiteId,
- type,
- url,
- referrer,
- title,
- query,
- os,
- browser,
- device,
- country,
- region,
- city,
- language,
- event,
- limit,
- offset,
- search,
- } = req.query;
+ const { websiteId, type, limit, offset, search } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
@@ -90,24 +72,13 @@ export default async (
const { startDate, endDate } = await parseDateRangeQuery(req);
const column = FILTER_COLUMNS[type] || type;
const filters = {
+ ...getQueryFilters(req),
startDate,
endDate,
- url,
- referrer,
- title,
- query,
- os,
- browser,
- device,
- country,
- region,
- city,
- language,
- event,
};
if (search) {
- filters[column] = {
+ filters[type] = {
column,
operator: OPERATORS.contains,
value: search,
diff --git a/src/pages/api/websites/[websiteId]/pageviews.ts b/src/pages/api/websites/[websiteId]/pageviews.ts
index 23ea335d..9ac4e870 100644
--- a/src/pages/api/websites/[websiteId]/pageviews.ts
+++ b/src/pages/api/websites/[websiteId]/pageviews.ts
@@ -1,6 +1,6 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
-import { parseDateRangeQuery } from 'lib/query';
+import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@@ -52,8 +52,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
- const { websiteId, timezone, url, referrer, title, os, browser, device, country, region, city } =
- req.query;
+ const { websiteId, timezone } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
@@ -63,19 +62,11 @@ export default async (
const { startDate, endDate, unit } = await parseDateRangeQuery(req);
const filters = {
+ ...getQueryFilters(req),
startDate,
endDate,
timezone,
unit,
- url,
- referrer,
- title,
- os,
- browser,
- device,
- country,
- region,
- city,
};
const [pageviews, sessions] = await Promise.all([
diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/stats.ts
index 129bfca6..dfc3df93 100644
--- a/src/pages/api/websites/[websiteId]/stats.ts
+++ b/src/pages/api/websites/[websiteId]/stats.ts
@@ -4,7 +4,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types';
-import { parseDateRangeQuery } from 'lib/query';
+import { getQueryFilters, parseDateRangeQuery } from 'lib/query';
import { getWebsiteStats } from 'queries';
export interface WebsiteStatsRequestQuery {
@@ -52,20 +52,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
- const {
- websiteId,
- url,
- referrer,
- title,
- query,
- event,
- os,
- browser,
- device,
- country,
- region,
- city,
- }: any & { websiteId: string } = req.query;
+ const { websiteId } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
@@ -77,19 +64,7 @@ export default async (
const prevStartDate = subMinutes(startDate, diff);
const prevEndDate = subMinutes(endDate, diff);
- const filters = {
- url,
- referrer,
- title,
- query,
- event,
- os,
- browser,
- device,
- country,
- region,
- city,
- };
+ const filters = getQueryFilters(req);
const metrics = await getWebsiteStats(websiteId, { ...filters, startDate, endDate });
diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/analytics/pageviews/getPageviewMetrics.ts
index 9b6a494b..7f8a39d8 100644
--- a/src/queries/analytics/pageviews/getPageviewMetrics.ts
+++ b/src/queries/analytics/pageviews/getPageviewMetrics.ts
@@ -70,7 +70,6 @@ async function clickhouseQuery(
offset: number = 0,
): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
-
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,