-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement "Year over Year" comparison mode #2704
Changes from all commits
4c6f47b
c240820
44e506e
9af66ed
a6c634a
b174e80
6004968
960354a
6f7a7f6
e4b55ef
7f479be
da1c092
3c3bd41
7839bde
3f59376
c2d480b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
defmodule Plausible.Stats.Comparisons do | ||
@moduledoc """ | ||
This module provides functions for comparing query periods. | ||
|
||
It allows you to compare a given period with a previous period or with the | ||
same period from the previous year. For example, you can compare this month's | ||
main graph with last month or with the same month from last year. | ||
""" | ||
|
||
alias Plausible.Stats | ||
|
||
@modes ~w(previous_period year_over_year) | ||
@disallowed_periods ~w(realtime all) | ||
|
||
@type mode() :: String.t() | nil | ||
|
||
@spec compare( | ||
Plausible.Site.t(), | ||
Stats.Query.t(), | ||
mode(), | ||
NaiveDateTime.t() | nil | ||
) :: {:ok, Stats.Query.t()} | {:error, :not_supported} | ||
def compare( | ||
%Plausible.Site{} = site, | ||
%Stats.Query{} = source_query, | ||
mode, | ||
now \\ nil | ||
) do | ||
if valid_mode?(source_query, mode) do | ||
now = now || Timex.now(site.timezone) | ||
{:ok, do_compare(source_query, mode, now)} | ||
else | ||
{:error, :not_supported} | ||
end | ||
end | ||
|
||
defp do_compare(source_query, "year_over_year", now) do | ||
start_date = Date.add(source_query.date_range.first, -365) | ||
end_date = earliest(source_query.date_range.last, now) |> Date.add(-365) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. elegant 👌 |
||
|
||
range = Date.range(start_date, end_date) | ||
%Stats.Query{source_query | date_range: range} | ||
end | ||
|
||
defp do_compare(source_query, "previous_period", now) do | ||
last = earliest(source_query.date_range.last, now) | ||
diff_in_days = Date.diff(source_query.date_range.first, last) - 1 | ||
|
||
new_first = Date.add(source_query.date_range.first, diff_in_days) | ||
new_last = Date.add(last, diff_in_days) | ||
|
||
range = Date.range(new_first, new_last) | ||
%Stats.Query{source_query | date_range: range} | ||
end | ||
|
||
defp earliest(a, b) do | ||
if Date.compare(a, b) in [:eq, :lt], do: a, else: b | ||
end | ||
|
||
@spec valid_mode?(Stats.Query.t(), mode()) :: boolean() | ||
@doc """ | ||
Returns whether the source query and the selected mode support comparisons. | ||
|
||
For example, the realtime view doesn't support comparisons. Additionally, only | ||
#{inspect(@modes)} are supported. | ||
""" | ||
def valid_mode?(%Stats.Query{period: period}, mode) do | ||
mode in @modes && period not in @disallowed_periods | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.StatsController do | |
use Plausible.Repo | ||
use Plug.ErrorHandler | ||
alias Plausible.Stats | ||
alias Plausible.Stats.{Query, Filters} | ||
alias Plausible.Stats.{Query, Filters, Comparisons} | ||
|
||
require Logger | ||
|
||
|
@@ -115,9 +115,9 @@ defmodule PlausibleWeb.Api.StatsController do | |
full_intervals = build_full_intervals(query, labels) | ||
|
||
comparison_result = | ||
if params["comparison"] do | ||
comparison_query = Query.shift_back(query, site) | ||
Stats.timeseries(site, comparison_query, [selected_metric]) | ||
case Comparisons.compare(site, query, params["comparison"]) do | ||
{:ok, comparison_query} -> Stats.timeseries(site, comparison_query, [selected_metric]) | ||
{:error, :not_supported} -> nil | ||
end | ||
|
||
json(conn, %{ | ||
|
@@ -174,9 +174,10 @@ defmodule PlausibleWeb.Api.StatsController do | |
site = conn.assigns[:site] | ||
|
||
with :ok <- validate_params(params) do | ||
comparison_mode = params["comparison"] || "previous_period" | ||
query = Query.from(site, params) |> Filters.add_prefix() | ||
|
||
{top_stats, sample_percent} = fetch_top_stats(site, query) | ||
{top_stats, sample_percent} = fetch_top_stats(site, query, comparison_mode) | ||
|
||
json(conn, %{ | ||
top_stats: top_stats, | ||
|
@@ -243,7 +244,8 @@ defmodule PlausibleWeb.Api.StatsController do | |
|
||
defp fetch_top_stats( | ||
site, | ||
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query | ||
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query, | ||
_comparison_mode | ||
) do | ||
query_30m = %Query{query | period: "30m"} | ||
|
||
|
@@ -270,7 +272,7 @@ defmodule PlausibleWeb.Api.StatsController do | |
{stats, 100} | ||
end | ||
|
||
defp fetch_top_stats(site, %Query{period: "realtime"} = query) do | ||
defp fetch_top_stats(site, %Query{period: "realtime"} = query, _comparison_mode) do | ||
query_30m = %Query{query | period: "30m"} | ||
|
||
%{ | ||
|
@@ -296,29 +298,41 @@ defmodule PlausibleWeb.Api.StatsController do | |
{stats, 100} | ||
end | ||
|
||
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _goal}} = query) do | ||
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _goal}} = query, comparison_mode) do | ||
total_q = Query.remove_event_filters(query, [:goal, :props]) | ||
prev_query = Query.shift_back(query, site) | ||
prev_total_query = Query.shift_back(total_q, site) | ||
|
||
{prev_converted_visitors, prev_completions} = | ||
case Stats.Comparisons.compare(site, query, comparison_mode) do | ||
{:ok, prev_query} -> | ||
%{visitors: %{value: prev_converted_visitors}, events: %{value: prev_completions}} = | ||
Stats.aggregate(site, prev_query, [:visitors, :events]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a concern for this PR but I think eventually we want to also support the different comparison modes in the API. In that case it would make sense to push the comparison logic down to |
||
|
||
{prev_converted_visitors, prev_completions} | ||
|
||
{:error, :not_supported} -> | ||
{nil, nil} | ||
end | ||
|
||
prev_unique_visitors = | ||
case Stats.Comparisons.compare(site, total_q, comparison_mode) do | ||
{:ok, prev_total_query} -> | ||
site | ||
|> Stats.aggregate(prev_total_query, [:visitors]) | ||
|> get_in([:visitors, :value]) | ||
|
||
{:error, :not_supported} -> | ||
nil | ||
end | ||
|
||
%{ | ||
visitors: %{value: unique_visitors} | ||
} = Stats.aggregate(site, total_q, [:visitors]) | ||
|
||
%{ | ||
visitors: %{value: prev_unique_visitors} | ||
} = Stats.aggregate(site, prev_total_query, [:visitors]) | ||
|
||
%{ | ||
visitors: %{value: converted_visitors}, | ||
events: %{value: completions} | ||
} = Stats.aggregate(site, query, [:visitors, :events]) | ||
|
||
%{ | ||
visitors: %{value: prev_converted_visitors}, | ||
events: %{value: prev_completions} | ||
} = Stats.aggregate(site, prev_query, [:visitors, :events]) | ||
|
||
conversion_rate = calculate_cr(unique_visitors, converted_visitors) | ||
prev_conversion_rate = calculate_cr(prev_unique_visitors, prev_converted_visitors) | ||
|
||
|
@@ -348,9 +362,7 @@ defmodule PlausibleWeb.Api.StatsController do | |
{stats, 100} | ||
end | ||
|
||
defp fetch_top_stats(site, query) do | ||
prev_query = Query.shift_back(query, site) | ||
|
||
defp fetch_top_stats(site, query, comparison_mode) do | ||
metrics = | ||
if query.filters["event:page"] do | ||
[:visitors, :pageviews, :bounce_rate, :time_on_page, :visits, :sample_percent] | ||
|
@@ -359,7 +371,12 @@ defmodule PlausibleWeb.Api.StatsController do | |
end | ||
|
||
current_results = Stats.aggregate(site, query, metrics) | ||
prev_results = Stats.aggregate(site, prev_query, metrics) | ||
|
||
prev_results = | ||
case Stats.Comparisons.compare(site, query, comparison_mode) do | ||
{:ok, prev_results_query} -> Stats.aggregate(site, prev_results_query, metrics) | ||
{:error, :not_supported} -> nil | ||
end | ||
|
||
stats = | ||
[ | ||
|
@@ -377,11 +394,11 @@ defmodule PlausibleWeb.Api.StatsController do | |
|
||
defp top_stats_entry(current_results, prev_results, name, key) do | ||
if current_results[key] do | ||
%{ | ||
name: name, | ||
value: current_results[key][:value], | ||
change: calculate_change(key, prev_results[key][:value], current_results[key][:value]) | ||
} | ||
value = get_in(current_results, [key, :value]) | ||
prev_value = get_in(prev_results, [key, :value]) | ||
change = prev_value && calculate_change(key, prev_value, value) | ||
|
||
%{name: name, value: value, change: change} | ||
end | ||
end | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously the comparison query property was a boolean, on or off. Now it supports multiple modes and it's a string, e.g.
last_year
,previous_period
.