Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
950eaca
delegate parse filters straight to ApiQueryParser
RobertJoonas Feb 20, 2026
63dca51
add missing dimensions parsing
RobertJoonas Feb 20, 2026
a07ce65
API v2: add partial_time_labels and present_index to meta
RobertJoonas Mar 4, 2026
18e2d68
separate time vs non-time dimensional breakdowns when merging compari…
RobertJoonas Mar 9, 2026
70dbfaa
extra time labels
RobertJoonas Mar 9, 2026
7ad120a
remove comparisons from legacy timeseries
RobertJoonas Mar 9, 2026
029cfe2
allow fixing now in the new endpoint via conn.private
RobertJoonas Mar 9, 2026
571b69a
transform main graph test to query against the new endpoint
RobertJoonas Mar 10, 2026
4923839
API v2: fix returning buckets outside of queried range
RobertJoonas Mar 10, 2026
bfe0e6b
fix tests
RobertJoonas Mar 10, 2026
18d8492
remove main_graph from Api.StatsController
RobertJoonas Mar 10, 2026
c896189
keep credo happy
RobertJoonas Mar 10, 2026
e643b00
allow time dimensions when querying views_per_visit
RobertJoonas Mar 11, 2026
a223b51
Revert "extra time labels"
RobertJoonas Mar 12, 2026
c66d469
add comparison_results to QueryResults and adjust tests
RobertJoonas Mar 11, 2026
9e11813
return comparison_results as a separate field for timeseries
RobertJoonas Mar 11, 2026
a473b69
time labels and result indices
RobertJoonas Mar 11, 2026
afe91e6
fix sparkline and rest of tests
RobertJoonas Mar 12, 2026
a039352
update changelog
RobertJoonas Mar 12, 2026
bae8ec9
make time_label_result_indices exclusive to internal API
RobertJoonas Mar 12, 2026
cefaa62
revert change in sparkline.ex (not necessary)
RobertJoonas Mar 12, 2026
770dd34
codespell fix
RobertJoonas Mar 12, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.

### Added

- Allow querying `views_per_visit` with a time dimension in Stats API
- Add `bounce_rate` to page-filtered Top Stats even when imports are included, but render a metric warning about imported data not included in `bounce_rate` tooltip.
- Add `time_on_page` to page-filtered Top Stats even when imports are included, unless legacy time on page is in view.
- Adds team_id to query debug metadata (saved in system.query_log log_comment column)
Expand All @@ -20,6 +21,7 @@ All notable changes to this project will be documented in this file.

### Fixed

- Fixed Stats API timeseries returning time buckets falling outside the queried range
- Fixed issue with all non-interactive events being counted as interactive
- Fixed countries map countries staying highlighted on Chrome

Expand Down
4 changes: 2 additions & 2 deletions lib/plausible/stats/api_query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,14 @@ defmodule Plausible.Stats.ApiQueryParser do
end
end

defp parse_dimensions(dimensions) when is_list(dimensions) do
def parse_dimensions(dimensions) when is_list(dimensions) do
parse_list(
dimensions,
&parse_dimension_entry(&1, "Invalid dimensions '#{i(dimensions)}'")
)
end

defp parse_dimensions(nil), do: {:ok, []}
def parse_dimensions(nil), do: {:ok, []}

def parse_order_by(order_by) when is_list(order_by) do
parse_list(order_by, &parse_order_by_entry/1)
Expand Down
15 changes: 8 additions & 7 deletions lib/plausible/stats/dashboard/query_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ defmodule Plausible.Stats.Dashboard.QueryParser do

@valid_comparison_shorthand_keys Map.keys(@valid_comparison_shorthands)

