Skip to content

Commit efe996d

Browse files
authored
Make comparisons to today consider only stats in the same exact time range (#6205)
* Make comparisons to today consider only stats in the same exact time range * Update tests to account for date range behaviour adjustment * Update CHANGELOG.md * Refactor `get_comparison_datetime_range` (h/t @RobertJoonas)
1 parent ae00c3c commit efe996d

10 files changed

Lines changed: 72 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
1313
- Adds team_id to query debug metadata (saved in system.query_log log_comment column)
1414
- Add "Unknown" option to Countries shield, for when the country code is unrecognized
1515
- Add "Last 24 Hours" to dashboard time range picker and Stats API v2
16+
- Always compare against the same time range in comparisons with "Today"
1617

1718
### Removed
1819

lib/plausible/stats/comparisons.ex

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ defmodule Plausible.Stats.Comparisons do
88
"""
99

1010
alias Plausible.Stats
11-
alias Plausible.Stats.{Query, DateTimeRange, Time}
11+
alias Plausible.Stats.{DateTimeRange, Query, Time}
12+
alias Plausible.Times
1213

1314
@spec get_comparison_utc_time_range(Stats.Query.t()) :: DateTimeRange.t()
1415
@doc """
@@ -53,8 +54,8 @@ defmodule Plausible.Stats.Comparisons do
5354
DateTimeRange.new!(from, to)
5455

5556
_ ->
56-
# For 24h period, work directly with datetime ranges to preserve time precision
57-
if source_query.input_date_range == :"24h" do
57+
# For 24h period and today, work directly with datetime ranges to preserve time precision
58+
if use_datetime_for_comparison?(source_query) do
5859
get_comparison_datetime_range(source_query)
5960
else
6061
comparison_date_range = get_comparison_date_range(source_query)
@@ -70,6 +71,17 @@ defmodule Plausible.Stats.Comparisons do
7071
DateTimeRange.to_timezone(datetime_range, "Etc/UTC")
7172
end
7273

74+
defp use_datetime_for_comparison?(query) do
75+
if query.input_date_range == :day do
76+
today_from_now = Times.to_date(query.now, query.timezone)
77+
day_from_range = Times.to_date(query.utc_time_range.first, query.timezone)
78+
79+
Date.compare(today_from_now, day_from_range) == :eq
80+
else
81+
query.input_date_range == :"24h"
82+
end
83+
end
84+
7385
def get_comparison_query(
7486
%Query{comparison_utc_time_range: %DateTimeRange{} = comparison_range} = source_query
7587
) do
@@ -120,38 +132,34 @@ defmodule Plausible.Stats.Comparisons do
120132
end
121133
end
122134

123-
# For 24h period, shift the datetime range directly to preserve time precision
135+
# For 24h and today periods, shift the datetime range directly to preserve time precision
124136
defp get_comparison_datetime_range(
125137
%Query{
126-
input_date_range: :"24h",
127-
include: %{compare: :previous_period, compare_match_day_of_week: true}
138+
input_date_range: input_range,
139+
include: %{compare: :previous_period} = include
128140
} = source_query
129-
) do
130-
days_back = 7
131-
comparison_start = DateTime.shift(source_query.utc_time_range.first, day: -days_back)
132-
comparison_end = DateTime.shift(source_query.utc_time_range.last, day: -days_back)
133-
134-
DateTimeRange.new!(comparison_start, comparison_end)
135-
end
141+
)
142+
when input_range in [:"24h", :day] do
143+
offset =
144+
if include.compare_match_day_of_week do
145+
[day: -7]
146+
else
147+
[hour: -24]
148+
end
136149

137-
defp get_comparison_datetime_range(
138-
%Query{
139-
input_date_range: :"24h",
140-
include: %{compare: :previous_period}
141-
} = source_query
142-
) do
143-
comparison_start = DateTime.shift(source_query.utc_time_range.first, hour: -24)
144-
comparison_end = DateTime.shift(source_query.utc_time_range.last, hour: -24)
150+
comparison_start = DateTime.shift(source_query.utc_time_range.first, offset)
151+
comparison_end = DateTime.shift(source_query.utc_time_range.last, offset)
145152

146153
DateTimeRange.new!(comparison_start, comparison_end)
147154
end
148155

149156
defp get_comparison_datetime_range(
150157
%Query{
151-
input_date_range: :"24h",
158+
input_date_range: input_range,
152159
include: %{compare: :year_over_year}
153160
} = source_query
154-
) do
161+
)
162+
when input_range in [:"24h", :day] do
155163
comparison_start = DateTime.shift(source_query.utc_time_range.first, year: -1)
156164
comparison_end = DateTime.shift(source_query.utc_time_range.last, year: -1)
157165

