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,