def parse(params) do
def parse(params, opts \\ []) do
with {:ok, input_date_range} <- parse_input_date_range(params),
{:ok, relative_date} <- parse_relative_date(params),
{:ok, filters} <- parse_filters(params),
{:ok, dimensions} <- ApiQueryParser.parse_dimensions(params["dimensions"]),
{:ok, filters} <- ApiQueryParser.parse_filters(params["filters"]),
{:ok, metrics} <- parse_metrics(params),
{:ok, include} <- parse_include(params) do
{:ok,
ParsedQueryParams.new!(%{
input_date_range: input_date_range,
relative_date: relative_date,
dimensions: dimensions,
filters: filters,
metrics: metrics,
include: include
include: include,
now: Keyword.get(opts, :now)
})}
end
end
Expand Down Expand Up @@ -78,6 +81,8 @@ defmodule Plausible.Stats.Dashboard.QueryParser do
compare: compare,
compare_match_day_of_week: params["include"]["compare_match_day_of_week"] == true,
time_labels: params["include"]["time_labels"] == true,
partial_time_labels: params["include"]["partial_time_labels"] == true,
present_index: params["include"]["present_index"] == true,
trim_relative_date_range: true,
drop_unavailable_time_on_page: true
}}
Expand Down Expand Up @@ -112,8 +117,4 @@ defmodule Plausible.Stats.Dashboard.QueryParser do
defp parse_include_compare(_) do
{:ok, nil}
end

defp parse_filters(%{"filters" => filters}) when is_list(filters) do
Plausible.Stats.ApiQueryParser.parse_filters(filters)
end
end
4 changes: 2 additions & 2 deletions lib/plausible/stats/query_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -510,11 +510,11 @@ defmodule Plausible.Stats.QueryBuilder do
message: "Metric `#{metric}` cannot be queried with a filter on `event:page`."
}}

length(query.dimensions) > 0 ->
Enum.any?(query.dimensions, &(not Time.time_dimension?(&1))) ->
{:error,
%QueryError{
code: :invalid_metrics,
message: "Metric `#{metric}` cannot be queried with `dimensions`."
message: "Metric `#{metric}` cannot be queried with non-time dimensions."
}}

true ->
Expand Down
10 changes: 10 additions & 0 deletions lib/plausible/stats/query_include.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ defmodule Plausible.Stats.QueryInclude do
defstruct imports: false,
imports_meta: false,
time_labels: false,
# `time_label_result_indices` is a convenience for our main graph component. It
# is not yet ready for a public API release because it should also account for
# breakdowns by multiple dimensions (time + non-time). Also, at this point it is
# still unclear whether `time_labels` will stay in the public API or not.
time_label_result_indices: false,
present_index: false,
partial_time_labels: false,
total_rows: false,
trim_relative_date_range: false,
compare: nil,
Expand All @@ -18,6 +25,9 @@ defmodule Plausible.Stats.QueryInclude do
imports: boolean(),
imports_meta: boolean(),
time_labels: boolean(),
time_label_result_indices: boolean(),
present_index: boolean(),
partial_time_labels: boolean(),
total_rows: boolean(),
trim_relative_date_range: boolean(),
compare:
Expand Down
110 changes: 105 additions & 5 deletions lib/plausible/stats/query_result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Plausible.Stats.QueryResult do
alias Plausible.Stats.{Query, QueryRunner, Filters}

defstruct results: [],
comparison_results: nil,
meta: %{},
query: nil

Expand Down Expand Up @@ -43,10 +44,11 @@ defmodule Plausible.Stats.QueryResult do