lib/plausible/stats/datetime_range.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,26 @@ defmodule Plausible.Stats.DateTimeRange do
2020
will become the last date at 23:59:59. Both dates will be turned into `%DateTime{}`
2121
structs in the given timezone.
2222
"""
23-
def new!(%Date{} = first, %Date{} = last, timezone) do
23+
def new!(%Date{} = first, last, timezone) do
2424
first =
2525
case DateTime.new(first, ~T[00:00:00], timezone) do
2626
{:ok, datetime} -> datetime
2727
{:gap, _just_before, just_after} -> just_after
2828
{:ambiguous, _first_datetime, second_datetime} -> second_datetime
2929
end
3030

31+
new!(first, last, timezone)
32+
end
33+
34+
def new!(first, %Date{} = last, timezone) do
3135
last =
3236
case DateTime.new(last, ~T[23:59:59], timezone) do
3337
{:ok, datetime} -> datetime
3438
{:gap, just_before, _just_after} -> just_before
3539
{:ambiguous, first_datetime, _second_datetime} -> first_datetime
3640
end
3741

38-
new!(first, last)
42+
new!(first, last, timezone)
3943
end
4044

4145
def new!(%DateTime{} = first, %DateTime{} = last, _timezone) do

lib/plausible/stats/legacy/legacy_query_builder.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
99
use Plausible
1010

1111
alias Plausible.Stats.{Filters, Interval, Query, ApiQueryParser, QueryBuilder, DateTimeRange}
12+
alias Plausible.Times
1213

1314
def from(site, params, debug_metadata, now \\ nil) do
1415
now = now || DateTime.utc_now(:second)
@@ -99,11 +100,16 @@ defmodule Plausible.Stats.Legacy.QueryBuilder do
99100
struct!(query, input_date_range: input_date_range_atom, utc_time_range: datetime_range)
100101
end
101102

102-
defp put_input_date_range(query, site, %{"period" => "day"} = params) do
103+
defp put_input_date_range(%Query{now: now} = query, site, %{"period" => "day"} = params) do
103104
date = parse_single_date(query, params)
104105

105106
datetime_range =
106-
DateTimeRange.new!(date, date, site.timezone) |> DateTimeRange.to_timezone("Etc/UTC")
107+
if Date.compare(Times.to_date(now, site.timezone), date) == :eq do
108+
DateTimeRange.new!(date, now, site.timezone)
109+
else
110+
DateTimeRange.new!(date, date, site.timezone)
111+
end
112+
|> DateTimeRange.to_timezone("Etc/UTC")
107113

108114
struct!(query, input_date_range: :day, utc_time_range: datetime_range)
109115
end

lib/plausible/stats/query_builder.ex

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ defmodule Plausible.Stats.QueryBuilder do
1818
QueryInclude
1919
}
2020

21+
alias Plausible.Times
22+
2123
@doc """
2224
Runs various validations and builds a `%Query{}` from already parsed params.
2325
@@ -96,8 +98,12 @@ defmodule Plausible.Stats.QueryBuilder do
9698
DateTimeRange.new!(first_datetime, last_datetime)
9799
end
98100

99-
defp build_datetime_range(:day, site, relative_date, _now) do
100-
DateTimeRange.new!(relative_date, relative_date, site.timezone)
101+
defp build_datetime_range(:day, site, relative_date, now) do
102+
if Date.compare(Times.to_date(now, site.timezone), relative_date) == :eq do
103+
DateTimeRange.new!(relative_date, now, site.timezone)
104+
else
105+
DateTimeRange.new!(relative_date, relative_date, site.timezone)
106+
end
101107
end
102108

103109
defp build_datetime_range(:"24h", _site, _relative_date, now) do
@@ -153,7 +159,7 @@ defmodule Plausible.Stats.QueryBuilder do
153159
dimensions: dimensions
154160
} = parsed_query_params
155161

156-
relative_date = relative_date || today_from_now(site, now)
162+
relative_date = relative_date || Times.to_date(now, site.timezone)
157163

158164
utc_time_range =
159165
input_date_range
@@ -560,10 +566,6 @@ defmodule Plausible.Stats.QueryBuilder do
560566
end
561567
end
562568

563-
defp today_from_now(site, now) do
564-
now |> DateTime.shift_zone!(site.timezone) |> DateTime.to_date()
565-
end
566-
567569
defp i(value), do: inspect(value, charlists: :as_lists)
568570

569571
defp validate_list(list, parser_function) do

lib/plausible/times.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ defmodule Plausible.Times do
6262
Timex.end_of_year(t)
6363
end
6464

65+
@spec to_date(DateTime.t(), String.t()) :: Date.t()
66+
def to_date(%DateTime{} = dt, tz) do
67+
dt
68+
|> DateTime.shift_zone!(tz)
69+
|> DateTime.to_date()
70+
end
71+
6572
@spec humanize(DateTime.t()) :: String.t()
6673
def humanize(%DateTime{} = dt) do
6774
Timex.Format.DateTime.Formatters.Relative.format!(dt, "{relative}")

