Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.

### Changed

- Filters appear in the search bar as ?f=is,page,/docs,/blog&f=... instead of ?filters=((is,page,(/docs,/blog)),...) for Plausible links sent on various platforms to work reliably.
- Details modal search inputs are now case-insensitive.
- Improved report performance in cases where site has a lot of unique pathnames

Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createAppRouter } from './dashboard/router'
import ErrorBoundary from './dashboard/error/error-boundary'
import * as api from './dashboard/api'
import * as timer from './dashboard/util/realtime-update-timer'
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query'
import { redirectForLegacyParams } from './dashboard/util/url-search-params'
import SiteContextProvider, {
parseSiteFromDataset
} from './dashboard/site-context'
Expand Down Expand Up @@ -38,7 +38,7 @@ if (container && container.dataset) {
}

try {
filtersBackwardsCompatibilityRedirect(window.location, window.history)
redirectForLegacyParams(window.location, window.history)
} catch (e) {
console.error('Error redirecting in a backwards compatible way', e)
}
Expand Down
13 changes: 10 additions & 3 deletions assets/js/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/** @format */

import React, { useState } from 'react'

import { useIsRealtimeDashboard } from './util/filters'
import React, { useMemo, useState } from 'react'
import VisitorGraph from './stats/graph/visitor-graph'
import Sources from './stats/sources'
import Pages from './stats/pages'
Expand All @@ -11,6 +9,8 @@ import Devices from './stats/devices'
import { TopBar } from './nav-menu/top-bar'
import Behaviours from './stats/behaviours'
import { FiltersBar } from './nav-menu/filters-bar'
import { useQueryContext } from './query-context'
import { isRealTimeDashboard } from './util/filters'

function DashboardStats({
importedDataInView,
Expand Down Expand Up @@ -48,6 +48,13 @@ function DashboardStats({
)
}

function useIsRealtimeDashboard() {
const {
query: { period }
} = useQueryContext()
return useMemo(() => isRealTimeDashboard({ period }), [period])
}

function Dashboard() {
const isRealTimeDashboard = useIsRealtimeDashboard()
const [importedDataInView, setImportedDataInView] = useState(false)
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/nav-menu/filters-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event'
import { TestContextProviders } from '../../../test-utils/app-context-providers'
import { FiltersBar, handleVisibility } from './filters-bar'
import { getRouterBasepath } from '../router'
import { stringifySearch } from '../util/url'
import { stringifySearch } from '../util/url-search-params'

const domain = 'dummy.site'

Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/navigation/use-app-navigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
NavigateOptions,
LinkProps
} from 'react-router-dom'
import { parseSearch, stringifySearch } from '../util/url'
import { parseSearch, stringifySearch } from '../util/url-search-params'

export type AppNavigationTarget = {
/**
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/query-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useLocation } from 'react-router'
import { useMountedEffect } from './custom-hooks'
import * as api from './api'
import { useSiteContext } from './site-context'
import { parseSearch } from './util/url'
import { parseSearch } from './util/url-search-params'
import dayjs from 'dayjs'
import { nowForSite, yesterday } from './util/date'
import {
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/query-dates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import DatePicker from './datepicker'
import { TestContextProviders } from '../../test-utils/app-context-providers'
import { stringifySearch } from './util/url'
import { stringifySearch } from './util/url-search-params'
import { useNavigate } from 'react-router-dom'
import { getRouterBasepath } from './router'

Expand Down
87 changes: 2 additions & 85 deletions assets/js/dashboard/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @format */

import { parseSearch, stringifySearch } from './util/url'
import {
nowForSite,
formatISO,
Expand All @@ -10,12 +9,7 @@ import {
parseUTCDate,
isAfter
} from './util/date'
import {
FILTER_OPERATIONS,
getFiltersByKeyPrefix,
parseLegacyFilter,
parseLegacyPropsFilter
} from './util/filters'
import { FILTER_OPERATIONS, getFiltersByKeyPrefix } from './util/filters'
import { PlausibleSite } from './site-context'
import { ComparisonMode, QueryPeriod } from './query-time-periods'
import { AppNavigationTarget } from './navigation/use-app-navigate'
Expand All @@ -37,7 +31,7 @@ export type Filter = [FilterOperator, FilterKey, FilterClause[]]
* for filters `[["is", "city", [2761369]], ["is", "country", ["AT"]]]`,
* labels would be `{"2761369": "Vienna", "AT": "Austria"}`
* */
export type FilterClauseLabels = Record<string, unknown>
export type FilterClauseLabels = Record<string, string>

export const queryDefaultValue = {
period: '30d' as QueryPeriod,
Expand Down Expand Up @@ -67,29 +61,6 @@ export function addFilter(
return { ...query, filters: [...query.filters, filter] }
}

const LEGACY_URL_PARAMETERS = {
goal: null,
source: null,
utm_medium: null,
utm_source: null,
utm_campaign: null,
utm_content: null,
utm_term: null,
referrer: null,
screen: null,
browser: null,
browser_version: null,
os: null,
os_version: null,
country: 'country_labels',
region: 'region_labels',
city: 'city_labels',
page: null,
hostname: null,
entry_page: null,
exit_page: null
}

export function postProcessFilters(filters: Array<Filter>): Array<Filter> {
return filters.map(([operation, dimension, clauses]) => {
// Rename old name of the operation
Expand All @@ -100,60 +71,6 @@ export function postProcessFilters(filters: Array<Filter>): Array<Filter> {
})
}

// Called once when dashboard is loaded load. Checks whether old filter style is used and if so,
// updates the filters and updates location
export function filtersBackwardsCompatibilityRedirect(
windowLocation: Location,
windowHistory: History
) {
const searchRecord = parseSearch(windowLocation.search)
const getValue = (k: string) => searchRecord[k]

// New filters are used - no need to do anything
if (getValue('filters')) {
return
}

const changedSearchRecordEntries = []
const filters: DashboardQuery['filters'] = []
let labels: DashboardQuery['labels'] = {}

for (const [key, value] of Object.entries(searchRecord)) {
if (LEGACY_URL_PARAMETERS.hasOwnProperty(key)) {
const filter = parseLegacyFilter(key, value) as Filter
filters.push(filter)
const labelsKey: string | null | undefined =
LEGACY_URL_PARAMETERS[key as keyof typeof LEGACY_URL_PARAMETERS]
if (labelsKey && getValue(labelsKey)) {
const clauses = filter[2]
const labelsValues = (getValue(labelsKey) as string)
.split('|')
.filter((label) => !!label)
const newLabels = Object.fromEntries(
clauses.map((clause, index) => [clause, labelsValues[index]])
)

labels = Object.assign(labels, newLabels)
}
} else {
changedSearchRecordEntries.push([key, value])
}
}

if (getValue('props')) {
filters.push(...(parseLegacyPropsFilter(getValue('props')) as Filter[]))
}

if (filters.length > 0) {
changedSearchRecordEntries.push(['filters', filters], ['labels', labels])
windowHistory.pushState(
{},
'',
`${windowLocation.pathname}${stringifySearch(Object.fromEntries(changedSearchRecordEntries))}`
)
}
}

// Returns a boolean indicating whether the given query includes a
// non-empty goal filterset containing a single, or multiple revenue
// goals with the same currency. Used to decide whether to render
Expand Down
60 changes: 1 addition & 59 deletions assets/js/dashboard/util/filters.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/** @format */

import React, { useMemo } from 'react'
import React from 'react'
import * as api from '../api'
import { useQueryContext } from '../query-context'

export const FILTER_MODAL_TO_FILTER_GROUP = {
page: ['page', 'entry_page', 'exit_page'],
Expand Down Expand Up @@ -40,12 +39,6 @@ export const FILTER_OPERATIONS_DISPLAY_NAMES = {
[FILTER_OPERATIONS.contains_not]: 'does not contain'
}

const OPERATION_PREFIX = {
[FILTER_OPERATIONS.isNot]: '!',
[FILTER_OPERATIONS.contains]: '~',
[FILTER_OPERATIONS.is]: ''
}

export function supportsIsNot(filterName) {
return !['goal', 'prop_key'].includes(filterName)
}
Expand All @@ -62,17 +55,6 @@ export function isFreeChoiceFilterOperation(operation) {
)
}

// As of March 2023, Safari does not support negative lookbehind regexes. In case it throws an error, falls back to plain | matching. This means
// escaping pipe characters in filters does not currently work in Safari
let NON_ESCAPED_PIPE_REGEX
try {
NON_ESCAPED_PIPE_REGEX = new RegExp('(?<!\\\\)\\|', 'g')
} catch (_e) {
NON_ESCAPED_PIPE_REGEX = '|'
}

const ESCAPED_PIPE = '\\|'

export function getLabel(labels, filterKey, value) {
if (['country', 'region', 'city'].includes(filterKey)) {
return labels[value]
Expand Down Expand Up @@ -118,27 +100,10 @@ export function hasGoalFilter(query) {
return getFiltersByKeyPrefix(query, 'goal').length > 0
}

export function useHasGoalFilter() {
const {
query: { filters }
} = useQueryContext()
return useMemo(
() => getFiltersByKeyPrefix({ filters }, 'goal').length > 0,
[filters]
)
}

export function isRealTimeDashboard(query) {
return query?.period === 'realtime'
}

export function useIsRealtimeDashboard() {
const {
query: { period }
} = useQueryContext()
return useMemo(() => isRealTimeDashboard({ period }), [period])
}

export function plainFilterText(query, [operation, filterKey, clauses]) {
const formattedFilter = formattedFilters[filterKey]

Expand Down Expand Up @@ -295,26 +260,3 @@ export const formattedFilters = {
entry_page: 'Entry Page',
exit_page: 'Exit Page'
}

export function parseLegacyFilter(filterKey, rawValue) {
const operation =
Object.keys(OPERATION_PREFIX).find(
(operation) => OPERATION_PREFIX[operation] === rawValue[0]
) || FILTER_OPERATIONS.is

const value =
operation === FILTER_OPERATIONS.is ? rawValue : rawValue.substring(1)

const clauses = value
.split(NON_ESCAPED_PIPE_REGEX)
.filter((clause) => !!clause)
.map((val) => val.replaceAll(ESCAPED_PIPE, '|'))

return [operation, filterKey, clauses]
}

export function parseLegacyPropsFilter(rawValue) {
return Object.entries(JSON.parse(rawValue)).map(([key, propVal]) => {
return parseLegacyFilter(`${EVENT_PROPS_PREFIX}${key}`, propVal)
})
}
Loading
Loading