From 428ecb16dd39aa2f2e95bc483d02efd4d128ea33 Mon Sep 17 00:00:00 2001 From: Alexander Spicer Date: Fri, 2 Aug 2024 16:33:45 -0700 Subject: [PATCH] retention timezones --- .../scenes/retention/retentionTableLogic.ts | 8 +- .../insights/retention_query_runner.py | 16 ++-- .../test/test_retention_query_runner.py | 94 +++++++++++++++++++ 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/frontend/src/scenes/retention/retentionTableLogic.ts b/frontend/src/scenes/retention/retentionTableLogic.ts index 3fd153edebcc6..0969d1ffa6f53 100644 --- a/frontend/src/scenes/retention/retentionTableLogic.ts +++ b/frontend/src/scenes/retention/retentionTableLogic.ts @@ -81,19 +81,19 @@ export const retentionTableLogic = kea([ } else { switch (period) { case 'Hour': - firstColumn = dayjs(currentResult.date).format('MMM D, h A') + firstColumn = dayjs.utc(currentResult.date).format('MMM D, h A') break case 'Month': - firstColumn = dayjs(currentResult.date).format('MMM YYYY') + firstColumn = dayjs.utc(currentResult.date).format('MMM YYYY') break case 'Week': { - const startDate = dayjs(currentResult.date) + const startDate = dayjs.utc(currentResult.date) const endDate = startDate.add(6, 'day') // To show last day of the week we add 6 days, not 7 firstColumn = `${startDate.format('MMM D')} to ${endDate.format('MMM D')}` break } default: - firstColumn = dayjs(currentResult.date).format('MMM D') + firstColumn = dayjs.utc(currentResult.date).format('MMM D') } } diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index d4fcf9d4ee9be..7df621e5241bc 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -368,6 +368,14 @@ def _refresh_frequency(self): return refresh_frequency + def get_date(self, first_interval): + date = self.query_date_range.date_from() + self.query_date_range.determine_time_delta( + first_interval, self.query_date_range.interval_name.title() + ) + if self.query_date_range.interval_type == IntervalType.HOUR: + date = date + self.team.timezone_info.utcoffset(date) + return date + def calculate(self) -> RetentionQueryResponse: query = self.to_query() hogql = to_printed_hogql(query, self.team) @@ -388,7 +396,6 @@ def calculate(self) -> RetentionQueryResponse: } for (breakdown_values, intervals_from_base, count) in response.results } - results = [ { "values": [ @@ -396,12 +403,7 @@ def calculate(self) -> RetentionQueryResponse: for return_interval in range(self.query_date_range.total_intervals - first_interval) ], "label": f"{self.query_date_range.interval_name.title()} {first_interval}", - "date": ( - self.query_date_range.date_from() - + self.query_date_range.determine_time_delta( - first_interval, self.query_date_range.interval_name.title() - ) - ), + "date": self.get_date(first_interval), } for first_interval in range(self.query_date_range.total_intervals) ] diff --git a/posthog/hogql_queries/insights/test/test_retention_query_runner.py b/posthog/hogql_queries/insights/test/test_retention_query_runner.py index 99700be28a414..7e096d7a9ab64 100644 --- a/posthog/hogql_queries/insights/test/test_retention_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_retention_query_runner.py @@ -660,6 +660,100 @@ def test_hour_interval(self): ], ) + def test_hour_interval_team_timezone(self): + self.team.timezone = "US/Pacific" + self.team.save() + + _create_person( + team=self.team, + distinct_ids=["person1", "alias1"], + properties={"email": "person1@test.com"}, + ) + _create_person( + team=self.team, + distinct_ids=["person2"], + properties={"email": "person2@test.com"}, + ) + + _create_events( + self.team, + [ + ("person1", _date(day=0, hour=6)), + ("person2", _date(day=0, hour=6)), + ("person1", _date(day=0, hour=7)), + ("person2", _date(day=0, hour=7)), + ("person1", _date(day=0, hour=8)), + ("person2", _date(day=0, hour=8)), + ("person1", _date(day=0, hour=10)), + ("person1", _date(day=0, hour=11)), + ("person2", _date(day=0, hour=11)), + ("person2", _date(day=0, hour=12)), + ("person1", _date(day=0, hour=14)), + ("person2", _date(day=0, hour=16)), + ], + ) + + result = self.run_query( + query={ + "dateRange": {"date_to": _date(0, hour=16, minute=13)}, + "retentionFilter": { + "period": "Hour", + "totalIntervals": 11, + }, + } + ) + + self.assertEqual( + pluck(result, "label"), + [ + "Hour 0", + "Hour 1", + "Hour 2", + "Hour 3", + "Hour 4", + "Hour 5", + "Hour 6", + "Hour 7", + "Hour 8", + "Hour 9", + "Hour 10", + ], + ) + + self.assertEqual( + pluck(result, "values", "count"), + [ + [2, 2, 2, 0, 1, 2, 1, 0, 1, 0, 1], + [2, 2, 0, 1, 2, 1, 0, 1, 0, 1], + [2, 0, 1, 2, 1, 0, 1, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 1, 0, 0], + [2, 1, 0, 1, 0, 1], + [1, 0, 0, 0, 1], + [0, 0, 0, 0], + [1, 0, 0], + [0, 0], + [1], + ], + ) + + self.assertEqual( + pluck(result, "date"), + [ + datetime(2020, 6, 10, 6, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 7, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 8, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 9, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 10, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 11, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 12, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 13, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 14, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 15, tzinfo=ZoneInfo("UTC")), + datetime(2020, 6, 10, 16, tzinfo=ZoneInfo("UTC")), + ], + ) + # ensure that the first interval is properly rounded according to the specified period def test_interval_rounding(self): _create_person(