Revert "Adds manual filters on FE, capability for exclusion filters (and the globbing we're used to) for path-based filters (#1067)"

This reverts commit b6eeb40472.
This commit is contained in:
Uku Taht 2021-06-02 14:35:17 +03:00
parent 2be99db54d
commit c37a2de627
14 changed files with 105 additions and 470 deletions

View File

@ -10,10 +10,7 @@ All notable changes to this project will be documented in this file.
- To authenticate against a local postgresql via socket authentication, the environment-variables
`DATABASE_SOCKET_DIR` & `DATABASE_NAME` were added.
- Time on Page metric available in detailed Top Pages report plausible/analytics#1007
- Glob (wildcard) based page, entry page and exit page filters plausible/analytics#1067
- Exclusion filters for page, entry page and exit page filters plausible/analytics#1067
- Menu to add new and edit existing filters directly plausible/analytics#1067
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters plausible/analytics#1073
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters
### Fixed
- Fix weekly report time range plausible/analytics#951

View File

@ -310,11 +310,3 @@ iframe[hidden] {
.pagination-link[disabled] {
@apply cursor-default bg-gray-100 dark:bg-gray-300 pointer-events-none;
}
.filter-list-text:hover ~ .filter-list-edit {
display: flex;
}
.filter-list-text:hover ~ .filter-list-remove {
display: none;
}

View File

@ -38,6 +38,7 @@
.modal__container {
background-color: #fff;
padding: 1rem 2rem;
max-width: 860px;
border-radius: 4px;
margin: 50px auto;
box-sizing: border-box;

View File

@ -161,10 +161,10 @@ class DatePicker extends React.Component {
const leftClasses = `flex items-center px-2 border-r border-gray-300 rounded-l
dark:border-gray-500 dark:text-gray-100 ${
disabledLeft ? "bg-gray-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
disabledLeft ? "bg-gray-200 dark:bg-gray-900" : ""
}`;
const rightClasses = `flex items-center px-2 rounded-r dark:text-gray-100 ${
disabledRight ? "bg-gray-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
disabledRight ? "bg-gray-200 dark:bg-gray-900" : ""
}`;
return (
<div className="flex rounded shadow bg-white mr-4 cursor-pointer dark:bg-gray-800">
@ -242,7 +242,7 @@ class DatePicker extends React.Component {
onKeyPress={this.open}
className="flex items-center justify-between rounded bg-white dark:bg-gray-800 shadow px-4
pr-3 py-2 leading-tight cursor-pointer text-sm font-medium text-gray-800
dark:text-gray-200 h-full hover:bg-gray-200 dark:hover:bg-gray-900"
dark:text-gray-200 h-full"
tabIndex="0"
role="button"
aria-haspopup="true"
@ -312,7 +312,7 @@ class DatePicker extends React.Component {
to={{from: false, to: false, period, ...opts}}
onClick={this.close.bind(this)}
query={this.props.query}
className={`${boldClass } px-4 py-2 md:text-sm leading-tight hover:bg-gray-200
className={`${boldClass } px-4 py-2 md:text-sm leading-tight hover:bg-gray-100
dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 flex items-center justify-between`}
>
{text}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { withRouter, Link } from 'react-router-dom'
import { countFilters, formattedFilters, navigateToQuery, removeQueryParam } from './query'
import { withRouter } from 'react-router-dom'
import { countFilters, navigateToQuery, removeQueryParam } from './query'
import Datamap from 'datamaps'
import Transition from "../transition.js";
@ -60,7 +60,7 @@ class Filters extends React.Component {
}
handleKeyup(e) {
const { query, history } = this.props
const {query, history} = this.props
if (e.ctrlKey || e.metaKey || e.altKey) return
@ -70,7 +70,7 @@ class Filters extends React.Component {
}
handleResize() {
this.setState({ viewport: window.innerWidth || 639 });
this.setState({ viewport: window.innerWidth || 639});
}
handleClick(e) {
@ -102,9 +102,6 @@ class Filters extends React.Component {
};
filterText(key, value, query) {
const negated = value[0] == '!' && ['page', 'entry_page', 'exit_page'].includes(key)
value = negated ? value.slice(1) : value
if (key === "goal") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Completed goal <b>{value}</b></span>
}
@ -114,50 +111,50 @@ class Filters extends React.Component {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{eventName}.{metaKey} is <b>{metaValue}</b></span>
}
if (key === "source") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Source is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Source: <b>{value}</b></span>
}
if (key === "utm_medium") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium: <b>{value}</b></span>
}
if (key === "utm_source") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source: <b>{value}</b></span>
}
if (key === "utm_campaign") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign: <b>{value}</b></span>
}
if (key === "referrer") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer: <b>{value}</b></span>
}
if (key === "screen") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size: <b>{value}</b></span>
}
if (key === "browser") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser: <b>{value}</b></span>
}
if (key === "browser_version") {
const browserName = query.filters["browser"] ? query.filters["browser"] : 'Browser'
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{browserName} Version is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{browserName}.Version: <b>{value}</b></span>
}
if (key === "os") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System: <b>{value}</b></span>
}
if (key === "os_version") {
const osName = query.filters["os"] ? query.filters["os"] : 'OS'
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{osName} Version is <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{osName}.Version: <b>{value}</b></span>
}
if (key === "country") {
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const selectedCountry = allCountries.find((c) => c.id === value) || { properties: { name: value } };
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Country is <b>{selectedCountry.properties.name}</b></span>
const selectedCountry = allCountries.find((c) => c.id === value) || {properties: {name: value}};
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Country: <b>{selectedCountry.properties.name}</b></span>
}
if (key === "page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page is{negated ? ' not' : ''} <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page: <b>{value}</b></span>
}
if (key === "entry_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page is{negated ? ' not' : ''} <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page: <b>{value}</b></span>
}
if (key === "exit_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page is{negated ? ' not' : ''} <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page: <b>{value}</b></span>
}
}
@ -174,50 +171,18 @@ class Filters extends React.Component {
}
renderDropdownFilter(history, [key, value], query) {
if (['goal', 'props'].includes(key)) {
return (
<div className="px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
{this.filterText(key, value, query)}
<b title={`Remove filter: ${formattedFilters[key]}`} className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
</div>
)
}
return (
<div className="px-3 md:px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
<Link
title={`Edit filter: ${formattedFilters[key]}`}
to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}
className="group flex w-full justify-between items-center"
>
{this.filterText(key, value, query)}
<svg className="ml-1 cursor-pointer group-hover:text-indigo-700 dark:group-hover:text-indigo-500 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</Link>
<b title={`Remove filter: ${formattedFilters[key]}`} className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
<div className="px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
{this.filterText(key, value, query)}
<b className="ml-1 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
</div>
)
}
renderListFilter(history, [key, value], query) {
return (
<span key={key} title={value} className="flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded mr-2 items-center">
{['goal', 'props'].includes(key) ? (
<span className="flex w-full h-full items-center py-2 pl-3">
{this.filterText(key, value, query)}
</span>
) : (
<>
<Link title={`Edit filter: ${formattedFilters[key]}`} className="filter-list-text flex w-full h-full items-center py-2 pl-3" to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}>
{this.filterText(key, value, query)}
</Link>
<span className="filter-list-edit hidden h-full w-full px-2 cursor-pointer text-indigo-700 dark:text-indigo-500 items-center">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="1 1 23 23" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</span>
</>
)}
<span title={`Remove filter: ${formattedFilters[key]}`} className="filter-list-remove flex h-full w-full px-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500 items-center" onClick={() => this.removeFilter(key, history, query)}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</span>
<span key={key} title={value} className="inline-flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded py-2 px-3 mr-2">
{this.filterText(key, value, query)} <b className="ml-1 cursor-pointer hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}></b>
</span>
)
}
@ -233,15 +198,11 @@ class Filters extends React.Component {
renderDropDownContent() {
const { viewport } = this.state;
const { history, query, site } = this.props;
const { history, query } = this.props;
return (
<div className="absolute mt-2 rounded shadow-md z-10" style={{ width: viewport <= 768 ? '320px' : '350px', right: '-5px' }} ref={node => this.dropDownNode = node}>
<div className="rounded bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 font-medium text-gray-800 dark:text-gray-200 flex flex-col">
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className="group border-b flex border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer">
<svg className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-indigo-700 dark:group-hover:text-indigo-500 hover:cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Add Filter
</Link>
{this.appliedFilters.map((filter) => this.renderDropdownFilter(history, filter, query))}
<div className="border-t border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer" onClick={() => this.clearAllFilters(history, query)}>
Clear All Filters
@ -278,16 +239,11 @@ class Filters extends React.Component {
}
renderFilterList() {
const { history, query, site } = this.props;
const { viewport } = this.state;
const { history, query } = this.props;
return (
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
<div id="filters">
{(this.appliedFilters.map((filter) => this.renderListFilter(history, filter, query)))}
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className={`button ${viewport <= 768 ? "px-2 mr-1" : "px-3 mr-2"} py-2 cursor-pointer ml-auto text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 shadow`}>
<svg className={`${viewport <= 768 ? "mr-1" : "mr-2"} h-4 w-4 text-indigo-500`} fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{viewport <= 768 ? "Filter" : "Add Filter"}
</Link>
</div>
);
}
@ -295,11 +251,15 @@ class Filters extends React.Component {
render() {
const { wrapped, viewport } = this.state;
if (this.appliedFilters.length > 0 && (wrapped === 2 || viewport <= 768)) {
return this.renderDropDown();
if (this.appliedFilters.length > 0) {
if (wrapped === 2 || viewport <= 768) {
return this.renderDropDown();
}
return this.renderFilterList();
}
return this.renderFilterList();
return null;
}
}

View File

@ -34,7 +34,7 @@ class Historical extends React.Component {
<div className="flex items-center w-full mb-2 sm:mb-0">
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />
<CurrentVisitors timer={this.props.timer} site={this.props.site} query={this.props.query} />
<Filters site={this.props.site} query={this.props.query} history={this.props.history} />
<Filters query={this.props.query} history={this.props.history} />
</div>
<Datepicker site={this.props.site} query={this.props.query} />
</div>

View File

@ -159,22 +159,3 @@ export function eventName(query) {
}
return 'pageviews'
}
export const formattedFilters = {
'goal': 'Goal',
'props': 'Props',
'source': 'Source',
'utm_medium': 'UTM Medium',
'utm_source': 'UTM Source',
'utm_campaign': 'UTM Campaign',
'referrer': 'Referrer',
'screen': 'Screen size',
'browser': 'Browser',
'browser_version': 'Browser Version',
'os': 'Operating System',
'os_version': 'Operating System Version',
'country': 'Country',
'page': 'Page',
'entry_page': 'Entry Page',
'exit_page': 'Exit Page'
}

View File

@ -33,7 +33,7 @@ class Realtime extends React.Component {
<div className="items-center justify-between w-full sm:flex">
<div className="flex items-center w-full mb-2 sm:mb-0">
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />
<Filters site={this.props.site} query={this.props.query} history={this.props.history} />
<Filters query={this.props.query} history={this.props.history} />
</div>
<Datepicker site={this.props.site} query={this.props.query} />
</div>

View File

@ -7,7 +7,6 @@ import PagesModal from './stats/modals/pages'
import EntryPagesModal from './stats/modals/entry-pages'
import ExitPagesModal from './stats/modals/exit-pages'
import CountriesModal from './stats/modals/countries'
import FilterModal from './stats/modals/filter'
import {BrowserRouter, Switch, Route, useLocation} from "react-router-dom";
@ -51,9 +50,6 @@ export default function Router({site, loggedIn}) {
<Route path="/:domain/countries">
<CountriesModal site={site} />
</Route>
<Route path={["/:domain/filter/:field", "/:domain/filter"]}>
<FilterModal site={site} />
</Route>
</Switch>
</Route>
</BrowserRouter>

View File

@ -1,137 +0,0 @@
import React from "react";
import { withRouter, Redirect } from 'react-router-dom'
import Modal from './modal'
import { parseQuery, formattedFilters, navigateToQuery } from '../../query'
class FilterModal extends React.Component {
constructor(props) {
super(props)
this.state = {
query: parseQuery(props.location.search, props.site),
selectedFilter: "",
negated: false,
updatedValue: "",
filterSaved: false
}
this.editableGoals = Object.keys(this.state.query.filters).filter(filter => !['goal', 'props'].includes(filter))
}
componentDidMount() {
this.setState({ selectedFilter: this.props.match.params.field })
}
componentDidUpdate(prevProps, prevState) {
const { query, selectedFilter } = this.state
if (prevState.selectedFilter !== selectedFilter) {
const negated = query.filters[selectedFilter] && query.filters[selectedFilter][0] == '!' && this.negationSupported(selectedFilter)
const updatedValue = negated ? query.filters[selectedFilter].slice(1) : (query.filters[selectedFilter] || "")
this.setState({ updatedValue, negated })
}
}
negationSupported(filter) {
return ['page', 'entry_page', 'exit_page'].includes(filter)
}
renderBody() {
const { selectedFilter, negated, updatedValue, query } = this.state;
const finalFilterValue = (this.negationSupported(selectedFilter) && negated ? '!' : '') + updatedValue
const finalizedQuery = new URLSearchParams(window.location.search)
const validFilter = this.editableGoals.includes(selectedFilter) && updatedValue
finalizedQuery.set(selectedFilter, finalFilterValue)
return (
<React.Fragment>
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] ? 'Edit' : 'Add'} Filter</h1>
<div className="my-4 border-b border-gray-300"></div>
<main className="modal__content">
<form className="flex flex-col" onSubmit={() => {
if (validFilter) {
this.setState({ finalizedQuery })
}
}}>
<select
value={selectedFilter}
className="my-2 block w-full py-2 pl-3 pr-10 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
placeholder="Select a Filter"
onChange={(e) => this.setState({ selectedFilter: e.target.value })}
>
<option disabled value="" className="hidden">Select a Filter</option>
{this.editableGoals.map(filter => <option key={filter} value={filter}>{formattedFilters[filter]}</option>)}
</select>
{this.negationSupported(selectedFilter) &&
<div className="mt-4 flex items-center">
<label className="text-gray-700 dark:text-gray-300 text-sm cursor-pointer">
<input
type="checkbox"
className={"text-indigo-600 bg-gray-100 dark:bg-gray-700 mr-2 relative inline-flex flex-shrink-0 h-6 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none"}
checked={negated}
name="exclude"
onChange={(e) => this.setState({ negated: e.target.checked })}
/>
Exclude pages matching this filter
</label>
</div>
}
{selectedFilter &&
<input
type="text"
className="mt-4 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
value={updatedValue}
placeholder="Filter value"
onChange={(e) => { this.setState({ updatedValue: e.target.value }) }}
/>
}
<button
type="submit"
disabled={!validFilter}
className={"button mt-4 w-2/3 mx-auto"}
>
{query.filters[selectedFilter] ? 'Update' : 'Add'} Filter
</button>
{query.filters[selectedFilter] &&
<button
className={"button mt-8 px-4 mx-auto flex bg-red-500 dark:bg-red-500 hover:bg-red-600 dark:hover:bg-red-700 items-center"}
onClick={() => {
const finalizedQuery = new URLSearchParams(window.location.search)
finalizedQuery.delete(selectedFilter)
this.setState({ finalizedQuery })
}}
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
Remove Filter
</button>
}
</form>
</main>
</React.Fragment>
)
}
render() {
const { finalizedQuery } = this.state
if (finalizedQuery) {
return <Redirect to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: finalizedQuery.toString() }} />
}
return (
<Modal site={this.props.site} maxWidth="460px">
{ this.renderBody()}
</Modal>
)
}
}
export default withRouter(FilterModal)

View File

@ -47,7 +47,7 @@ class Modal extends React.Component {
<div className="modal is-open" onClick={this.props.onClick}>
<div className="modal__overlay">
<button className="modal__close"></button>
<div ref={this.node} className="modal__container dark:bg-gray-800" style={{maxWidth: this.props.maxWidth || '860px'}}>
<div ref={this.node} className="modal__container dark:bg-gray-800">
{this.props.children}
</div>

View File

@ -14,7 +14,6 @@ module.exports = {
extend: {
colors: {
orange: colors.orange,
'gray-950': 'rgb(13, 18, 30)',
'gray-850': 'rgb(26, 32, 44)',
'gray-825': 'rgb(37, 47, 63)'
},
@ -31,7 +30,7 @@ module.exports = {
'9': 9,
},
maxWidth: {
'2xs': '15rem',
'2xs': '16rem',
'3xs': '12rem',
}
},

View File

@ -257,7 +257,13 @@ defmodule Plausible.Stats.Clickhouse do
from(s in referrers, where: s.referrer_source != "")
end
referrers = apply_page_as_entry_page(referrers, site, query)
referrers =
if query.filters["page"] do
page = query.filters["page"]
from(s in referrers, where: s.entry_page == ^page)
else
referrers
end
referrers =
if include_details do
@ -320,7 +326,13 @@ defmodule Plausible.Stats.Clickhouse do
end
defp apply_page_as_entry_page(db_query, _site, query) do
include_path_filter_entry(db_query, query.filters["page"])
page = query.filters["page"]
if is_binary(page) do
from(s in db_query, where: s.entry_page == ^page)
else
db_query
end
end
def utm_mediums(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
@ -1144,9 +1156,21 @@ defmodule Plausible.Stats.Clickhouse do
sessions_q
end
sessions_q = include_path_filter_entry(sessions_q, query.filters["entry_page"])
sessions_q =
if query.filters["entry_page"] do
entry_page = query.filters["entry_page"]
from(s in sessions_q, where: s.entry_page == ^entry_page)
else
sessions_q
end
sessions_q = include_path_filter_exit(sessions_q, query.filters["exit_page"])
sessions_q =
if query.filters["exit_page"] do
exit_page = query.filters["exit_page"]
from(s in sessions_q, where: s.exit_page == ^exit_page)
else
sessions_q
end
q =
from(e in "events",
@ -1170,7 +1194,13 @@ defmodule Plausible.Stats.Clickhouse do
q
end
q = include_path_filter(q, query.filters["page"])
q =
if query.filters["page"] do
page = query.filters["page"]
from(e in q, where: e.pathname == ^page)
else
q
end
if query.filters["props"] do
[{key, val}] = query.filters["props"] |> Enum.into([])
@ -1320,9 +1350,21 @@ defmodule Plausible.Stats.Clickhouse do
q
end
q = include_path_filter_entry(q, query.filters["entry_page"])
q =
if query.filters["entry_page"] do
entry_page = query.filters["entry_page"]
from(s in q, where: s.entry_page == ^entry_page)
else
q
end
q = include_path_filter_exit(q, query.filters["exit_page"])
q =
if query.filters["exit_page"] do
exit_page = query.filters["exit_page"]
from(s in q, where: s.exit_page == ^exit_page)
else
q
end
if query.filters["referrer"] do
ref = query.filters["referrer"]
@ -1422,7 +1464,13 @@ defmodule Plausible.Stats.Clickhouse do
q
end
q = include_path_filter(q, query.filters["page"])
q =
if query.filters["page"] do
page = query.filters["page"]
from(e in q, where: e.pathname == ^page)
else
q
end
if query.filters["props"] do
[{key, val}] = query.filters["props"] |> Enum.into([])
@ -1502,9 +1550,12 @@ defmodule Plausible.Stats.Clickhouse do
end
if path do
{contains_regex, path_regex} = convert_path_regex(path)
if String.match?(path, ~r/\*/) do
path_regex =
"^#{path}\/?$"
|> String.replace(~r/\*\*/, ".*")
|> String.replace(~r/(?<!\.)\*/, "[^/]*")
if contains_regex do
from(e in q, where: fragment("match(?, ?)", e.pathname, ^path_regex))
else
from(e in q, where: e.pathname == ^path)
@ -1513,91 +1564,4 @@ defmodule Plausible.Stats.Clickhouse do
q
end
end
defp check_negated_filter(filter) do
negated = String.at(filter, 0) == "!"
updated_filter = if negated, do: String.slice(filter, 1..-1), else: filter
{negated, updated_filter}
end
defp convert_path_regex(path) do
contains_regex = String.match?(path, ~r/\*/)
regex =
"^#{path}\/?$"
|> String.replace(~r/\*\*/, ".*")
|> String.replace(~r/(?<!\.)\*/, "[^/]*")
{contains_regex, regex}
end
defp include_path_filter(db_query, path) do
if path do
{negated, path} = check_negated_filter(path)
{contains_regex, path_regex} = convert_path_regex(path)
if contains_regex do
if negated do
from(e in db_query, where: fragment("not(match(?, ?))", e.pathname, ^path_regex))
else
from(e in db_query, where: fragment("match(?, ?)", e.pathname, ^path_regex))
end
else
if negated do
from(e in db_query, where: e.pathname != ^path)
else
from(e in db_query, where: e.pathname == ^path)
end
end
else
db_query
end
end
defp include_path_filter_entry(db_query, path) do
if path do
{negated, path} = check_negated_filter(path)
{contains_regex, path_regex} = convert_path_regex(path)
if contains_regex do
if negated do
from(e in db_query, where: fragment("not(match(?, ?))", e.entry_page, ^path_regex))
else
from(e in db_query, where: fragment("match(?, ?)", e.entry_page, ^path_regex))
end
else
if negated do
from(e in db_query, where: e.entry_page != ^path)
else
from(e in db_query, where: e.entry_page == ^path)
end
end
else
db_query
end
end
defp include_path_filter_exit(db_query, path) do
if path do
{negated, path} = check_negated_filter(path)
{contains_regex, path_regex} = convert_path_regex(path)
if contains_regex do
if negated do
from(e in db_query, where: fragment("not(match(?, ?))", e.exit_page, ^path_regex))
else
from(e in db_query, where: fragment("match(?, ?)", e.exit_page, ^path_regex))
end
else
if negated do
from(e in db_query, where: e.exit_page != ^path)
else
from(e in db_query, where: e.exit_page == ^path)
end
end
else
db_query
end
end
end

View File

@ -55,100 +55,6 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
]
end
test "filters pages based on wildards", %{conn: conn, site: site} do
filters = Jason.encode!(%{page: "/*re*"})
conn =
get(
conn,
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
filters
}"
)
assert json_response(conn, 200) == [
%{
"time_on_page" => 1,
"bounce_rate" => nil,
"count" => 2,
"pageviews" => 2,
"name" => "/register"
},
%{
"time_on_page" => nil,
"bounce_rate" => nil,
"count" => 1,
"pageviews" => 1,
"name" => "/irrelevant"
}
]
end
test "filters pages based on exclusion", %{conn: conn, site: site} do
filters = Jason.encode!(%{page: "!/"})
conn =
get(
conn,
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
filters
}"
)
assert json_response(conn, 200) == [
%{
"time_on_page" => 1,
"bounce_rate" => nil,
"count" => 2,
"pageviews" => 2,
"name" => "/register"
},
%{
"time_on_page" => nil,
"bounce_rate" => nil,
"count" => 1,
"pageviews" => 1,
"name" => "/contact"
},
%{
"time_on_page" => nil,
"bounce_rate" => nil,
"count" => 1,
"pageviews" => 1,
"name" => "/irrelevant"
}
]
end
test "filters pages based on wildard exclusion", %{conn: conn, site: site} do
filters = Jason.encode!(%{page: "!/*re*"})
conn =
get(
conn,
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
filters
}"
)
assert json_response(conn, 200) == [
%{
"time_on_page" => 82800,
"bounce_rate" => 33.0,
"count" => 3,
"pageviews" => 3,
"name" => "/"
},
%{
"time_on_page" => nil,
"bounce_rate" => nil,
"count" => 1,
"pageviews" => 1,
"name" => "/contact"
}
]
end
test "returns top pages in realtime report", %{conn: conn, site: site} do
conn = get(conn, "/api/stats/#{site.domain}/pages?period=realtime")
@ -191,18 +97,6 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
}
]
end
test "filters based on exclusion for entry pages", %{conn: conn, site: site} do
filters = Jason.encode!(%{entry_page: "!/"})
conn =
get(
conn,
"/api/stats/#{site.domain}/entry-pages?period=day&date=2019-01-01&filters=#{filters}"
)
assert json_response(conn, 200) == []
end
end
describe "GET /api/stats/:domain/exit-pages" do
@ -215,17 +109,5 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
%{"count" => 3, "exits" => 3, "name" => "/", "exit_rate" => 100.0}
]
end
test "filters based on exclusion for entry pages", %{conn: conn, site: site} do
filters = Jason.encode!(%{exit_page: "!/"})
conn =
get(
conn,
"/api/stats/#{site.domain}/exit-pages?period=day&date=2019-01-01&filters=#{filters}"
)
assert json_response(conn, 200) == []
end
end
end