diff --git a/src/sentry/replays/post_process.py b/src/sentry/replays/post_process.py index b23303d64faed5..f2ab7f936479b4 100644 --- a/src/sentry/replays/post_process.py +++ b/src/sentry/replays/post_process.py @@ -32,12 +32,20 @@ class BrowserResponseType(TypedDict, total=False): version: str | None +class UserGeoResponseType(TypedDict, total=False): + city: str | None + country_code: str | None + region: str | None + subdivision: str | None + + class UserResponseType(TypedDict, total=False): id: str | None username: str | None email: str | None ip: str | None display_name: str | None + geo: UserGeoResponseType class OTAUpdatesResponseType(TypedDict, total=False): @@ -132,6 +140,12 @@ def generate_normalized_output(response: list[dict[str, Any]]) -> Generator[Repl "username": item.pop("user_username", None), "email": item.pop("user_email", None), "ip": item.pop("user_ip", None), + "geo": { + "city": item.pop("user_geo_city", None), + "country_code": item.pop("user_geo_country_code", None), + "region": item.pop("user_geo_region", None), + "subdivision": item.pop("user_geo_subdivision", None), + }, } ret_item["user"]["display_name"] = ( ret_item["user"]["username"] @@ -225,7 +239,19 @@ def _archived_row(replay_id: str, project_id: int) -> dict[str, Any]: "error_ids": [], "environment": None, "tags": [], - "user": {"id": "Archived Replay", "display_name": "Archived Replay"}, + "user": { + "id": "Archived Replay", + "display_name": "Archived Replay", + "username": None, + "email": None, + "ip": None, + "geo": { + "city": None, + "country_code": None, + "region": None, + "subdivision": None, + }, + }, "sdk": {"name": None, "version": None}, "os": {"name": None, "version": None}, "browser": {"name": None, "version": None}, diff --git a/src/sentry/replays/query.py b/src/sentry/replays/query.py index 1179cef8229ff1..7608db1d2a6cd1 100644 --- a/src/sentry/replays/query.py +++ b/src/sentry/replays/query.py @@ -564,6 +564,10 @@ def _empty_uuids_lambda(): "user.email": ["user_email"], "user.username": ["user_username"], "user.ip": ["user_ip"], + "user.geo.city": ["user_geo_city"], + "user.geo.country_code": ["user_geo_country_code"], + "user.geo.region": ["user_geo_region"], + "user.geo.subdivision": ["user_geo_subdivision"], "os.name": ["os_name"], "os.version": ["os_version"], "browser.name": ["browser_name"], @@ -719,6 +723,10 @@ def _empty_uuids_lambda(): parameters=[anyIfNonZeroIP(column_name="ip_address_v4", aliased=False)], alias="user_ip", ), + "user_geo_city": anyIf(column_name="user_geo_city"), + "user_geo_country_code": anyIf(column_name="user_geo_country_code"), + "user_geo_region": anyIf(column_name="user_geo_region"), + "user_geo_subdivision": anyIf(column_name="user_geo_subdivision"), "os_name": anyIf(column_name="os_name"), "os_version": anyIf(column_name="os_version"), "browser_name": anyIf(column_name="browser_name"), diff --git a/src/sentry/replays/testutils.py b/src/sentry/replays/testutils.py index 532b85e0b61ed7..70fcedadc8e9df 100644 --- a/src/sentry/replays/testutils.py +++ b/src/sentry/replays/testutils.py @@ -125,6 +125,12 @@ def mock_expected_response( "email": kwargs.pop("user_email", "username@example.com"), "username": kwargs.pop("user_name", "username"), "ip": kwargs.pop("user_ip", "127.0.0.1"), + "geo": { + "city": kwargs.pop("user_geo_city", "San Francisco"), + "country_code": kwargs.pop("user_geo_country_code", "USA"), + "region": kwargs.pop("user_geo_region", "United States"), + "subdivision": kwargs.pop("user_geo_subdivision", "California"), + }, }, "tags": kwargs.pop("tags", {}), "activity": kwargs.pop("activity", 0), @@ -185,6 +191,12 @@ def mock_replay( "username": kwargs.pop("user_name", "username"), "email": kwargs.pop("user_email", "username@example.com"), "ip_address": kwargs.pop("ipv4", "127.0.0.1"), + "geo": { + "city": kwargs.pop("user_geo_city", "San Francisco"), + "country_code": kwargs.pop("user_geo_country_code", "USA"), + "region": kwargs.pop("user_geo_region", "United States"), + "subdivision": kwargs.pop("user_geo_subdivision", "California"), + }, }, "sdk": { "name": kwargs.pop("sdk_name", "sentry.javascript.react"), diff --git a/src/sentry/replays/usecases/query/configs/aggregate.py b/src/sentry/replays/usecases/query/configs/aggregate.py index e0e181cd258c9f..85403a09f0676c 100644 --- a/src/sentry/replays/usecases/query/configs/aggregate.py +++ b/src/sentry/replays/usecases/query/configs/aggregate.py @@ -134,6 +134,10 @@ def array_string_field(column_name: str) -> StringColumnField: "user.id": string_field("user_id"), "user.ip_address": NullableStringColumnField("ip_address_v4", parse_ipv4, SumOfIPv4Scalar), "user.username": string_field("user_name"), + "user.geo.city": string_field("user_geo_city"), + "user.geo.country_code": string_field("user_geo_country_code"), + "user.geo.region": string_field("user_geo_region"), + "user.geo.subdivision": string_field("user_geo_subdivision"), "viewed_by_id": IntegerColumnField("viewed_by_id", parse_int, SumOfIntegerIdScalar), "warning_ids": UUIDColumnField("warning_id", parse_uuid, SumOfUUIDScalar), } diff --git a/tests/sentry/replays/test_organization_replay_index.py b/tests/sentry/replays/test_organization_replay_index.py index 5737fbaebbeae0..6cc932d6a5ac29 100644 --- a/tests/sentry/replays/test_organization_replay_index.py +++ b/tests/sentry/replays/test_organization_replay_index.py @@ -235,12 +235,17 @@ def test_get_replays_browse_screen_fields(self): assert "urls" in response_data["data"][0] assert "user" in response_data["data"][0] - assert len(response_data["data"][0]["user"]) == 5 + assert len(response_data["data"][0]["user"]) == 6 assert "id" in response_data["data"][0]["user"] assert "username" in response_data["data"][0]["user"] assert "email" in response_data["data"][0]["user"] assert "ip" in response_data["data"][0]["user"] assert "display_name" in response_data["data"][0]["user"] + assert "geo" in response_data["data"][0]["user"] + assert "city" in response_data["data"][0]["user"]["geo"] + assert "country_code" in response_data["data"][0]["user"]["geo"] + assert "region" in response_data["data"][0]["user"]["geo"] + assert "subdivision" in response_data["data"][0]["user"]["geo"] def test_get_replays_tags_field(self): """Test replay response with fields requested in production.""" @@ -658,6 +663,11 @@ def test_get_replays_user_filters(self): "user.ip:127.0.0.1", "user.ip:[127.0.0.1, 10.0.4.4]", "!user.ip:[127.1.1.1, 10.0.4.4]", + 'user.geo.city:"San Francisco"', + "user.geo.country_code:USA", + 'user.geo.region:"United States"', + "user.geo.subdivision:California", + 'user.geo.city:"San Francisco" AND user.geo.country_code:USA AND user.geo.region:"United States" AND user.geo.subdivision:California', "sdk.name:sentry.javascript.react", "ota_updates.channel:stable", "ota_updates.runtime_version:1.2.3", @@ -774,6 +784,12 @@ def test_get_replays_user_filters(self): "seen_by_me:false", "user.email:[user2@example.com]", "!user.email:[username@example.com, user2@example.com]", + '!user.geo.city:"San Francisco"', + "!user.geo.country_code:USA", + '!user.geo.region:"United States"', + "!user.geo.subdivision:California", + 'user.geo.city:"San Francisco" AND !user.geo.country_code:USA', + '!user.geo.subdivision:California OR !user.geo.region:"United States"', "!ota_updates.channel:stable", "!ota_updates.runtime_version:1.2.3", "!ota_updates.update_id:1234567890", @@ -1068,7 +1084,19 @@ def test_archived_records_are_null_fields(self): "error_ids": [], "environment": None, "tags": [], - "user": {"id": "Archived Replay", "display_name": "Archived Replay"}, + "user": { + "id": "Archived Replay", + "display_name": "Archived Replay", + "username": None, + "email": None, + "ip": None, + "geo": { + "city": None, + "country_code": None, + "region": None, + "subdivision": None, + }, + }, "sdk": {"name": None, "version": None}, "os": {"name": None, "version": None}, "browser": {"name": None, "version": None},