From f846e04a5929e542b1f9c30ff941bd357ee2ac22 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Sat, 31 Oct 2026 09:50:18 -0700 Subject: [PATCH] fix(releases): default missing health stats and group counts Missing health stats serialized as null and per-project newGroups could serialize as null when the database value was null. Return an empty stats object and a zero count so API clients always get the documented shape. The stats mapping is rebuilt with str keys so the response no longer aliases the release-health input mapping. --- src/sentry/api/serializers/models/release.py | 4 +- tests/sentry/api/serializers/test_release.py | 53 +++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/sentry/api/serializers/models/release.py b/src/sentry/api/serializers/models/release.py index 13687a0277a5..53ae2588dadd 100644 --- a/src/sentry/api/serializers/models/release.py +++ b/src/sentry/api/serializers/models/release.py @@ -75,7 +75,7 @@ def _expose_health_data(data): "totalProjectSessions24h": data["total_project_sessions_24h"], "adoption": data["adoption"], "sessionsAdoption": data["sessions_adoption"], - "stats": data.get("stats"), + "stats": {str(k): v for k, v in (data.get("stats") or {}).items()}, # XXX: legacy key, should be removed later. "hasHealthData": data["has_health_data"], } @@ -86,7 +86,7 @@ def _expose_project(project): "id": project["id"], "slug": project["slug"], "name": project["name"], - "newGroups": project["new_groups"], + "newGroups": project["new_groups"] or 0, "platform": project["platform"], "platforms": project["platforms"], # XXX: Legacy should be removed diff --git a/tests/sentry/api/serializers/test_release.py b/tests/sentry/api/serializers/test_release.py index 9270b60135b2..c7d2ed8ea7f6 100644 --- a/tests/sentry/api/serializers/test_release.py +++ b/tests/sentry/api/serializers/test_release.py @@ -10,7 +10,11 @@ from sentry import tagstore from sentry.api.endpoints.organization_releases import ReleaseSerializerWithProjects from sentry.api.serializers import serialize -from sentry.api.serializers.models.release import GroupEventReleaseSerializer, get_users_for_authors +from sentry.api.serializers.models.release import ( + GroupEventReleaseSerializer, + _expose_health_data, + get_users_for_authors, +) from sentry.integrations.models.external_actor import ExternalActor from sentry.models.commit import Commit from sentry.models.commitauthor import CommitAuthor @@ -595,6 +599,22 @@ def test_with_none_new_groups(self) -> None: assert result["version"] == "0.1" assert result["newGroups"] == 0 # Should default to 0 when None + def test_none_new_groups_per_project_defaults_to_zero(self) -> None: + project = self.create_project() + + release = Release.objects.create( + organization_id=project.organization_id, + version="0.2", + ) + release.add_project(project) + + ReleaseProject.objects.filter(release=release, project=project).update(new_groups=None) + + result = serialize(release, user=self.user, project=project) + + assert result["newGroups"] == 0 + assert result["projects"][0]["newGroups"] == 0 # Should default to 0 when None + def test_new_groups_single_release(self) -> None: """ Test new groups counts for one release with multiple projects, each having different issue counts. @@ -1263,3 +1283,34 @@ def test_external_actor_duplicate_external_name_prefers_most_recent(self) -> Non assert users[str(author.id)].get("id", "not present") == str(user2.id) assert users[str(author.id)]["email"] == "user2@company.com" assert users[str(author.id)]["name"] == "User 2" + + +class ExposeHealthDataTest(TestCase): + def test_none_and_empty_payload_returns_none(self) -> None: + assert _expose_health_data(None) is None + assert _expose_health_data({}) is None + + def test_missing_stats_defaults_to_empty_dict(self) -> None: + result = _expose_health_data( + { + "duration_p50": None, + "duration_p90": None, + "crash_free_users": None, + "crash_free_sessions": None, + "sessions_crashed": 0, + "sessions_errored": 0, + "total_users": 0, + "total_users_24h": 0, + "total_project_users_24h": 0, + "total_sessions": 0, + "total_sessions_24h": 0, + "total_project_sessions_24h": 0, + "adoption": None, + "sessions_adoption": None, + "has_health_data": False, + } + ) + + assert result is not None + assert result["stats"] == {} + assert result["sessionsCrashed"] == 0