Skip to content

Commit 9648296

Browse files
authored
Remove internal query api schema (#5969)
* move goal name validation test * move realtime query construction tests * Drop include.compare parsing tests These are focused solely on the parsing part and not the utc_time_range construction which is tested in Plausible.Stats.ComparisonsTest. * Move filter validation to QueryBuilder + tests * move relative_date param tests * make tests use public schema where possible * fixup: remove duplicate describe block * add tests for passing filter validation ...and remove internal operators to stick to testing only the public schema in QueryParseAndBuildTest * remove time:minute dimension validation test * move exit_rate metric validation tests * make test meaningful again has_not_done is available in the public API too. Use matches_wildcard instead. * move consolidated_site_ids tests * use QueryBuilder.build for ad-hoc query building where possible * adjust api_query_parser error messages * use QueryBuilder for validating segments too * remove :internal query api schema * clean up ApiQueryParser of internal-only behaviour * add segment validtion comment
1 parent 981dc85 commit 9648296

File tree

27 files changed

+806
-978
lines changed

27 files changed

+806
-978
lines changed

assets/js/types/query-api.d.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@ export type Metric =
1212
| "conversion_rate"
1313
| "group_conversion_rate"
1414
| "time_on_page"
15-
| "exit_rate"
1615
| "total_revenue"
1716
| "average_revenue"
1817
| "scroll_depth";
1918
export type DateRangeShorthand =
20-
| "30m"
21-
| "realtime"
2219
| "all"
2320
| "day"
2421
| "7d"
@@ -71,7 +68,7 @@ export type SimpleFilterDimensions =
7168
| "visit:exit_page_hostname";
7269
export type CustomPropertyFilterDimensions = string;
7370
export type GoalDimension = "event:goal";
74-
export type TimeDimensions = ("time" | "time:month" | "time:week" | "time:day" | "time:hour") | "time:minute";
71+
export type TimeDimensions = "time" | "time:month" | "time:week" | "time:day" | "time:hour";
7572
export type FilterTree = FilterEntry | FilterAndOr | FilterNot | FilterHasDone;
7673
export type FilterEntry = FilterWithoutGoals | FilterWithIs | FilterWithContains | FilterWithPattern;
7774
/**
@@ -126,7 +123,7 @@ export type FilterWithContains =
126123
* @maxItems 3
127124
*/
128125
export type FilterWithPattern = [
129-
FilterOperationRegex | ("matches_wildcard" | "matches_wildcard_not"),
126+
FilterOperationRegex,
130127
SimpleFilterDimensions | CustomPropertyFilterDimensions,
131128
Clauses
132129
];
@@ -157,7 +154,6 @@ export type OrderByEntry = [
157154
Metric | SimpleFilterDimensions | CustomPropertyFilterDimensions | TimeDimensions,
158155
"asc" | "desc"
159156
];
160-
export type ComparisonMode = "previous_period" | "year_over_year";
161157

162158
export interface QueryApiSchema {
163159
/**
@@ -170,7 +166,6 @@ export interface QueryApiSchema {
170166
* @minItems 1
171167
*/
172168
metrics: [Metric, ...Metric[]];
173-
date?: string;
174169
/**
175170
* Date range to query
176171
*/
@@ -198,14 +193,6 @@ export interface QueryApiSchema {
198193
* If set and using `day`, `month` or `year` date_ranges, the query will be trimmed to the current date
199194
*/
200195
trim_relative_date_range?: boolean;
201-
/**
202-
* If set, executes the same query but over a comparison date range
203-
*/
204-
compare?: DateRange | ComparisonMode | never;
205-
/**
206-
* With the `compare` option, if set and using time:day dimensions, day-of-week of comparison query is matched
207-
*/
208-
compare_match_day_of_week?: boolean;
209196
};
210197
pagination?: {
211198
/**

extra/lib/plausible/stats/consolidated_view.ex

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,16 @@ defmodule Plausible.Stats.ConsolidatedView do
8686
end
8787

8888
defp query_24h_intervals(view, now) do
89+
from_datetime = now |> NaiveDateTime.shift(hour: -24) |> DateTime.from_naive!("Etc/UTC")
90+
to_datetime = now |> DateTime.from_naive!("Etc/UTC")
91+
8992
graph_query =
90-
Stats.Query.parse_and_build!(
91-
view,
92-
:internal,
93-
%{
94-
"site_id" => view.domain,
95-
"metrics" => ["visitors"],
96-
"date_range" => [
97-
NaiveDateTime.shift(now, hour: -24)
98-
|> DateTime.from_naive!("Etc/UTC")
99-
|> DateTime.to_iso8601(),
100-
now
101-
|> DateTime.from_naive!("Etc/UTC")
102-
|> DateTime.to_iso8601()
103-
],
104-
"dimensions" => ["time:hour"],
105-
"order_by" => [["time:hour", "asc"]]
106-
}
107-
)
93+
QueryBuilder.build!(view, %ParsedQueryParams{
94+
metrics: [:visitors],
95+
input_date_range: {:datetime_range, from_datetime, to_datetime},
96+
dimensions: ["time:hour"],
97+
order_by: [{"time:hour", :asc}]
98+
})
10899

109100
%Stats.QueryResult{results: results} = Stats.query(view, graph_query)
110101

extra/lib/plausible_web/live/funnel_settings/form.ex

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
1010

1111
import PlausibleWeb.Live.Components.Form
1212
alias Plausible.{Goals, Funnels}
13+
alias Plausible.Stats.{QueryBuilder, ParsedQueryParams}
1314

1415
def mount(_params, %{"domain" => domain} = session, socket) do
1516
site =
@@ -349,15 +350,10 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
349350
)
350351

351352
query =
352-
Plausible.Stats.Query.parse_and_build!(
353-
site,
354-
:internal,
355-
%{
356-
"site_id" => site.domain,
357-
"date_range" => "month",
358-
"metrics" => ["pageviews"]
359-
}
360-
)
353+
QueryBuilder.build!(site, %ParsedQueryParams{
354+
metrics: [:pageviews],
355+
input_date_range: :month
356+
})
361357

362358
{:ok, {definition, query}}
363359
end

lib/plausible/segments/segment.ex

Lines changed: 21 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Plausible.Segments.Segment do
55
use Plausible
66
use Ecto.Schema
77
import Ecto.Changeset
8+
alias Plausible.Stats.{ApiQueryParser, QueryBuilder, ParsedQueryParams}
89

910
@segment_types [:personal, :site]
1011

@@ -102,77 +103,33 @@ defmodule Plausible.Segments.Segment do
102103
),
103104
do: :ok
104105

105-
def validate_segment_data_if_exists(%Plausible.Site{} = site, segment_data, restricted_depth?),
106-
do: validate_segment_data(site, segment_data, restricted_depth?)
107-
108-
def validate_segment_data(
109-
%Plausible.Site{} = site,
110-
%{"filters" => filters},
111-
restricted_depth?
112-
) do
113-
with {:ok, %Plausible.Stats.Query{filters: parsed_filters}} <-
114-
build_naive_query_from_segment_data(site, filters),
115-
:ok <- maybe_validate_filters_depth(parsed_filters, restricted_depth?) do
116-
:ok
117-
else
118-
{:error, message} ->
119-
reformat_filters_errors(message)
120-
121-
:error_deep_filters_not_supported ->
122-
reformat_filters_errors("Invalid filters. Deep filters are not supported.")
123-
end
106+
def validate_segment_data_if_exists(%Plausible.Site{} = site, segment_data, restricted_depth?) do
107+
validate_segment_data(site, segment_data, restricted_depth?)
124108
end
125109

110+
@spec validate_segment_data(Plausible.Site.t(), map(), boolean()) ::
111+
:ok | {:error, {:invalid_filters, String.t()}}
126112
@doc """
127-
This function builds a simple query using the filters from Plausibe.Segment.segment_data
128-
to test whether the filters used in the segment stand as legitimate query filters.
129-
If they don't, it indicates an error with the filters that must be passed to the client,
130-
so they could reconfigure the filters.
113+
Checks whether the filters in segment_data are valid by attempting to build
114+
a `%Query{}` struct with them. The `metrics` and `input_date_range` values
115+
do not matter as long as they don't affect filters being the only field
116+
affecting query validity.
131117
"""
132-
def build_naive_query_from_segment_data(%Plausible.Site{} = site, filters),
133-
do:
134-
Plausible.Stats.Query.parse_and_build(
135-
site,
136-
:internal,
137-
%{
138-
"site_id" => site.domain,
139-
"metrics" => ["visitors"],
140-
"date_range" => "7d",
141-
"filters" => filters
142-
},
143-
%{}
144-
)
145-
146-
@doc """
147-
This function handles the error from building the naive query that is used to validate segment filters.
148-
If the error is only about filters, it's marked as :invalid_filters error and ultimately forwarded to client.
149-
If the error is not only about filters, the client can't do anything about the situation,
150-
and the error message is returned as-is.
151-
152-
### Examples
153-
iex> reformat_filters_errors(~s(#/metrics/0 Invalid metric "Visitors"\\n#/filters/0 Invalid filter "A"))
154-
{:error, ~s(#/metrics/0 Invalid metric "Visitors"\\n#/filters/0 Invalid filter "A")}
155-
156-
iex> reformat_filters_errors(~s(#/filters/0 Invalid filter "A"\\n#/filters/1 Invalid filter "B"))
157-
{:error, {:invalid_filters, ~s(#/filters/0 Invalid filter "A"\\n#/filters/1 Invalid filter "B")}}
158-
159-
iex> reformat_filters_errors("Invalid filters. Dimension `event:goal` can only be filtered at the top level.")
160-
{:error, {:invalid_filters, "Invalid filters. Dimension `event:goal` can only be filtered at the top level."}}
161-
"""
162-
def reformat_filters_errors(message) do
163-
lines = String.split(message, "\n")
164-
165-
if Enum.all?(lines, fn line ->
166-
String.starts_with?(line, "#/filters/") or String.starts_with?(line, "Invalid filters.")
167-
end) do
168-
{:error, {:invalid_filters, message}}
118+
def validate_segment_data(%Plausible.Site{} = site, %{"filters" => filters}, restricted_depth?) do
119+
with {:ok, parsed_filters} <- ApiQueryParser.parse_filters(filters),
120+
{:ok, _} <-
121+
QueryBuilder.build(site, %ParsedQueryParams{
122+
metrics: [:visitors],
123+
input_date_range: {:last_n_days, 7},
124+
filters: parsed_filters
125+
}),
126+
:ok <- maybe_validate_filters_depth(parsed_filters, restricted_depth?) do
127+
:ok
169128
else
170-
{:error, message}
129+
{:error, message} -> {:error, {:invalid_filters, message}}
171130
end
172131
end
173132

174-
@spec maybe_validate_filters_depth([any()], boolean()) ::
175-
:ok | :error_deep_filters_not_supported
176133
defp maybe_validate_filters_depth(filters, restricted_depth?)
177134

178135
defp maybe_validate_filters_depth(_filters, false), do: :ok
@@ -181,7 +138,7 @@ defmodule Plausible.Segments.Segment do
181138
if Enum.all?(filters, &dashboard_compatible_filter?/1) do
182139
:ok
183140
else
184-
:error_deep_filters_not_supported
141+
{:error, "Invalid filters. Deep filters are not supported."}
185142
end
186143
end
187144

lib/plausible/stats/api_query_parser.ex

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ defmodule Plausible.Stats.ApiQueryParser do
2222

2323
def default_pagination(), do: @default_pagination
2424

25-
def parse(schema_type, params) when is_map(params) do
26-
input_date_range = Map.get(params, "date_range")
27-
28-
with :ok <- JSONSchema.validate(schema_type, params),
29-
{:ok, input_date_range} <- parse_input_date_range(input_date_range),
25+
def parse(params) when is_map(params) do
26+
with :ok <- JSONSchema.validate(params),
27+
{:ok, input_date_range} <- parse_input_date_range(params["date_range"]),
3028
{:ok, metrics} <- parse_metrics(Map.fetch!(params, "metrics")),
3129
{:ok, filters} <- parse_filters(params["filters"]),
3230
{:ok, dimensions} <- parse_dimensions(params["dimensions"]),
@@ -96,7 +94,7 @@ defmodule Plausible.Stats.ApiQueryParser do
9694
def parse_filter_second(_operator, filter), do: parse_filter_dimension(filter)
9795

9896
defp parse_filter_dimension([_operator, filter_dimension | _rest] = filter) do
99-
parse_filter_dimension_string(filter_dimension, "Invalid filter '#{i(filter)}")
97+
parse_filter_dimension_string(filter_dimension, "Invalid filter '#{i(filter)}'.")
10098
end
10199

102100
defp parse_filter_dimension(filter), do: {:error, "Invalid filter '#{i(filter)}'."}
@@ -149,7 +147,7 @@ defmodule Plausible.Stats.ApiQueryParser do
149147
end
150148
end
151149

152-
defp parse_clauses_list(filter), do: {:error, "Invalid filter '#{i(filter)}'"}
150+
defp parse_clauses_list(filter), do: {:error, "Invalid filter '#{i(filter)}'."}
153151

154152
defp parse_filter_modifiers(modifiers) when is_map(modifiers) do
155153
{:ok, [atomize_keys(modifiers)]}
@@ -159,8 +157,6 @@ defmodule Plausible.Stats.ApiQueryParser do
159157
{:ok, []}
160158
end
161159

162-
defp parse_input_date_range("realtime"), do: {:ok, :realtime}
163-
defp parse_input_date_range("30m"), do: {:ok, :realtime_30m}
164160
defp parse_input_date_range("day"), do: {:ok, :day}
165161
defp parse_input_date_range("month"), do: {:ok, :month}
166162
defp parse_input_date_range("year"), do: {:ok, :year}
@@ -280,24 +276,12 @@ defmodule Plausible.Stats.ApiQueryParser do
280276

281277
@allowed_include_keys Enum.map(Map.keys(@default_include), &Atom.to_string/1)
282278

283-
defp parse_include_entry("compare", value) do
284-
with {:ok, parsed_compare} <- parse_compare(value) do
285-
{:ok, {:compare, parsed_compare}}
286-
end
287-
end
288-
289279
defp parse_include_entry(key, value) when key in @allowed_include_keys do
290280
{:ok, {String.to_existing_atom(key), value}}
291281
end
292282

293283
defp parse_include_entry(key, _value), do: {:error, "Invalid include key'#{i(key)}'."}
294284

295-
defp parse_compare(false), do: {:ok, nil}
296-
defp parse_compare("previous_period"), do: {:ok, :previous_period}
297-
defp parse_compare("year_over_year"), do: {:ok, :year_over_year}
298-
defp parse_compare([from_date, to_date]), do: parse_date_strings(from_date, to_date)
299-
defp parse_compare(compare), do: {:error, "Invalid include.compare '#{i(compare)}'."}
300-
301285
defp parse_pagination(pagination) when is_map(pagination) do
302286
{:ok, Map.merge(@default_pagination, atomize_keys(pagination))}
303287
end
@@ -306,13 +290,10 @@ defmodule Plausible.Stats.ApiQueryParser do
306290

307291
defp atomize_keys(map) when is_map(map) do
308292
Map.new(map, fn {key, value} ->
309-
key = String.to_existing_atom(key)
310-
{key, atomize_keys(value)}
293+
{String.to_existing_atom(key), value}
311294
end)
312295
end
313296

314-
defp atomize_keys(value), do: value
315-
316297
defp parse_filter_dimension_string(dimension, error_message \\ "") do
317298
case dimension do
318299
"event:props:" <> property_name ->

lib/plausible/stats/goal_suggestions.ex

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Plausible.Stats.GoalSuggestions do
22
@moduledoc false
33

44
alias Plausible.{Repo, ClickhouseRepo}
5-
alias Plausible.Stats.Query
5+
alias Plausible.Stats.{Query, QueryBuilder, ParsedQueryParams, QueryInclude}
66
import Plausible.Stats.Base
77
import Ecto.Query
88

@@ -39,16 +39,11 @@ defmodule Plausible.Stats.GoalSuggestions do
3939
from_date = Date.shift(to_date, month: -6)
4040

4141
query =
42-
Plausible.Stats.Query.parse_and_build!(
43-
site,
44-
:internal,
45-
%{
46-
"site_id" => site.domain,
47-
"date_range" => [Date.to_iso8601(from_date), Date.to_iso8601(to_date)],
48-
"metrics" => ["pageviews"],
49-
"include" => %{"imports" => true}
50-
}
51-
)
42+
QueryBuilder.build!(site, %ParsedQueryParams{
43+
input_date_range: {:date_range, from_date, to_date},
44+
metrics: [:pageviews],
45+
include: %QueryInclude{imports: true}
46+
})
5247

5348
native_q =
5449
from(e in base_event_query(query),

0 commit comments

Comments
 (0)