test/plausible/stats/query/query_from_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ defmodule Plausible.Stats.Query.QueryFromTest do
4747
q = Query.from(site, %{"period" => "day"}, now: @now)
4848

4949
assert q.utc_time_range.first == ~U[2024-05-03 04:00:00Z]
50-
assert q.utc_time_range.last == ~U[2024-05-04 03:59:59Z]
50+
assert q.utc_time_range.last == @now
5151
assert q.interval == "hour"
5252
end
5353

test/plausible/stats/query/query_parse_and_build_test.exs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ defmodule Plausible.Stats.Query.QueryParseAndBuildTest do
55
alias Plausible.Stats.{Query, DateTimeRange, Filters, QueryError}
66

77
@now DateTime.new!(~D[2021-05-05], ~T[12:30:00], "Etc/UTC")
8+
@date_range_today %DateTimeRange{
9+
first: DateTime.new!(~D[2021-05-05], ~T[00:00:00], "Etc/UTC"),
10+
last: @now
11+
}
812
@date_range_day %DateTimeRange{
913
first: DateTime.new!(~D[2021-05-05], ~T[00:00:00], "Etc/UTC"),
1014
last: DateTime.new!(~D[2021-05-05], ~T[23:59:59], "Etc/UTC")
@@ -1217,7 +1221,7 @@ defmodule Plausible.Stats.Query.QueryParseAndBuildTest do
12171221
end
12181222

12191223
for {shortcut, expected_date_range} <- [
1220-
{"day", @date_range_day},
1224+
{"day", @date_range_today},
12211225
{"7d", @date_range_7d},
12221226
{"10d", @date_range_10d},
12231227
{"30d", @date_range_30d},

test/plausible_web/controllers/api/external_stats_controller/query_timezone_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTimezoneTest do
4141
populate_stats(site, [
4242
build(:pageview, timestamp: ~N[2024-01-01 10:00:00]),
4343
build(:pageview, timestamp: ~N[2024-01-01 12:00:00]),
44-
build(:pageview, timestamp: ~N[2024-01-02 12:00:00])
44+
build(:pageview, timestamp: ~N[2024-01-01 15:30:00])
4545
])
4646

4747
conn =

test/plausible_web/controllers/api/stats_controller/main_graph_test.exs

Lines changed: 3 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,12 +1583,11 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
15831583
assert last_week_plot == [33.33, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
15841584
end
15851585

1586-
test "does not trim hourly relative date range when comparing", %{conn: conn, site: site} do
1586+
test "does trim hourly relative date range when comparing", %{conn: conn, site: site} do
15871587
populate_stats(site, [
15881588
build(:pageview, timestamp: ~N[2021-01-08 00:00:00]),
15891589
build(:pageview, timestamp: ~N[2021-01-08 06:05:00]),
1590-
build(:pageview, timestamp: ~N[2021-01-08 08:59:00]),
1591-
build(:pageview, timestamp: ~N[2021-01-08 23:59:00])
1590+
build(:pageview, timestamp: ~N[2021-01-08 08:04:00])
15921591
])
15931592

15941593
conn =
@@ -1608,22 +1607,7 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
16081607
"2021-01-08 05:00:00",
16091608
"2021-01-08 06:00:00",
16101609
"2021-01-08 07:00:00",
1611-
"2021-01-08 08:00:00",
1612-
"2021-01-08 09:00:00",
1613-
"2021-01-08 10:00:00",
1614-
"2021-01-08 11:00:00",
1615-
"2021-01-08 12:00:00",
1616-
"2021-01-08 13:00:00",
1617-
"2021-01-08 14:00:00",
1618-
"2021-01-08 15:00:00",
1619-
"2021-01-08 16:00:00",
1620-
"2021-01-08 17:00:00",
1621-
"2021-01-08 18:00:00",
1622-
"2021-01-08 19:00:00",
1623-
"2021-01-08 20:00:00",
1624-
"2021-01-08 21:00:00",
1625-
"2021-01-08 22:00:00",
1626-
"2021-01-08 23:00:00"
1610+
"2021-01-08 08:00:00"
16271611
],
16281612
"plot" => [
16291613
1,
@@ -1634,39 +1618,9 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
16341618
0,
16351619
1,
16361620
0,
1637-
1,
1638-
0,
1639-
0,
1640-
0,
1641-
0,
1642-
0,
1643-
0,
1644-
0,
1645-
0,
1646-
0,
1647-
0,
1648-
0,
1649-
0,
1650-
0,
1651-
0,
16521621
1
16531622
],
16541623
"comparison_plot" => [
1655-
0,
1656-
0,
1657-
0,
1658-
0,
1659-
0,
1660-
0,
1661-
0,
1662-
0,
1663-
0,
1664-
0,
1665-
0,
1666-
0,
1667-
0,
1668-
0,
1669-
0,
16701624
0,
16711625
0,
16721626
0,

0 commit comments

Comments
 (0)