mirror of
https://github.com/plausible/analytics.git
synced 2025-01-03 15:17:58 +03:00
Implement "Year over Year" comparison mode (#2704)
This pull request adds support for multiple comparison modes, changes the comparison checkbox to a combobox, and implements the year over year comparison mode. The feature is still behind a feature flag. Co-authored-by: Uku Taht <uku.taht@gmail.com>
This commit is contained in:
parent
874d664521
commit
8b0f0cabc2
@ -45,7 +45,7 @@ export function serializeQuery(query, extraQuery=[]) {
|
|||||||
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
|
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
|
||||||
if (query.with_imported) { queryObj.with_imported = query.with_imported }
|
if (query.with_imported) { queryObj.with_imported = query.with_imported }
|
||||||
if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH }
|
if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH }
|
||||||
if (query.comparison) { queryObj.comparison = true }
|
if (query.comparison) { queryObj.comparison = query.comparison }
|
||||||
Object.assign(queryObj, ...extraQuery)
|
Object.assign(queryObj, ...extraQuery)
|
||||||
|
|
||||||
return '?' + serialize(queryObj)
|
return '?' + serialize(queryObj)
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { navigateToQuery } from './query'
|
import { navigateToQuery } from './query'
|
||||||
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
const COMPARISON_MODES = {
|
||||||
|
'previous_period': 'Previous period',
|
||||||
|
'year_over_year': 'Year over year',
|
||||||
|
}
|
||||||
|
|
||||||
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
|
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
|
||||||
|
|
||||||
@ -8,14 +16,46 @@ const ComparisonInput = function({ site, query, history }) {
|
|||||||
if (!site.flags.comparisons) return null
|
if (!site.flags.comparisons) return null
|
||||||
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
||||||
|
|
||||||
function update(event) {
|
function update(key) {
|
||||||
navigateToQuery(history, query, { comparison: event.target.checked })
|
navigateToQuery(history, query, { comparison: key })
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem({ label, value, isCurrentlySelected }) {
|
||||||
|
const labelClass = classNames("font-medium text-sm", { "font-bold disabled": isCurrentlySelected })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={value}
|
||||||
|
onClick={() => update(value)}
|
||||||
|
className="px-4 py-2 leading-tight hover:bg-gray-100 dark:text-white hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 flex hover:cursor-pointer">
|
||||||
|
<span className={labelClass}>{ label }</span>
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-none mx-3">
|
<div className="flex ml-auto pl-2">
|
||||||
<input id="comparison-input" type="checkbox" onChange={update} checked={query.comparison} className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
|
<div className="w-20 sm:w-36 md:w-48 md:relative">
|
||||||
<label htmlFor="comparison-input" className="ml-1.5 font-medium text-xs md:text-sm text-gray-700 dark:text-white">Compare</label>
|
<Menu as="div" className="relative inline-block pl-2 w-full">
|
||||||
|
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow truncate cursor-pointer w-full">
|
||||||
|
<span>{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
|
||||||
|
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-5" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95">
|
||||||
|
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
|
||||||
|
{ renderItem({ label: "Disabled", value: false, isCurrentlySelected: !query.comparison }) }
|
||||||
|
{ Object.keys(COMPARISON_MODES).map((key) => renderItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison})) }
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export function parseQuery(querystring, site) {
|
|||||||
period = '30d'
|
period = '30d'
|
||||||
}
|
}
|
||||||
|
|
||||||
let comparison = !!q.get('comparison')
|
let comparison = q.get('comparison')
|
||||||
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null
|
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
70
lib/plausible/stats/comparisons.ex
Normal file
70
lib/plausible/stats/comparisons.ex
Normal file
@ -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)
|
||||||
|
|
||||||
|
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
|
@ -10,58 +10,7 @@ defmodule Plausible.Stats.Query do
|
|||||||
require OpenTelemetry.Tracer, as: Tracer
|
require OpenTelemetry.Tracer, as: Tracer
|
||||||
alias Plausible.Stats.{FilterParser, Interval}
|
alias Plausible.Stats.{FilterParser, Interval}
|
||||||
|
|
||||||
def shift_back(%__MODULE__{period: "year"} = query, site) do
|
@type t :: %__MODULE__{}
|
||||||
# Querying current year to date
|
|
||||||
{new_first, new_last} =
|
|
||||||
if Timex.compare(Timex.now(site.timezone), query.date_range.first, :year) == 0 do
|
|
||||||
diff =
|
|
||||||
Timex.diff(
|
|
||||||
Timex.beginning_of_year(Timex.now(site.timezone)),
|
|
||||||
Timex.now(site.timezone),
|
|
||||||
:days
|
|
||||||
) - 1
|
|
||||||
|
|
||||||
{query.date_range.first |> Timex.shift(days: diff),
|
|
||||||
Timex.now(site.timezone) |> Timex.to_date() |> Timex.shift(days: diff)}
|
|
||||||
else
|
|
||||||
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
|
|
||||||
|
|
||||||
{query.date_range.first |> Timex.shift(days: diff),
|
|
||||||
query.date_range.last |> Timex.shift(days: diff)}
|
|
||||||
end
|
|
||||||
|
|
||||||
Map.put(query, :date_range, Date.range(new_first, new_last))
|
|
||||||
end
|
|
||||||
|
|
||||||
def shift_back(%__MODULE__{period: "month"} = query, site) do
|
|
||||||
# Querying current month to date
|
|
||||||
{new_first, new_last} =
|
|
||||||
if Timex.compare(Timex.now(site.timezone), query.date_range.first, :month) == 0 do
|
|
||||||
diff =
|
|
||||||
Timex.diff(
|
|
||||||
Timex.beginning_of_month(Timex.now(site.timezone)),
|
|
||||||
Timex.now(site.timezone),
|
|
||||||
:days
|
|
||||||
) - 1
|
|
||||||
|
|
||||||
{query.date_range.first |> Timex.shift(days: diff),
|
|
||||||
Timex.now(site.timezone) |> Timex.to_date() |> Timex.shift(days: diff)}
|
|
||||||
else
|
|
||||||
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
|
|
||||||
|
|
||||||
{query.date_range.first |> Timex.shift(days: diff),
|
|
||||||
query.date_range.last |> Timex.shift(days: diff)}
|
|
||||||
end
|
|
||||||
|
|
||||||
Map.put(query, :date_range, Date.range(new_first, new_last))
|
|
||||||
end
|
|
||||||
|
|
||||||
def shift_back(query, _site) do
|
|
||||||
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
|
|
||||||
new_first = query.date_range.first |> Timex.shift(days: diff)
|
|
||||||
new_last = query.date_range.last |> Timex.shift(days: diff)
|
|
||||||
Map.put(query, :date_range, Date.range(new_first, new_last))
|
|
||||||
end
|
|
||||||
|
|
||||||
def from(site, %{"period" => "realtime"} = params) do
|
def from(site, %{"period" => "realtime"} = params) do
|
||||||
date = today(site.timezone)
|
date = today(site.timezone)
|
||||||
|
@ -4,6 +4,10 @@ defmodule Plausible.Stats.Timeseries do
|
|||||||
import Plausible.Stats.{Base, Util}
|
import Plausible.Stats.{Base, Util}
|
||||||
use Plausible.Stats.Fragments
|
use Plausible.Stats.Fragments
|
||||||
|
|
||||||
|
@typep metric :: :pageviews | :visitors | :visits | :bounce_rate | :visit_duration
|
||||||
|
@typep value :: nil | integer() | float()
|
||||||
|
@type results :: nonempty_list(%{required(:date) => Date.t(), required(metric()) => value()})
|
||||||
|
|
||||||
@event_metrics [:visitors, :pageviews]
|
@event_metrics [:visitors, :pageviews]
|
||||||
@session_metrics [:visits, :bounce_rate, :visit_duration, :views_per_visit]
|
@session_metrics [:visits, :bounce_rate, :visit_duration, :views_per_visit]
|
||||||
def timeseries(site, query, metrics) do
|
def timeseries(site, query, metrics) do
|
||||||
|
@ -20,7 +20,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
|
|||||||
{:ok, metrics} <- parse_and_validate_metrics(params, nil, query) do
|
{:ok, metrics} <- parse_and_validate_metrics(params, nil, query) do
|
||||||
results =
|
results =
|
||||||
if params["compare"] == "previous_period" do
|
if params["compare"] == "previous_period" do
|
||||||
prev_query = Query.shift_back(query, site)
|
{:ok, prev_query} = Plausible.Stats.Comparisons.compare(site, query, "previous_period")
|
||||||
|
|
||||||
[prev_result, curr_result] =
|
[prev_result, curr_result] =
|
||||||
Plausible.ClickhouseRepo.parallel_tasks([
|
Plausible.ClickhouseRepo.parallel_tasks([
|
||||||
|
@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
use Plug.ErrorHandler
|
use Plug.ErrorHandler
|
||||||
alias Plausible.Stats
|
alias Plausible.Stats
|
||||||
alias Plausible.Stats.{Query, Filters}
|
alias Plausible.Stats.{Query, Filters, Comparisons}
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@ -115,9 +115,9 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
full_intervals = build_full_intervals(query, labels)
|
full_intervals = build_full_intervals(query, labels)
|
||||||
|
|
||||||
comparison_result =
|
comparison_result =
|
||||||
if params["comparison"] do
|
case Comparisons.compare(site, query, params["comparison"]) do
|
||||||
comparison_query = Query.shift_back(query, site)
|
{:ok, comparison_query} -> Stats.timeseries(site, comparison_query, [selected_metric])
|
||||||
Stats.timeseries(site, comparison_query, [selected_metric])
|
{:error, :not_supported} -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
@ -174,9 +174,10 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
with :ok <- validate_params(params) do
|
with :ok <- validate_params(params) do
|
||||||
|
comparison_mode = params["comparison"] || "previous_period"
|
||||||
query = Query.from(site, params) |> Filters.add_prefix()
|
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, %{
|
json(conn, %{
|
||||||
top_stats: top_stats,
|
top_stats: top_stats,
|
||||||
@ -243,7 +244,8 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
defp fetch_top_stats(
|
defp fetch_top_stats(
|
||||||
site,
|
site,
|
||||||
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query
|
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query,
|
||||||
|
_comparison_mode
|
||||||
) do
|
) do
|
||||||
query_30m = %Query{query | period: "30m"}
|
query_30m = %Query{query | period: "30m"}
|
||||||
|
|
||||||
@ -270,7 +272,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
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"}
|
query_30m = %Query{query | period: "30m"}
|
||||||
|
|
||||||
%{
|
%{
|
||||||
@ -296,29 +298,41 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
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])
|
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])
|
||||||
|
|
||||||
|
{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}
|
visitors: %{value: unique_visitors}
|
||||||
} = Stats.aggregate(site, total_q, [:visitors])
|
} = Stats.aggregate(site, total_q, [:visitors])
|
||||||
|
|
||||||
%{
|
|
||||||
visitors: %{value: prev_unique_visitors}
|
|
||||||
} = Stats.aggregate(site, prev_total_query, [:visitors])
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
visitors: %{value: converted_visitors},
|
visitors: %{value: converted_visitors},
|
||||||
events: %{value: completions}
|
events: %{value: completions}
|
||||||
} = Stats.aggregate(site, query, [:visitors, :events])
|
} = 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)
|
conversion_rate = calculate_cr(unique_visitors, converted_visitors)
|
||||||
prev_conversion_rate = calculate_cr(prev_unique_visitors, prev_converted_visitors)
|
prev_conversion_rate = calculate_cr(prev_unique_visitors, prev_converted_visitors)
|
||||||
|
|
||||||
@ -348,9 +362,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_top_stats(site, query) do
|
defp fetch_top_stats(site, query, comparison_mode) do
|
||||||
prev_query = Query.shift_back(query, site)
|
|
||||||
|
|
||||||
metrics =
|
metrics =
|
||||||
if query.filters["event:page"] do
|
if query.filters["event:page"] do
|
||||||
[
|
[
|
||||||
@ -375,7 +387,12 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
current_results = Stats.aggregate(site, query, metrics)
|
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 =
|
stats =
|
||||||
[
|
[
|
||||||
@ -394,11 +411,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
defp top_stats_entry(current_results, prev_results, name, key) do
|
defp top_stats_entry(current_results, prev_results, name, key) do
|
||||||
if current_results[key] do
|
if current_results[key] do
|
||||||
%{
|
value = get_in(current_results, [key, :value])
|
||||||
name: name,
|
prev_value = get_in(prev_results, [key, :value])
|
||||||
value: current_results[key][:value],
|
change = prev_value && calculate_change(key, prev_value, value)
|
||||||
change: calculate_change(key, prev_results[key][:value], current_results[key][:value])
|
|
||||||
}
|
%{name: name, value: value, change: change}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ defmodule Plausible.Workers.SendEmailReport do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp send_report(email, site, name, unsubscribe_link, query) do
|
defp send_report(email, site, name, unsubscribe_link, query) do
|
||||||
prev_query = Query.shift_back(query, site)
|
{:ok, prev_query} = Stats.Comparisons.compare(site, query, "previous_period")
|
||||||
curr_period = Stats.aggregate(site, query, [:pageviews, :visitors, :bounce_rate])
|
curr_period = Stats.aggregate(site, query, [:pageviews, :visitors, :bounce_rate])
|
||||||
prev_period = Stats.aggregate(site, prev_query, [:pageviews, :visitors, :bounce_rate])
|
prev_period = Stats.aggregate(site, prev_query, [:pageviews, :visitors, :bounce_rate])
|
||||||
|
|
||||||
|
131
test/plausible/stats/comparisons_test.exs
Normal file
131
test/plausible/stats/comparisons_test.exs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
defmodule Plausible.Stats.ComparisonsTest do
|
||||||
|
use Plausible.DataCase
|
||||||
|
alias Plausible.Stats.{Query, Comparisons}
|
||||||
|
|
||||||
|
describe "this month" do
|
||||||
|
test "shifts back this month period" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
|
||||||
|
now = ~N[2023-03-02 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2023-02-27]
|
||||||
|
assert comparison.date_range.last == ~D[2023-02-28]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back this month period when it's the first day of the month" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2023-02-28]
|
||||||
|
assert comparison.date_range.last == ~D[2023-02-28]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "previous month" do
|
||||||
|
test "shifts back using the same number of days when previous_period" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2023-01-04]
|
||||||
|
assert comparison.date_range.last == ~D[2023-01-31]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back the full month when year_over_year" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-02-01]
|
||||||
|
assert comparison.date_range.last == ~D[2022-02-28]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back whole month plus one day when year_over_year and a leap year" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2019-02-01]
|
||||||
|
assert comparison.date_range.last == ~D[2019-03-01]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "year to date" do
|
||||||
|
test "shifts back by the same number of days when previous_period" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-11-02]
|
||||||
|
assert comparison.date_range.last == ~D[2022-12-31]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back by the same number of days when year_over_year" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
||||||
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-01-01]
|
||||||
|
assert comparison.date_range.last == ~D[2022-03-01]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "previous year" do
|
||||||
|
test "shifts back a whole year when year_over_year" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year")
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2021-01-01]
|
||||||
|
assert comparison.date_range.last == ~D[2021-12-31]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back a whole year when previous_period" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period")
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2021-01-01]
|
||||||
|
assert comparison.date_range.last == ~D[2021-12-31]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "custom" do
|
||||||
|
test "shifts back by the same number of days when previous_period" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period")
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-12-25]
|
||||||
|
assert comparison.date_range.last == ~D[2022-12-31]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shifts back to last year when year_over_year" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year")
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-01-01]
|
||||||
|
assert comparison.date_range.last == ~D[2022-01-07]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -591,4 +591,65 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /api/stats/main-graph - comparisons" do
|
||||||
|
setup [:create_user, :log_in, :create_new_site, :add_imported_data]
|
||||||
|
|
||||||
|
test "returns past month stats when period=30d and comparison=previous_period", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
conn =
|
||||||
|
get(conn, "/api/stats/#{site.domain}/main-graph?period=30d&comparison=previous_period")
|
||||||
|
|
||||||
|
assert %{"labels" => labels, "comparison_labels" => comparison_labels} =
|
||||||
|
json_response(conn, 200)
|
||||||
|
|
||||||
|
{:ok, first} = Timex.today() |> Timex.shift(days: -30) |> Timex.format("{ISOdate}")
|
||||||
|
{:ok, last} = Timex.today() |> Timex.format("{ISOdate}")
|
||||||
|
|
||||||
|
assert List.first(labels) == first
|
||||||
|
assert List.last(labels) == last
|
||||||
|
|
||||||
|
{:ok, first} = Timex.today() |> Timex.shift(days: -61) |> Timex.format("{ISOdate}")
|
||||||
|
{:ok, last} = Timex.today() |> Timex.shift(days: -31) |> Timex.format("{ISOdate}")
|
||||||
|
|
||||||
|
assert List.first(comparison_labels) == first
|
||||||
|
assert List.last(comparison_labels) == last
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns past year stats when period=month and comparison=year_over_year", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
populate_stats(site, [
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-01 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-05 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-30 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-31 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2019-01-01 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2019-01-01 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2019-01-05 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2019-01-05 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2019-01-31 00:00:00])
|
||||||
|
])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(
|
||||||
|
conn,
|
||||||
|
"/api/stats/#{site.domain}/main-graph?period=month&date=2020-01-01&comparison=year_over_year"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert %{"plot" => plot, "comparison_plot" => comparison_plot} = json_response(conn, 200)
|
||||||
|
|
||||||
|
assert 1 == Enum.at(plot, 0)
|
||||||
|
assert 2 == Enum.at(comparison_plot, 0)
|
||||||
|
|
||||||
|
assert 1 == Enum.at(plot, 4)
|
||||||
|
assert 2 == Enum.at(comparison_plot, 4)
|
||||||
|
|
||||||
|
assert 1 == Enum.at(plot, 30)
|
||||||
|
assert 1 == Enum.at(comparison_plot, 30)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user