Skip to content
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

Support realtime periods in API v2 #4469

Merged
merged 24 commits into from
Sep 2, 2024
Merged

Support realtime periods in API v2 #4469

merged 24 commits into from
Sep 2, 2024

Conversation

RobertJoonas
Copy link
Contributor

@RobertJoonas RobertJoonas commented Aug 27, 2024

Changes

  • Parse query date ranges into a new DateTimeRange struct, including the timezone information.
  • support realtime and 30m date range shortcuts in the private API JSON schema
  • make it possible to pass a date parameter (in the private API JSON schema) relative to which the date_range will be constructed.

Tests

  • Automated tests have been added

Changelog

  • This PR does not make a user-facing change

Documentation

  • This change does not need a documentation update

Dark mode

  • This PR does not change the UI

@RobertJoonas RobertJoonas requested a review from macobo August 27, 2024 06:21
lib/plausible/stats/naive_datetime_range.ex Outdated Show resolved Hide resolved
lib/plausible/stats/query_result.ex Outdated Show resolved Hide resolved
lib/plausible/stats/filters/query_parser.ex Outdated Show resolved Hide resolved
test/plausible/stats/query_parser_test.exs Outdated Show resolved Hide resolved
test/plausible/stats/query_parser_test.exs Outdated Show resolved Hide resolved
lib/plausible_web/controllers/api/stats_controller.ex Outdated Show resolved Hide resolved
test/plausible/stats/query_parser_test.exs Show resolved Hide resolved
lib/plausible/stats/comparisons.ex Outdated Show resolved Hide resolved
@RobertJoonas RobertJoonas marked this pull request as draft August 27, 2024 11:53
Copy link

Preview environment👷🏼‍♀️🏗️
PR-4469

@RobertJoonas RobertJoonas marked this pull request as ready for review August 30, 2024 11:55
@RobertJoonas RobertJoonas requested a review from macobo September 2, 2024 08:25
def parse(site, schema_type, params, now \\ nil) when is_map(params) do
def parse(site, schema_type, params, test_opts \\ []) when is_map(params) do
now = Keyword.get(test_opts, :now, DateTime.utc_now(:second))
date = Keyword.get(test_opts, :date, today(site))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Why do we need both date and now parameters? Isn't date = now |> to_date?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we can work with only now. d3a9035

with :ok <- JSONSchema.validate(schema_type, params),
{:ok, date} <- parse_date(site, Map.get(params, "date"), now),
now <- if(now, do: now, else: DateTime.utc_now(:second)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this if check can never fail right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in d3a9035

end

defp datetime_from_timestamp(timestamp_string) do
with [timestamp, timezone] <- String.split(timestamp_string),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid cooking our encoding scheme and use DateTime.from_iso8601 instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not. The ISO8601 format alone does not provide the timezone information which we need to create the DateTime struct.

lib/plausible/stats/filters/query_parser.ex Outdated Show resolved Hide resolved
test/plausible/stats/query_parser_test.exs Show resolved Hide resolved
test/plausible/stats/query_parser_test.exs Outdated Show resolved Hide resolved
@@ -513,6 +612,121 @@ defmodule Plausible.Stats.Filters.QueryParserTest do

%{"site_id" => site.domain, "date_range" => ["21415-00", "eee"], "metrics" => ["visitors"]}
|> check_error(site, "#/date_range: Invalid date range [\"21415-00\", \"eee\"]")

# Timestamps do not include timezone information
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's too many test cases in this test case.

Let's create separate test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Checks parsing the date range relative to the `date` parameter (which is only
# available in the internal API schema) instead of using `@now`.
def check_date_range_with_date(date_range, site, expected_date_range) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we make date an optional parameter of check_date_range instead?

E.g. `def check_date_range(date_params, site, expected_date_range, schema_type \ public)

This reduces the number of abstractions we need to deal with reading tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -79,6 +81,15 @@ defmodule Plausible.Stats.QueryResult do
|> Enum.reject(fn {_, value} -> is_nil(value) end)
|> Enum.into(%{})
end

defp to_iso_8601_with_timezone(%DateTime{time_zone: timezone} = datetime) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: would DateTime.to_iso8601(datetime, :extended) not work here? This should make the parsing more standard as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would include the UTC offset in the iso8601 string format, but not the timezone name that we need.

Copy link
Contributor

@macobo macobo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

@RobertJoonas RobertJoonas merged commit f04c47f into master Sep 2, 2024
10 checks passed
@RobertJoonas RobertJoonas deleted the realtime-in-api-v2 branch September 2, 2024 09:57
@ruslandoga ruslandoga mentioned this pull request Sep 7, 2024
4 tasks
case DateTime.new(last, ~T[23:59:59], timezone) do
{:ok, datetime} -> datetime
{:gap, just_before, _just_after} -> just_before
{:ambiguous, first_datetime, _second_datetime} -> first_datetime
Copy link
Contributor

@ruslandoga ruslandoga Sep 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: I wonder why lower values are picked for the end of the range, and higher -- for the beginning? This makes the whole range smaller: range=max(possible_timestamps)..min(possible_timestamps) - potentially leaving some events out. Why not range=min(possible_timestamps)..max(possible_timestamps)

It probably doesn't matter though, since clocks are usually moved at 02:00 or 03:00 so midnight should be safe from gaps and ambiguities.

Copy link
Contributor Author

@RobertJoonas RobertJoonas Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruslandoga, good question! The idea was to make sure that the date of the parsed datetime will always remain the same as the requested date.

One specific example: let's say the client requests a date_range of ["2022-09-11", "2022-09-11"] and their timezone is America/Santiago. The first datetime will be parsed as follows:

iex(3)> DateTime.new(~D[2022-09-11], ~T[00:00:00], "America/Santiago")
{:gap, #DateTime<2022-09-10 23:59:59.999999-04:00 -04 America/Santiago>,
 #DateTime<2022-09-11 01:00:00-03:00 -03 America/Santiago>}

If we choose the first date (i.e. "just before"), we'll end up parsing the datetime into a different date ("2022-09-10") from what was actually requested ("2022-09-11").

That means, when dealing with the date range later on in code (i.e. constructing the time labels for timeseries requests or doing comparisons), we'd run into errors, or we'd have to build some ugly exceptions to handle these cases.

That said, when allowing a flexible datetime format in the public API, where one could request a date range such that 2022-09-11 00:00:00 in "America/Santiago" is the last datetime, we'd still run into the same issue.

So perhaps the proper fix could be to always select just_before or just_after depending on which falls on the same date? \cc @macobo also bringing this to your attention as you're working on it.

Copy link
Contributor

@ruslandoga ruslandoga Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. I didn't think about conversion to dates for labels and comparisons. I assumed the filtering always happens on "full" timestamps (i.e. DateTime). And I didn't know that there were timezones with gaps at midnight! Thank you for the explanation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants