diff --git a/posthog/models/filters/test/__snapshots__/test_filter.ambr b/posthog/models/filters/test/__snapshots__/test_filter.ambr index 6f37e28782d08..4cfbea3908442 100644 --- a/posthog/models/filters/test/__snapshots__/test_filter.ambr +++ b/posthog/models/filters/test/__snapshots__/test_filter.ambr @@ -353,7 +353,7 @@ INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") WHERE ("posthog_persondistinctid"."distinct_id" = 'example_id' AND "posthog_person"."team_id" = 2 - AND ("posthog_person"."properties" -> 'created_at') > '"2021-02-06T10:00:00+00:00"') + AND "posthog_person"."id" = -1) LIMIT 1 ' --- diff --git a/posthog/models/filters/test/test_filter.py b/posthog/models/filters/test/test_filter.py index ed1f84653ff74..4dcfe35556058 100644 --- a/posthog/models/filters/test/test_filter.py +++ b/posthog/models/filters/test/test_filter.py @@ -854,9 +854,8 @@ def test_person_relative_date_parsing_with_invalid_date(self): .filter(properties_to_Q(filter.property_groups.flat)) .exists() ) - # matches '2m' - # TODO: Should this not match instead? - self.assertTrue(matched_person) + # needs an exact match + self.assertFalse(matched_person) filter = Filter( data={ diff --git a/posthog/queries/base.py b/posthog/queries/base.py index e93b5fad112bc..ac92db83ad00a 100644 --- a/posthog/queries/base.py +++ b/posthog/queries/base.py @@ -435,11 +435,16 @@ def is_truthy_or_falsy_property_value(value: Any) -> bool: def relative_date_parse_for_feature_flag_matching(value: str) -> Optional[datetime.datetime]: - regex = r"(?P[0-9]+)(?P[a-z])" + regex = r"^(?P[0-9]+)(?P[a-z])$" match = re.search(regex, value) parsed_dt = datetime.datetime.now(tz=ZoneInfo("UTC")) if match: number = int(match.group("number")) + + if number >= 10_000: + # Guard against overflow, disallow numbers greater than 10_000 + return None + interval = match.group("interval") if interval == "h": parsed_dt = parsed_dt - relativedelta(hours=number) diff --git a/posthog/queries/test/test_base.py b/posthog/queries/test/test_base.py index e856bb315800b..a3c4dd9f3a3ab 100644 --- a/posthog/queries/test/test_base.py +++ b/posthog/queries/test/test_base.py @@ -1,5 +1,6 @@ import datetime import re +import unittest from unittest.mock import patch from dateutil import parser, tz @@ -10,7 +11,7 @@ from posthog.models.filters.path_filter import PathFilter from posthog.models.property.property import Property -from posthog.queries.base import match_property, sanitize_property_key +from posthog.queries.base import match_property, relative_date_parse_for_feature_flag_matching, sanitize_property_key from posthog.test.base import APIBaseTest @@ -406,3 +407,151 @@ def test_sanitize_keys(key, expected): sanitized_key = sanitize_property_key(key) assert sanitized_key == expected + + +class TestRelativeDateParsing(unittest.TestCase): + def test_invalid_input(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1") is None + assert relative_date_parse_for_feature_flag_matching("1x") is None + assert relative_date_parse_for_feature_flag_matching("1.2y") is None + assert relative_date_parse_for_feature_flag_matching("1z") is None + assert relative_date_parse_for_feature_flag_matching("1s") is None + assert relative_date_parse_for_feature_flag_matching("123344000.134m") is None + assert relative_date_parse_for_feature_flag_matching("bazinga") is None + assert relative_date_parse_for_feature_flag_matching("000bello") is None + assert relative_date_parse_for_feature_flag_matching("000hello") is None + + assert relative_date_parse_for_feature_flag_matching("000h") is not None + assert relative_date_parse_for_feature_flag_matching("1000h") is not None + + def test_overflow(self): + assert relative_date_parse_for_feature_flag_matching("1000000h") is None + assert relative_date_parse_for_feature_flag_matching("100000000000000000y") is None + + def test_hour_parsing(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1h") == datetime.datetime( + 2020, 1, 1, 11, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2h") == datetime.datetime( + 2020, 1, 1, 10, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("24h") == datetime.datetime( + 2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("30h") == datetime.datetime( + 2019, 12, 31, 6, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("48h") == datetime.datetime( + 2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + + assert relative_date_parse_for_feature_flag_matching( + "24h" + ) == relative_date_parse_for_feature_flag_matching("1d") + assert relative_date_parse_for_feature_flag_matching( + "48h" + ) == relative_date_parse_for_feature_flag_matching("2d") + + def test_day_parsing(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1d") == datetime.datetime( + 2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2d") == datetime.datetime( + 2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("7d") == datetime.datetime( + 2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("14d") == datetime.datetime( + 2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("30d") == datetime.datetime( + 2019, 12, 2, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + + assert relative_date_parse_for_feature_flag_matching("7d") == relative_date_parse_for_feature_flag_matching( + "1w" + ) + + def test_week_parsing(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1w") == datetime.datetime( + 2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2w") == datetime.datetime( + 2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("4w") == datetime.datetime( + 2019, 12, 4, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("8w") == datetime.datetime( + 2019, 11, 6, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + + assert relative_date_parse_for_feature_flag_matching("1m") == datetime.datetime( + 2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("4w") != relative_date_parse_for_feature_flag_matching( + "1m" + ) + + def test_month_parsing(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1m") == datetime.datetime( + 2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2m") == datetime.datetime( + 2019, 11, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("4m") == datetime.datetime( + 2019, 9, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("8m") == datetime.datetime( + 2019, 5, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + + assert relative_date_parse_for_feature_flag_matching("1y") == datetime.datetime( + 2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching( + "12m" + ) == relative_date_parse_for_feature_flag_matching("1y") + + with freeze_time("2020-04-03T00:00:00"): + assert relative_date_parse_for_feature_flag_matching("1m") == datetime.datetime( + 2020, 3, 3, 0, 0, 0, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2m") == datetime.datetime( + 2020, 2, 3, 0, 0, 0, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("4m") == datetime.datetime( + 2019, 12, 3, 0, 0, 0, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("8m") == datetime.datetime( + 2019, 8, 3, 0, 0, 0, tzinfo=tz.gettz("UTC") + ) + + assert relative_date_parse_for_feature_flag_matching("1y") == datetime.datetime( + 2019, 4, 3, 0, 0, 0, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching( + "12m" + ) == relative_date_parse_for_feature_flag_matching("1y") + + def test_year_parsing(self): + with freeze_time("2020-01-01T12:01:20.1340Z"): + assert relative_date_parse_for_feature_flag_matching("1y") == datetime.datetime( + 2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("2y") == datetime.datetime( + 2018, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("4y") == datetime.datetime( + 2016, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + ) + assert relative_date_parse_for_feature_flag_matching("8y") == datetime.datetime( + 2012, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC") + )