diff --git a/assets/js/types/query-api.d.ts b/assets/js/types/query-api.d.ts index e27091b8f473..f244fba28eee 100644 --- a/assets/js/types/query-api.d.ts +++ b/assets/js/types/query-api.d.ts @@ -213,11 +213,8 @@ export interface QueryApiSchema { match_day_of_week?: boolean; /** * If custom period. A list of two ISO8601 dates or timestamps to compare against. - * - * @minItems 2 - * @maxItems 2 */ - date_range: [string, string]; + date_range: DateTimeRange | DateRange; }; }; pagination?: { diff --git a/lib/plausible/stats/comparisons.ex b/lib/plausible/stats/comparisons.ex index 1338a2181956..8dede383493e 100644 --- a/lib/plausible/stats/comparisons.ex +++ b/lib/plausible/stats/comparisons.ex @@ -128,7 +128,7 @@ defmodule Plausible.Stats.Comparisons do end defp get_comparison_date_range(source_query, %{mode: "custom"} = options) do - DateTimeRange.to_date_range(options.date_range, source_query.timezone) + DateTimeRange.new!(options.date_range.first, options.date_range.last, source_query.timezone) end defp maybe_match_day_of_week(comparison_date_range, source_date_range, options) do diff --git a/lib/plausible/stats/datetime_range.ex b/lib/plausible/stats/datetime_range.ex index 234222f7a88a..b2fc6756bdce 100644 --- a/lib/plausible/stats/datetime_range.ex +++ b/lib/plausible/stats/datetime_range.ex @@ -38,6 +38,10 @@ defmodule Plausible.Stats.DateTimeRange do new!(first, last) end + def new!(%DateTime{} = first, %DateTime{} = last, _timezone) do + new!(first, last) + end + def new!(%DateTime{} = first, %DateTime{} = last) do first = DateTime.truncate(first, :second) last = DateTime.truncate(last, :second) diff --git a/lib/plausible/stats/filters/query_parser.ex b/lib/plausible/stats/filters/query_parser.ex index 9cb6e257faeb..1f9c75cdab41 100644 --- a/lib/plausible/stats/filters/query_parser.ex +++ b/lib/plausible/stats/filters/query_parser.ex @@ -378,7 +378,7 @@ defmodule Plausible.Stats.Filters.QueryParser do end defp update_comparisons_date_range(%{comparisons: %{date_range: date_range}} = include, site) do - with {:ok, parsed_date_range} <- parse_date_range_pair(site, date_range) do + with {:ok, parsed_date_range} <- parse_time_range(site, date_range, nil, nil) do {:ok, put_in(include, [:comparisons, :date_range], parsed_date_range)} end end diff --git a/priv/json-schemas/query-api-schema.json b/priv/json-schemas/query-api-schema.json index d35739e5f1f3..7ec2e5187e8d 100644 --- a/priv/json-schemas/query-api-schema.json +++ b/priv/json-schemas/query-api-schema.json @@ -98,16 +98,11 @@ "description": "If set and using time:day dimensions, day-of-week of comparison query is matched" }, "date_range": { - "type": "array", - "additionalItems": false, - "minItems": 2, - "maxItems": 2, - "items": { - "type": "string", - "format": "date" - }, - "description": "If custom period. A list of two ISO8601 dates or timestamps to compare against.", - "examples": [["2024-01-01", "2024-01-31"]] + "oneOf": [ + { "$ref": "#/definitions/date_time_range" }, + { "$ref": "#/definitions/date_range" } + ], + "description": "If custom period. A list of two ISO8601 dates or timestamps to compare against." } }, "required": ["mode", "date_range"], diff --git a/test/plausible/stats/query_result_test.exs b/test/plausible/stats/query_result_test.exs index 6fc484120551..5b369b66bb8e 100644 --- a/test/plausible/stats/query_result_test.exs +++ b/test/plausible/stats/query_result_test.exs @@ -51,8 +51,8 @@ defmodule Plausible.Stats.QueryResultTest do "pageviews" ], "date_range": [ - "2024-01-01T00:00:00+00:00", - "2024-02-01T23:59:59+00:00" + "2024-01-01T00:00:00Z", + "2024-02-01T23:59:59Z" ], "filters": [], "dimensions": [], diff --git a/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs b/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs index 3ceab8825264..c0cedde41780 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_sites_crud_api_test.exs @@ -736,7 +736,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do assert_matches ^strict_map(%{ "domain" => "new.example.com", - "timezone" => "UTC", + "timezone" => "Etc/UTC", "custom_properties" => [], "tracker_script_configuration" => ^strict_map(%{ @@ -772,7 +772,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do assert_matches ^strict_map(%{ "domain" => ^site.domain, - "timezone" => "UTC", + "timezone" => "Etc/UTC", "custom_properties" => [], "tracker_script_configuration" => ^strict_map(%{ @@ -807,7 +807,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do assert_matches ^strict_map(%{ "domain" => "new.example.com", - "timezone" => "UTC", + "timezone" => "Etc/UTC", "custom_properties" => [], "tracker_script_configuration" => ^strict_map(%{ diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs index a353d26e4cd0..91c51ce4ab17 100644 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ b/test/plausible_web/controllers/api/external_sites_controller_test.exs @@ -1961,7 +1961,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do assert json_response(conn, 200) == %{ "domain" => "new.example.com", - "timezone" => "UTC", + "timezone" => "Etc/UTC", "custom_properties" => [] } diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs index 3720789b6124..830c94df5ffa 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_comparisons_test.exs @@ -1,5 +1,6 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do use PlausibleWeb.ConnCase + import Plausible.Teams.Test setup [:create_user, :create_site, :create_api_key, :use_api_key, :create_site_import] @@ -377,4 +378,99 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryComparisonsTest do } ] end + + describe "custom comparison range" do + test "can use date range for custom comparison", %{conn: conn, site: site} do + populate_stats(site, [ + build(:pageview, timestamp: ~N[2021-01-01 00:00:00]), + build(:pageview, timestamp: ~N[2021-01-01 00:25:00]), + build(:pageview, timestamp: ~N[2021-01-07 00:00:00]) + ]) + + conn = + post(conn, "/api/v2/query-internal-test", %{ + "site_id" => site.domain, + "metrics" => ["pageviews"], + "date_range" => ["2021-01-07", "2021-01-13"], + "include" => %{ + "comparisons" => %{"mode" => "custom", "date_range" => ["2021-01-01", "2021-01-06"]} + } + }) + + assert json_response(conn, 200)["results"] == [ + %{ + "dimensions" => [], + "metrics" => [1], + "comparison" => %{"change" => [-50], "dimensions" => [], "metrics" => [2]} + } + ] + end + + test "can use datetime range for custom comparison", %{conn: conn, site: site} do + populate_stats(site, [ + build(:pageview, timestamp: ~N[2021-01-01 01:00:00]), + build(:pageview, user_id: 1, timestamp: ~N[2021-01-01 05:25:00]), + build(:pageview, user_id: 1, timestamp: ~N[2021-01-01 05:26:00]), + build(:pageview, timestamp: ~N[2021-01-02 04:00:00]), + build(:pageview, timestamp: ~N[2021-01-03 02:00:00]) + ]) + + conn = + post(conn, "/api/v2/query-internal-test", %{ + "site_id" => site.domain, + "metrics" => ["visitors", "pageviews"], + "date_range" => ["2021-01-02T03:00:00Z", "2021-01-03T02:59:59Z"], + "include" => %{ + "comparisons" => %{ + "mode" => "custom", + "date_range" => ["2021-01-01T03:00:00Z", "2021-01-02T02:59:59Z"] + } + } + }) + + assert json_response(conn, 200)["results"] == [ + %{ + "dimensions" => [], + "metrics" => [2, 2], + "comparison" => %{"change" => [100, 0], "dimensions" => [], "metrics" => [1, 2]} + } + ] + end + + test "custom datetime range comparison handles timezones correctly", %{conn: conn, user: user} do + weird_tz_site = new_site(owner: user, timezone: "America/Havana") + + populate_stats(weird_tz_site, [ + # 03:00 America/Havana + build(:pageview, timestamp: ~N[2021-01-01 08:00:00]), + # 05:25 America/Havana + build(:pageview, timestamp: ~N[2021-01-01 10:25:00]), + # 04:00 America/Havana + build(:pageview, timestamp: ~N[2021-01-02 09:00:00]), + # 02:00 America/Havana + build(:pageview, timestamp: ~N[2021-01-03 08:00:00]) + ]) + + conn = + post(conn, "/api/v2/query-internal-test", %{ + "site_id" => weird_tz_site.domain, + "metrics" => ["pageviews"], + "date_range" => ["2021-01-02T03:00:00Z", "2021-01-03T02:59:59Z"], + "include" => %{ + "comparisons" => %{ + "mode" => "custom", + "date_range" => ["2021-01-01T03:00:00Z", "2021-01-02T02:59:59Z"] + } + } + }) + + assert json_response(conn, 200)["results"] == [ + %{ + "dimensions" => [], + "metrics" => [1], + "comparison" => %{"change" => [-50], "dimensions" => [], "metrics" => [2]} + } + ] + end + end end diff --git a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs index 7064d835dd1d..998e7e5e4f50 100644 --- a/test/plausible_web/controllers/api/external_stats_controller/query_test.exs +++ b/test/plausible_web/controllers/api/external_stats_controller/query_test.exs @@ -1615,8 +1615,8 @@ defmodule PlausibleWeb.Api.ExternalStatsController.QueryTest do ] assert json_response(conn, 200)["query"]["date_range"] == [ - "2021-01-01T00:00:00+00:00", - "2021-01-15T23:59:59+00:00" + "2021-01-01T00:00:00Z", + "2021-01-15T23:59:59Z" ] end diff --git a/test/support/factory.ex b/test/support/factory.ex index 52055b455a38..55366971b684 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -82,7 +82,7 @@ defmodule Plausible.Factory do site = %Plausible.Site{ native_stats_start_at: ~N[2000-01-01 00:00:00], domain: domain, - timezone: "UTC" + timezone: "Etc/UTC" } merge_attributes(site, attrs)