diff --git a/assets/js/dashboard/filters.js b/assets/js/dashboard/filters.js
index 492dbecc1..738c87017 100644
--- a/assets/js/dashboard/filters.js
+++ b/assets/js/dashboard/filters.js
@@ -29,7 +29,17 @@ function clearAllFilters(history, query) {
);
}
-function filterText(key, value, query) {
+function filterType(val) {
+ if (val.startsWith('!')) {
+ return ['is not', val.substr(1)]
+ }
+
+ return ['is', val]
+}
+
+function filterText(key, rawValue, query) {
+ const [type, value] = filterType(rawValue)
+
if (key === "goal") {
return <>Completed goal {value}>
}
@@ -38,51 +48,24 @@ function filterText(key, value, query) {
const eventName = query.filters.goal ? query.filters.goal : 'event'
return <>{eventName}.{metaKey} is {metaValue}>
}
- if (key === "source") {
- return <>Source: {value}>
- }
- if (key === "utm_medium") {
- return <>UTM medium: {value}>
- }
- if (key === "utm_source") {
- return <>UTM source: {value}>
- }
- if (key === "utm_campaign") {
- return <>UTM campaign: {value}>
- }
- if (key === "referrer") {
- return <>Referrer: {value}>
- }
- if (key === "screen") {
- return <>Screen size: {value}>
- }
- if (key === "browser") {
- return <>Browser: {value}>
- }
if (key === "browser_version") {
const browserName = query.filters.browser ? query.filters.browser : 'Browser'
- return <>{browserName}.Version: {value}>
- }
- if (key === "os") {
- return <>Operating System: {value}>
+ return <>{browserName}.Version {type} {value}>
}
if (key === "os_version") {
const osName = query.filters.os ? query.filters.os : 'OS'
- return <>{osName}.Version: {value}>
+ return <>{osName}.Version {type} {value}>
}
if (key === "country") {
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const selectedCountry = allCountries.find((c) => c.id === value) || {properties: {name: value}};
- return <>Country: {selectedCountry.properties.name}>
+ return <>Country {type} {selectedCountry.properties.name}>
}
- if (key === "page") {
- return <>Page: {value}>
- }
- if (key === "entry_page") {
- return <>Entry Page: {value}>
- }
- if (key === "exit_page") {
- return <>Exit Page: {value}>
+
+ const formattedFilter = formattedFilters[key]
+
+ if (formattedFilter) {
+ return <>{formattedFilter} {type} {value}>
}
throw new Error(`Unknown filter: ${key}`)
diff --git a/lib/plausible/stats/aggregate.ex b/lib/plausible/stats/aggregate.ex
index 724827b1f..23ff36f73 100644
--- a/lib/plausible/stats/aggregate.ex
+++ b/lib/plausible/stats/aggregate.ex
@@ -98,7 +98,10 @@ defmodule Plausible.Stats.Aggregate do
{where_clause, where_arg} =
case query.filters["event:page"] do
{:is, page} ->
- {"p=?", page}
+ {"p = ?", page}
+
+ {:is_not, page} ->
+ {"p != ?", page}
{:matches, expr} ->
regex = page_regex(expr)
diff --git a/lib/plausible/stats/base.ex b/lib/plausible/stats/base.ex
index fae213a83..e4746537f 100644
--- a/lib/plausible/stats/base.ex
+++ b/lib/plausible/stats/base.ex
@@ -38,6 +38,9 @@ defmodule Plausible.Stats.Base do
{:is, page} ->
from(e in q, where: e.pathname == ^page)
+ {:is_not, page} ->
+ from(e in q, where: e.pathname != ^page)
+
{:matches, glob_expr} ->
regex = page_regex(glob_expr)
from(e in q, where: fragment("match(?, ?)", e.pathname, ^regex))
@@ -45,15 +48,19 @@ defmodule Plausible.Stats.Base do
{:member, list} ->
from(e in q, where: e.pathname in ^list)
- _ ->
+ nil ->
q
+
+ _ ->
+ raise "Unknown filter type"
end
q =
case query.filters["event:name"] do
{:is, name} -> from(e in q, where: e.name == ^name)
{:member, list} -> from(e in q, where: e.name in ^list)
- _ -> q
+ nil -> q
+ _ -> raise "Unknown filter type"
end
q =
@@ -64,8 +71,11 @@ defmodule Plausible.Stats.Base do
{:is, :event, event} ->
from(e in q, where: e.name == ^event)
- _ ->
+ nil ->
q
+
+ _ ->
+ raise "Unknown goal type"
end
Enum.reduce(query.filters, q, fn {filter_key, filter_value}, query ->
@@ -116,6 +126,9 @@ defmodule Plausible.Stats.Base do
{:is, page} ->
from(e in sessions_q, where: e.entry_page == ^page)
+ {:is_not, page} ->
+ from(e in sessions_q, where: e.entry_page != ^page)
+
{:matches, glob_expr} ->
regex = page_regex(glob_expr)
from(s in sessions_q, where: fragment("match(?, ?)", s.entry_page, ^regex))
@@ -123,8 +136,11 @@ defmodule Plausible.Stats.Base do
{:member, list} ->
from(e in sessions_q, where: e.entry_page in ^list)
- _ ->
+ nil ->
sessions_q
+
+ _ ->
+ raise "Unknown filter type"
end
Enum.reduce(Filters.visit_props(), sessions_q, fn prop_name, sessions_q ->
@@ -148,8 +164,11 @@ defmodule Plausible.Stats.Base do
fragment_data = [{String.to_existing_atom(prop_name), {:in, list}}]
from(s in sessions_q, where: fragment(^fragment_data))
- _ ->
+ nil ->
sessions_q
+
+ _ ->
+ raise "Unknown filter type"
end
end)
end
diff --git a/lib/plausible/stats/filters.ex b/lib/plausible/stats/filters.ex
index 3dbce01ac..7569be821 100644
--- a/lib/plausible/stats/filters.ex
+++ b/lib/plausible/stats/filters.ex
@@ -28,21 +28,15 @@ defmodule Plausible.Stats.Filters do
Enum.reduce(query.filters, %{}, fn {name, val}, new_filters ->
cond do
name == "country" ->
- new_val = Plausible.Stats.CountryName.to_alpha2(val)
- Map.put(new_filters, "visit:country", {:is, new_val})
-
- name == "page" ->
- if String.match?(val, ~r/\*/) do
- Map.put(new_filters, "event:page", {:matches, val})
- else
- Map.put(new_filters, "event:page", {:is, val})
- end
+ {filter_type, filter_val} = filter_value(name, val)
+ new_val = Plausible.Stats.CountryName.to_alpha2(filter_val)
+ Map.put(new_filters, "visit:country", {filter_type, new_val})
name in (@visit_props ++ ["goal"]) ->
- Map.put(new_filters, "visit:" <> name, {:is, val})
+ Map.put(new_filters, "visit:" <> name, filter_value(name, val))
name in @event_props ->
- Map.put(new_filters, "event:" <> name, {:is, val})
+ Map.put(new_filters, "event:" <> name, filter_value(name, val))
name == "props" ->
Enum.reduce(val, new_filters, fn {prop_key, prop_val}, new_filters ->
@@ -50,10 +44,26 @@ defmodule Plausible.Stats.Filters do
end)
true ->
- Map.put(new_filters, name, {:is, val})
+ raise "Unknown filter prop"
end
end)
%Plausible.Stats.Query{query | filters: new_filters}
end
+
+ defp filter_value(key, "!" <> val) do
+ if String.contains?(key, "page") && String.match?(val, ~r/\*/) do
+ {:does_not_match, val}
+ else
+ {:is_not, val}
+ end
+ end
+
+ defp filter_value(key, val) do
+ if String.contains?(key, "page") && String.match?(val, ~r/\*/) do
+ {:matches, val}
+ else
+ {:is, val}
+ end
+ end
end