`results` should already-built by Plausible.Stats.QueryRunner
"""
def from(%QueryRunner{results: results} = runner) do
def from(%QueryRunner{results: results, comparison_results: comparison_results} = runner) do
struct!(
__MODULE__,
results: results,
comparison_results: comparison_results,
meta: meta(runner) |> Jason.OrderedObject.new(),
query: query(runner) |> Jason.OrderedObject.new()
)
Expand All @@ -56,7 +58,12 @@ defmodule Plausible.Stats.QueryResult do
%{}
|> add_imports_meta(runner.main_query)
|> add_metric_warnings_meta(runner.main_query)
|> add_time_labels_meta(runner.main_query)
|> add_time_labels_meta(runner)
|> add_time_labels_result_indices_meta(runner)
|> add_comparison_time_labels_meta(runner)
|> add_comparison_time_label_result_indices_meta(runner)
|> add_present_index_meta(runner.main_query)
|> add_partial_time_labels_meta(runner.main_query)
|> add_total_rows_meta(runner.main_query, runner.total_rows)
|> Enum.sort_by(&elem(&1, 0))
end
Expand Down Expand Up @@ -85,14 +92,81 @@ defmodule Plausible.Stats.QueryResult do
end
end

defp add_time_labels_meta(meta, query) do
defp add_time_labels_meta(meta, %QueryRunner{main_query: query}) do
if query.include.time_labels do
Map.put(meta, :time_labels, Plausible.Stats.Time.time_labels(query))
else
meta
end
end

defp add_comparison_time_labels_meta(meta, %QueryRunner{main_query: query} = runner) do
if query.include.time_labels && query.include.compare do
Map.put(
meta,
:comparison_time_labels,
Plausible.Stats.Time.time_labels(runner.comparison_query)
)
else
meta
end
end

defp add_time_labels_result_indices_meta(meta, %QueryRunner{main_query: query} = runner) do
time_labels = meta[:time_labels]

if query.include.time_label_result_indices and is_list(time_labels) do
Map.put(
meta,
:time_label_result_indices,
result_indices_for_time_labels(time_labels, runner.main_results)
)
else
meta
end
end

defp add_comparison_time_label_result_indices_meta(
meta,
%QueryRunner{main_query: query} = runner
) do
comp_time_labels = meta[:comparison_time_labels]

if query.include.time_label_result_indices and is_list(comp_time_labels) do
Map.put(
meta,
:comparison_time_label_result_indices,
result_indices_for_time_labels(comp_time_labels, runner.comparison_results)
)
else
meta
end
end

defp add_present_index_meta(meta, query) do
time_labels = meta[:time_labels]

if query.include.present_index and is_list(time_labels) do
Map.put(meta, :present_index, Plausible.Stats.Time.present_index(time_labels, query))
else
meta
end
end

defp add_partial_time_labels_meta(meta, query) do
time_labels = meta[:time_labels]

if query.include.partial_time_labels and is_list(time_labels) do
Map.put(
meta,
:partial_time_labels,
Plausible.Stats.Time.partial_time_labels(time_labels, query)
)
else
meta
end
end

defp add_total_rows_meta(meta, query, total_rows) do
if query.include.total_rows do
Map.put(meta, :total_rows, total_rows)
Expand Down Expand Up @@ -209,6 +283,15 @@ defmodule Plausible.Stats.QueryResult do

defp metric_warning(_metric, _query), do: nil

defp result_indices_for_time_labels(time_labels, results_list) do
index_lookup_map =
results_list
|> Enum.with_index()
|> Map.new(fn {%{dimensions: [dim]}, idx} -> {dim, idx} end)

Enum.map(time_labels, &Map.get(index_lookup_map, &1))
end

defp to_iso8601(datetime, timezone) do
datetime
|> DateTime.shift_zone!(timezone)
Expand All @@ -217,8 +300,25 @@ defmodule Plausible.Stats.QueryResult do
end

defimpl Jason.Encoder, for: Plausible.Stats.QueryResult do
def encode(%Plausible.Stats.QueryResult{results: results, meta: meta, query: query}, opts) do
Jason.OrderedObject.new(results: results, meta: meta, query: query)
def encode(
%Plausible.Stats.QueryResult{
results: results,
comparison_results: comparison_results,
meta: meta,
query: query
},
opts
) do
if comparison_results do
Jason.OrderedObject.new(
results: results,
comparison_results: comparison_results,
meta: meta,
query: query
)
else
Jason.OrderedObject.new(results: results, meta: meta, query: query)
end
|> Jason.Encoder.encode(opts)
end
end
Loading
Loading