From 08a6a7f7664c234e888b7b26bf0bbc2cb67d5e20 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 13:55:56 -0700 Subject: [PATCH 01/12] fix projects[x].newgroups --- src/sentry/releases/use_cases/release.py | 32 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 361c9cdf6fafbb..c9b54246ba47e4 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -74,12 +74,22 @@ def serialize( fetch_owners=lambda owner_ids: fetch_owners(user, owner_ids), ) - project_map = get_projects(projects, new_groups_map.values(), fetch_project_platforms) + project_map = get_projects(projects, fetch_project_platforms) - release_projects_map = { - release_id: [project_map[project_id] for project_id in mapping.keys()] - for release_id, mapping in new_groups_map.items() - } + # release_projects_map = { + # release_id: [project_map[project_id] for project_id in mapping.keys()] + # for release_id, mapping in new_groups_map.items() + # } + + release_projects_map = {} + for release_id, mapping in new_groups_map.items(): + projects_for_release = [project_map[project_id] for project_id in mapping.keys()] + + # get the new groups for each project only for this release + for project_data in projects_for_release: + project_data["newGroups"] = mapping[project_data["id"]] + + release_projects_map[release_id] = projects_for_release adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -120,17 +130,17 @@ def serialize( @sentry_sdk.trace def get_projects( projects: Iterable[Project], - project_group_counts: Iterable[dict[int, int]], + # project_group_counts: Iterable[dict[int, int]], fetch_platforms: Callable[[Iterable[int]], list[tuple[int, str]]], ) -> dict[int, SerializedProject]: platforms = defaultdict(list) for project_id, platform in fetch_platforms([p.id for p in projects]): platforms[project_id].append(platform) - new_groups: defaultdict[int, int] = defaultdict(int) - for mapping in project_group_counts: - for project_id, count in mapping.items(): - new_groups[project_id] += count + # new_groups: defaultdict[int, int] = defaultdict(int) + # for mapping in project_group_counts: + # for project_id, count in mapping.items(): + # new_groups[project_id] += count return { project.id: { @@ -138,7 +148,7 @@ def get_projects( "slug": project.slug, "name": project.name, "platform": project.platform, - "newGroups": new_groups[project.id], + # "newGroups": new_groups[project.id], "platforms": platforms[project.id], "hasHealthData": False, } From 0d9aac656dd6071add52c7d8fd5e8da3931cf516 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 14:25:18 -0700 Subject: [PATCH 02/12] start tests --- src/sentry/releases/use_cases/release.py | 50 ++-- .../use_cases/test_release_serializer.py | 232 ++++++++++++++++++ 2 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 tests/sentry/releases/use_cases/test_release_serializer.py diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index c9b54246ba47e4..78b4370e35393f 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -74,6 +74,7 @@ def serialize( fetch_owners=lambda owner_ids: fetch_owners(user, owner_ids), ) + # project_map = get_projects_old(projects, new_groups_map.values(), fetch_project_platforms) project_map = get_projects(projects, fetch_project_platforms) # release_projects_map = { @@ -81,15 +82,12 @@ def serialize( # for release_id, mapping in new_groups_map.items() # } - release_projects_map = {} - for release_id, mapping in new_groups_map.items(): - projects_for_release = [project_map[project_id] for project_id in mapping.keys()] - - # get the new groups for each project only for this release - for project_data in projects_for_release: - project_data["newGroups"] = mapping[project_data["id"]] - - release_projects_map[release_id] = projects_for_release + release_projects_map = { + release_id: [ + {**project_map[project_id], "newGroups": count} for project_id, count in mapping.items() + ] + for release_id, mapping in new_groups_map.items() + } adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -130,17 +128,39 @@ def serialize( @sentry_sdk.trace def get_projects( projects: Iterable[Project], - # project_group_counts: Iterable[dict[int, int]], fetch_platforms: Callable[[Iterable[int]], list[tuple[int, str]]], ) -> dict[int, SerializedProject]: platforms = defaultdict(list) for project_id, platform in fetch_platforms([p.id for p in projects]): platforms[project_id].append(platform) - # new_groups: defaultdict[int, int] = defaultdict(int) - # for mapping in project_group_counts: - # for project_id, count in mapping.items(): - # new_groups[project_id] += count + return { + project.id: { + "id": project.id, + "slug": project.slug, + "name": project.name, + "platform": project.platform, + "platforms": platforms[project.id], + "hasHealthData": False, + } + for project in projects + } + + +@sentry_sdk.trace +def get_projects_old( + projects: Iterable[Project], + project_group_counts: Iterable[dict[int, int]], + fetch_platforms: Callable[[Iterable[int]], list[tuple[int, str]]], +) -> dict[int, SerializedProject]: + platforms = defaultdict(list) + for project_id, platform in fetch_platforms([p.id for p in projects]): + platforms[project_id].append(platform) + + new_groups: defaultdict[int, int] = defaultdict(int) + for mapping in project_group_counts: + for project_id, count in mapping.items(): + new_groups[project_id] += count return { project.id: { @@ -148,7 +168,7 @@ def get_projects( "slug": project.slug, "name": project.name, "platform": project.platform, - # "newGroups": new_groups[project.id], + "newGroups": new_groups[project.id], "platforms": platforms[project.id], "hasHealthData": False, } diff --git a/tests/sentry/releases/use_cases/test_release_serializer.py b/tests/sentry/releases/use_cases/test_release_serializer.py new file mode 100644 index 00000000000000..3b82339285111c --- /dev/null +++ b/tests/sentry/releases/use_cases/test_release_serializer.py @@ -0,0 +1,232 @@ +from __future__ import annotations + +from sentry.models.release import Release +from sentry.models.releases.release_project import ReleaseProject +from sentry.releases.use_cases.release import serialize as release_serializer +from sentry.testutils.cases import TestCase + + +class ReleaseSerializerUseCaseTest(TestCase): + """ + Tests for the releases.use_cases.release.serialize function. + + This tests the NEW serializer that fixes the per-project newGroups calculation, + as opposed to the old model-based serializer in api.serializers.models.release. + """ + + def test_new_groups_single_release_per_project(self): + """ + Test new groups counts for one release with multiple projects, each having different issue counts. + """ + project_a = self.create_project(name="Project A", slug="project-a") + project_b = self.create_project( + name="Project B", slug="project-b", organization=project_a.organization + ) + + # Create release in projects A and B + release_version = "1.0.0" + release = Release.objects.create( + organization_id=project_a.organization_id, version=release_version + ) + release.add_project(project_a) + release.add_project(project_b) + + # 3 new groups for project A, 2 new groups for project B + ReleaseProject.objects.filter(release=release, project=project_a).update(new_groups=3) + ReleaseProject.objects.filter(release=release, project=project_b).update(new_groups=2) + + result = release_serializer( + releases=[release], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], # No environment filtering + projects=[project_a, project_b], + ) + + assert len(result) == 1 + serialized_release = result[0] + + # total new groups count (5 == 3 + 2) + assert serialized_release["newGroups"] == 5 + + # new groups count for each project (3 for A, 2 for B) + projects_by_id = {p["id"]: p for p in serialized_release["projects"]} + assert projects_by_id[project_a.id]["newGroups"] == 3 + assert projects_by_id[project_b.id]["newGroups"] == 2 + + assert projects_by_id[project_a.id]["name"] == "Project A" + assert projects_by_id[project_a.id]["slug"] == "project-a" + assert projects_by_id[project_b.id]["name"] == "Project B" + assert projects_by_id[project_b.id]["slug"] == "project-b" + + def test_multiple_releases_per_project_isolation(self): + """ + Test new groups count for multiple releases per project. + """ + project_a = self.create_project(name="Project A", slug="project-a") + project_b = self.create_project( + name="Project B", slug="project-b", organization=project_a.organization + ) + + # Create releases 1 and 2, both in projects A and B + release_1 = Release.objects.create( + organization_id=project_a.organization_id, version="1.0.0" + ) + release_1.add_project(project_a) + release_1.add_project(project_b) + release_2 = Release.objects.create( + organization_id=project_a.organization_id, version="2.0.0" + ) + release_2.add_project(project_a) + release_2.add_project(project_b) + + # Release 1.0.0 has 3 new groups for project A, 2 new groups for project B + ReleaseProject.objects.filter(release=release_1, project=project_a).update(new_groups=3) + ReleaseProject.objects.filter(release=release_1, project=project_b).update(new_groups=2) + + # Release 2.0.0 has 1 new groups for project A, 4 new groups for project B + ReleaseProject.objects.filter(release=release_2, project=project_a).update(new_groups=1) + ReleaseProject.objects.filter(release=release_2, project=project_b).update(new_groups=4) + + # 1. Serialize Release 1.0.0 ONLY + result_1 = release_serializer( + releases=[release_1], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], + projects=[project_a, project_b], + ) + + assert len(result_1) == 1 + release_1_data = result_1[0] + assert release_1_data["version"] == "1.0.0" + assert release_1_data["newGroups"] == 5 # total new groups count (5 == 3 + 2) + projects_1_by_id = {p["id"]: p for p in release_1_data["projects"]} + # new groups count for each project (3 for A, 2 for B) + assert projects_1_by_id[project_a.id]["newGroups"] == 3 + assert projects_1_by_id[project_b.id]["newGroups"] == 2 + + # 2. Serialize Release 2.0.0 ONLY + result_2 = release_serializer( + releases=[release_2], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], + projects=[project_a, project_b], + ) + + assert len(result_2) == 1 + release_2_data = result_2[0] + assert release_2_data["version"] == "2.0.0" + assert release_2_data["newGroups"] == 5 # total new groups count (5 == 1 + 4) + projects_2_by_id = {p["id"]: p for p in release_2_data["projects"]} + # new groups count for each project (1 for A, 4 for B) + assert projects_2_by_id[project_a.id]["newGroups"] == 1 + assert projects_2_by_id[project_b.id]["newGroups"] == 4 + + # 3. Serialize both releases together + result_both = release_serializer( + releases=[release_1, release_2], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], + projects=[project_a, project_b], + ) + + assert len(result_both) == 2 + + # Find each release in the results + releases_by_version = {r["version"]: r for r in result_both} + both_release_1 = releases_by_version["1.0.0"] + both_release_2 = releases_by_version["2.0.0"] + + # Verify that when serialized together, counts are still isolated per release + both_projects_1_by_id = {p["id"]: p for p in both_release_1["projects"]} + both_projects_2_by_id = {p["id"]: p for p in both_release_2["projects"]} + # Release 1.0.0 should still have its isolated counts + assert both_projects_1_by_id[project_a.id]["newGroups"] == 3 + assert both_projects_1_by_id[project_b.id]["newGroups"] == 2 + # Release 2.0.0 should still have its isolated counts + assert both_projects_2_by_id[project_a.id]["newGroups"] == 1 + assert both_projects_2_by_id[project_b.id]["newGroups"] == 4 + + def test_environment_filtering_per_project(self): + """ + Test environment filtering with per-project counts. + + Scenario: + - Release "1.0.0" in Project A: 3 issues in production, 1 issue in staging + - Release "1.0.0" in Project B: 2 issues in production, 0 issues in staging + - When filtering by production: Project A = 3, Project B = 2 + - When filtering by staging: Project A = 1, Project B = 0 + - When no environment filter: Project A = 3, Project B = 2 (uses ReleaseProject.new_groups) + + This verifies environment filtering works correctly with our fix. + """ + # TODO: Implement test + pass + + def test_cross_project_release_environment_complex(self): + """ + Test complex scenario: Multiple releases, projects, and environments. + + Scenario: + - Release "1.0.0": + - Project A: 3 issues in production, 1 issue in staging + - Project B: 2 issues in production, 0 issues in staging + - Release "2.0.0": + - Project A: 1 issue in production, 2 issues in staging + - Project B: 4 issues in production, 1 issue in staging + + When serializing Release "1.0.0" with production filter: + - Should return: Project A = 3, Project B = 2 + - Should NOT include counts from Release "2.0.0" + + This is the most comprehensive test of our fix. + """ + # TODO: Implement test + pass + + # def test_model_counts_vs_serializer_consistency(self): + # """ + # Test that our serializer produces consistent results with model counts. + + # Scenario: + # - Create releases and projects with known issue counts + # - Force buffer processing to update ReleaseProject.new_groups and ReleaseProjectEnvironment.new_issues_count + # - Compare serializer output with direct model queries + # - Verify they match after buffer processing + + # This ensures our fix maintains consistency with the underlying model data. + # """ + # # TODO: Implement test + # pass + + def test_backward_compatibility_release_level_totals(self): + """ + Test that release-level totals (release.newGroups) remain unchanged. + + Scenario: + - Create multiple projects with different issue counts + - Verify that release.newGroups still equals sum of all project counts + - Ensure existing frontend code that uses release.newGroups continues to work + + This ensures our fix doesn't break existing functionality. + """ + # TODO: Implement test + pass + + def test_empty_and_edge_cases(self): + """ + Test edge cases: empty projects, zero counts, missing data. + + Scenarios: + - Release with no projects + - Release with projects but zero new issues + - Release with missing ReleaseProject records + - Release with None values in new_groups fields + + This ensures our fix handles edge cases gracefully. + """ + # TODO: Implement test + pass From f3a884a3be0b1ab731adf3e6d3d1fb15c918904e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 14:44:39 -0700 Subject: [PATCH 03/12] env filtering test --- src/sentry/releases/use_cases/release.py | 2 +- .../use_cases/test_release_serializer.py | 156 +++++++++++------- 2 files changed, 93 insertions(+), 65 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 78b4370e35393f..8e8fccfb867013 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -430,7 +430,7 @@ def fetch_issue_count( qs1 = ReleaseProjectEnvironment.objects.filter(release_id__in=release_ids) qs1 = qs1.filter(environment_id__in=environment_ids) qs1 = qs1.filter(project_id__in=project_ids) - qs1 = qs1.annotate(new_groups=Sum("new_issues_count")) + qs1 = qs1.values("project_id", "release_id").annotate(new_groups=Sum("new_issues_count")) return list(qs1.values_list("project_id", "release_id", "new_groups")) else: qs2 = ReleaseProject.objects.filter(release_id__in=release_ids) diff --git a/tests/sentry/releases/use_cases/test_release_serializer.py b/tests/sentry/releases/use_cases/test_release_serializer.py index 3b82339285111c..f7c25fa7c4691d 100644 --- a/tests/sentry/releases/use_cases/test_release_serializer.py +++ b/tests/sentry/releases/use_cases/test_release_serializer.py @@ -1,6 +1,7 @@ from __future__ import annotations from sentry.models.release import Release +from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment from sentry.models.releases.release_project import ReleaseProject from sentry.releases.use_cases.release import serialize as release_serializer from sentry.testutils.cases import TestCase @@ -59,7 +60,7 @@ def test_new_groups_single_release_per_project(self): assert projects_by_id[project_b.id]["name"] == "Project B" assert projects_by_id[project_b.id]["slug"] == "project-b" - def test_multiple_releases_per_project_isolation(self): + def test_new_groups_multiple_releases_per_project(self): """ Test new groups count for multiple releases per project. """ @@ -135,36 +136,109 @@ def test_multiple_releases_per_project_isolation(self): assert len(result_both) == 2 - # Find each release in the results + # Verify that when serialized together, counts are still isolated per release releases_by_version = {r["version"]: r for r in result_both} both_release_1 = releases_by_version["1.0.0"] both_release_2 = releases_by_version["2.0.0"] - - # Verify that when serialized together, counts are still isolated per release both_projects_1_by_id = {p["id"]: p for p in both_release_1["projects"]} both_projects_2_by_id = {p["id"]: p for p in both_release_2["projects"]} - # Release 1.0.0 should still have its isolated counts assert both_projects_1_by_id[project_a.id]["newGroups"] == 3 assert both_projects_1_by_id[project_b.id]["newGroups"] == 2 - # Release 2.0.0 should still have its isolated counts assert both_projects_2_by_id[project_a.id]["newGroups"] == 1 assert both_projects_2_by_id[project_b.id]["newGroups"] == 4 - def test_environment_filtering_per_project(self): + def test_new_groups_environment_filtering(self): + """ + Test new group counts withenvironment filtering. """ - Test environment filtering with per-project counts. + project_a = self.create_project(name="Project A", slug="project-a") + project_b = self.create_project( + name="Project B", slug="project-b", organization=project_a.organization + ) - Scenario: - - Release "1.0.0" in Project A: 3 issues in production, 1 issue in staging - - Release "1.0.0" in Project B: 2 issues in production, 0 issues in staging - - When filtering by production: Project A = 3, Project B = 2 - - When filtering by staging: Project A = 1, Project B = 0 - - When no environment filter: Project A = 3, Project B = 2 (uses ReleaseProject.new_groups) + production = self.create_environment(name="production", organization=project_a.organization) + staging = self.create_environment(name="staging", organization=project_a.organization) - This verifies environment filtering works correctly with our fix. - """ - # TODO: Implement test - pass + release = Release.objects.create(organization_id=project_a.organization_id, version="1.0.0") + release.add_project(project_a) + release.add_project(project_b) + + # 4 new groups for project A, 2 new groups for project B + ReleaseProject.objects.filter(release=release, project=project_a).update(new_groups=4) + ReleaseProject.objects.filter(release=release, project=project_b).update(new_groups=2) + + # Project A: 3 issues in production, 1 issue in staging (total = 4) + ReleaseProjectEnvironment.objects.create( + release=release, project=project_a, environment=production, new_issues_count=3 + ) + ReleaseProjectEnvironment.objects.create( + release=release, project=project_a, environment=staging, new_issues_count=1 + ) + + # Project B: 2 issues in production, 0 issues in staging (total = 2) + ReleaseProjectEnvironment.objects.create( + release=release, project=project_b, environment=production, new_issues_count=2 + ) + + # 1. No environment filter + result_no_env = release_serializer( + releases=[release], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], + projects=[project_a, project_b], + ) + assert len(result_no_env) == 1 + release_data_no_env = result_no_env[0] + projects_no_env = {p["id"]: p for p in release_data_no_env["projects"]} + assert projects_no_env[project_a.id]["newGroups"] == 4 + assert projects_no_env[project_b.id]["newGroups"] == 2 + assert release_data_no_env["newGroups"] == 6 + + # 2. Filter by production environment + result_production = release_serializer( + releases=[release], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[production.id], + projects=[project_a, project_b], + ) + assert len(result_production) == 1 + release_data_production = result_production[0] + projects_production = {p["id"]: p for p in release_data_production["projects"]} + assert projects_production[project_a.id]["newGroups"] == 3 + assert projects_production[project_b.id]["newGroups"] == 2 + assert release_data_production["newGroups"] == 5 + + # 3. Filter by staging environment + result_staging = release_serializer( + releases=[release], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[staging.id], # Staging only + projects=[project_a, project_b], + ) + assert len(result_staging) == 1 + release_data_staging = result_staging[0] + projects_staging = {p["id"]: p for p in release_data_staging["projects"]} + assert projects_staging[project_a.id]["newGroups"] == 1 + assert project_b.id not in projects_staging + assert release_data_staging["newGroups"] == 1 + + # 4. Filter by both environments + result_both_envs = release_serializer( + releases=[release], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[production.id, staging.id], + projects=[project_a, project_b], + ) + assert len(result_both_envs) == 1 + release_data_both_envs = result_both_envs[0] + projects_both_envs = {p["id"]: p for p in release_data_both_envs["projects"]} + assert projects_both_envs[project_a.id]["newGroups"] == 4 + assert projects_both_envs[project_b.id]["newGroups"] == 2 + assert release_data_both_envs["newGroups"] == 6 def test_cross_project_release_environment_complex(self): """ @@ -181,52 +255,6 @@ def test_cross_project_release_environment_complex(self): When serializing Release "1.0.0" with production filter: - Should return: Project A = 3, Project B = 2 - Should NOT include counts from Release "2.0.0" - - This is the most comprehensive test of our fix. - """ - # TODO: Implement test - pass - - # def test_model_counts_vs_serializer_consistency(self): - # """ - # Test that our serializer produces consistent results with model counts. - - # Scenario: - # - Create releases and projects with known issue counts - # - Force buffer processing to update ReleaseProject.new_groups and ReleaseProjectEnvironment.new_issues_count - # - Compare serializer output with direct model queries - # - Verify they match after buffer processing - - # This ensures our fix maintains consistency with the underlying model data. - # """ - # # TODO: Implement test - # pass - - def test_backward_compatibility_release_level_totals(self): - """ - Test that release-level totals (release.newGroups) remain unchanged. - - Scenario: - - Create multiple projects with different issue counts - - Verify that release.newGroups still equals sum of all project counts - - Ensure existing frontend code that uses release.newGroups continues to work - - This ensures our fix doesn't break existing functionality. - """ - # TODO: Implement test - pass - - def test_empty_and_edge_cases(self): - """ - Test edge cases: empty projects, zero counts, missing data. - - Scenarios: - - Release with no projects - - Release with projects but zero new issues - - Release with missing ReleaseProject records - - Release with None values in new_groups fields - - This ensures our fix handles edge cases gracefully. """ # TODO: Implement test pass From f8f950bc65183cff4e0970e8d176bfb87b3105c7 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:03:42 -0700 Subject: [PATCH 04/12] finish tests --- .../use_cases/test_release_serializer.py | 281 ++++++++++++------ 1 file changed, 193 insertions(+), 88 deletions(-) diff --git a/tests/sentry/releases/use_cases/test_release_serializer.py b/tests/sentry/releases/use_cases/test_release_serializer.py index f7c25fa7c4691d..039899b487c1fd 100644 --- a/tests/sentry/releases/use_cases/test_release_serializer.py +++ b/tests/sentry/releases/use_cases/test_release_serializer.py @@ -51,14 +51,14 @@ def test_new_groups_single_release_per_project(self): assert serialized_release["newGroups"] == 5 # new groups count for each project (3 for A, 2 for B) - projects_by_id = {p["id"]: p for p in serialized_release["projects"]} - assert projects_by_id[project_a.id]["newGroups"] == 3 - assert projects_by_id[project_b.id]["newGroups"] == 2 + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 3 + assert projects[project_b.id]["newGroups"] == 2 - assert projects_by_id[project_a.id]["name"] == "Project A" - assert projects_by_id[project_a.id]["slug"] == "project-a" - assert projects_by_id[project_b.id]["name"] == "Project B" - assert projects_by_id[project_b.id]["slug"] == "project-b" + assert projects[project_a.id]["name"] == "Project A" + assert projects[project_a.id]["slug"] == "project-a" + assert projects[project_b.id]["name"] == "Project B" + assert projects[project_b.id]["slug"] == "project-b" def test_new_groups_multiple_releases_per_project(self): """ @@ -89,67 +89,66 @@ def test_new_groups_multiple_releases_per_project(self): ReleaseProject.objects.filter(release=release_2, project=project_a).update(new_groups=1) ReleaseProject.objects.filter(release=release_2, project=project_b).update(new_groups=4) - # 1. Serialize Release 1.0.0 ONLY - result_1 = release_serializer( + # 1. Serialize Release 1.0.0 + result = release_serializer( releases=[release_1], user=self.user, organization_id=project_a.organization_id, environment_ids=[], projects=[project_a, project_b], ) - - assert len(result_1) == 1 - release_1_data = result_1[0] - assert release_1_data["version"] == "1.0.0" - assert release_1_data["newGroups"] == 5 # total new groups count (5 == 3 + 2) - projects_1_by_id = {p["id"]: p for p in release_1_data["projects"]} + assert len(result) == 1 + serialized_release = result[0] + assert serialized_release["version"] == "1.0.0" + assert serialized_release["newGroups"] == 5 # total new groups count (5 == 3 + 2) + projects = {p["id"]: p for p in serialized_release["projects"]} # new groups count for each project (3 for A, 2 for B) - assert projects_1_by_id[project_a.id]["newGroups"] == 3 - assert projects_1_by_id[project_b.id]["newGroups"] == 2 + assert projects[project_a.id]["newGroups"] == 3 + assert projects[project_b.id]["newGroups"] == 2 - # 2. Serialize Release 2.0.0 ONLY - result_2 = release_serializer( + # 2. Serialize Release 2.0.0 + result = release_serializer( releases=[release_2], user=self.user, organization_id=project_a.organization_id, environment_ids=[], projects=[project_a, project_b], ) - - assert len(result_2) == 1 - release_2_data = result_2[0] - assert release_2_data["version"] == "2.0.0" - assert release_2_data["newGroups"] == 5 # total new groups count (5 == 1 + 4) - projects_2_by_id = {p["id"]: p for p in release_2_data["projects"]} + assert len(result) == 1 + serialized_release = result[0] + assert serialized_release["version"] == "2.0.0" + assert serialized_release["newGroups"] == 5 # total new groups count (5 == 1 + 4) + projects = {p["id"]: p for p in serialized_release["projects"]} # new groups count for each project (1 for A, 4 for B) - assert projects_2_by_id[project_a.id]["newGroups"] == 1 - assert projects_2_by_id[project_b.id]["newGroups"] == 4 + assert projects[project_a.id]["newGroups"] == 1 + assert projects[project_b.id]["newGroups"] == 4 # 3. Serialize both releases together - result_both = release_serializer( + result = release_serializer( releases=[release_1, release_2], user=self.user, organization_id=project_a.organization_id, environment_ids=[], projects=[project_a, project_b], ) - - assert len(result_both) == 2 - - # Verify that when serialized together, counts are still isolated per release - releases_by_version = {r["version"]: r for r in result_both} - both_release_1 = releases_by_version["1.0.0"] - both_release_2 = releases_by_version["2.0.0"] - both_projects_1_by_id = {p["id"]: p for p in both_release_1["projects"]} - both_projects_2_by_id = {p["id"]: p for p in both_release_2["projects"]} - assert both_projects_1_by_id[project_a.id]["newGroups"] == 3 - assert both_projects_1_by_id[project_b.id]["newGroups"] == 2 - assert both_projects_2_by_id[project_a.id]["newGroups"] == 1 - assert both_projects_2_by_id[project_b.id]["newGroups"] == 4 + assert len(result) == 2 + serialized_releases = {r["version"]: r for r in result} + serialized_release_1 = serialized_releases["1.0.0"] + serialized_release_2 = serialized_releases["2.0.0"] + # both new group counts should be 5 + assert serialized_release_1["newGroups"] == 5 + assert serialized_release_2["newGroups"] == 5 + # new groups counts for each project + projects_1 = {p["id"]: p for p in serialized_release_1["projects"]} + projects_2 = {p["id"]: p for p in serialized_release_2["projects"]} + assert projects_1[project_a.id]["newGroups"] == 3 + assert projects_1[project_b.id]["newGroups"] == 2 + assert projects_2[project_a.id]["newGroups"] == 1 + assert projects_2[project_b.id]["newGroups"] == 4 def test_new_groups_environment_filtering(self): """ - Test new group counts withenvironment filtering. + Test new group counts for a single release with environment filtering. """ project_a = self.create_project(name="Project A", slug="project-a") project_b = self.create_project( @@ -181,80 +180,186 @@ def test_new_groups_environment_filtering(self): ) # 1. No environment filter - result_no_env = release_serializer( + result = release_serializer( releases=[release], user=self.user, organization_id=project_a.organization_id, environment_ids=[], projects=[project_a, project_b], ) - assert len(result_no_env) == 1 - release_data_no_env = result_no_env[0] - projects_no_env = {p["id"]: p for p in release_data_no_env["projects"]} - assert projects_no_env[project_a.id]["newGroups"] == 4 - assert projects_no_env[project_b.id]["newGroups"] == 2 - assert release_data_no_env["newGroups"] == 6 + assert len(result) == 1 + serialized_release = result[0] + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 4 + assert projects[project_b.id]["newGroups"] == 2 + assert serialized_release["newGroups"] == 6 # 2. Filter by production environment - result_production = release_serializer( + result = release_serializer( releases=[release], user=self.user, organization_id=project_a.organization_id, environment_ids=[production.id], projects=[project_a, project_b], ) - assert len(result_production) == 1 - release_data_production = result_production[0] - projects_production = {p["id"]: p for p in release_data_production["projects"]} - assert projects_production[project_a.id]["newGroups"] == 3 - assert projects_production[project_b.id]["newGroups"] == 2 - assert release_data_production["newGroups"] == 5 + assert len(result) == 1 + serialized_release = result[0] + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 3 + assert projects[project_b.id]["newGroups"] == 2 + assert serialized_release["newGroups"] == 5 # 3. Filter by staging environment - result_staging = release_serializer( + result = release_serializer( releases=[release], user=self.user, organization_id=project_a.organization_id, - environment_ids=[staging.id], # Staging only + environment_ids=[staging.id], projects=[project_a, project_b], ) - assert len(result_staging) == 1 - release_data_staging = result_staging[0] - projects_staging = {p["id"]: p for p in release_data_staging["projects"]} - assert projects_staging[project_a.id]["newGroups"] == 1 - assert project_b.id not in projects_staging - assert release_data_staging["newGroups"] == 1 + assert len(result) == 1 + serialized_release = result[0] + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 1 + assert project_b.id not in projects + assert serialized_release["newGroups"] == 1 # 4. Filter by both environments - result_both_envs = release_serializer( + result = release_serializer( releases=[release], user=self.user, organization_id=project_a.organization_id, environment_ids=[production.id, staging.id], projects=[project_a, project_b], ) - assert len(result_both_envs) == 1 - release_data_both_envs = result_both_envs[0] - projects_both_envs = {p["id"]: p for p in release_data_both_envs["projects"]} - assert projects_both_envs[project_a.id]["newGroups"] == 4 - assert projects_both_envs[project_b.id]["newGroups"] == 2 - assert release_data_both_envs["newGroups"] == 6 - - def test_cross_project_release_environment_complex(self): + assert len(result) == 1 + serialized_release = result[0] + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 4 + assert projects[project_b.id]["newGroups"] == 2 + assert serialized_release["newGroups"] == 6 + + def test_new_groups_cross_project_release_environment(self): """ - Test complex scenario: Multiple releases, projects, and environments. - - Scenario: - - Release "1.0.0": - - Project A: 3 issues in production, 1 issue in staging - - Project B: 2 issues in production, 0 issues in staging - - Release "2.0.0": - - Project A: 1 issue in production, 2 issues in staging - - Project B: 4 issues in production, 1 issue in staging - - When serializing Release "1.0.0" with production filter: - - Should return: Project A = 3, Project B = 2 - - Should NOT include counts from Release "2.0.0" + Test new group counts for multiple releases with different environments. """ - # TODO: Implement test - pass + project_a = self.create_project(name="Project A", slug="project-a") + project_b = self.create_project( + name="Project B", slug="project-b", organization=project_a.organization + ) + + production = self.create_environment(name="production", organization=project_a.organization) + staging = self.create_environment(name="staging", organization=project_a.organization) + + release_1 = Release.objects.create( + organization_id=project_a.organization_id, version="1.0.0" + ) + release_1.add_project(project_a) + release_1.add_project(project_b) + + release_2 = Release.objects.create( + organization_id=project_a.organization_id, version="2.0.0" + ) + release_2.add_project(project_a) + release_2.add_project(project_b) + + # Release 1.0.0: Project A = 4 (3+1), Project B = 2 (2+0) + ReleaseProject.objects.filter(release=release_1, project=project_a).update(new_groups=4) + ReleaseProject.objects.filter(release=release_1, project=project_b).update(new_groups=2) + # Release 2.0.0: Project A = 3 (1+2), Project B = 5 (4+1) + ReleaseProject.objects.filter(release=release_2, project=project_a).update(new_groups=3) + ReleaseProject.objects.filter(release=release_2, project=project_b).update(new_groups=5) + + # Release 1.0.0 - Project A: 3 in production, 1 in staging + ReleaseProjectEnvironment.objects.create( + release=release_1, project=project_a, environment=production, new_issues_count=3 + ) + ReleaseProjectEnvironment.objects.create( + release=release_1, project=project_a, environment=staging, new_issues_count=1 + ) + # Release 1.0.0 - Project B: 2 in production, 0 in staging (no staging record) + ReleaseProjectEnvironment.objects.create( + release=release_1, project=project_b, environment=production, new_issues_count=2 + ) + # Release 2.0.0 - Project A: 1 in production, 2 in staging + ReleaseProjectEnvironment.objects.create( + release=release_2, project=project_a, environment=production, new_issues_count=1 + ) + ReleaseProjectEnvironment.objects.create( + release=release_2, project=project_a, environment=staging, new_issues_count=2 + ) + # Release 2.0.0 - Project B: 4 in production, 1 in staging + ReleaseProjectEnvironment.objects.create( + release=release_2, project=project_b, environment=production, new_issues_count=4 + ) + ReleaseProjectEnvironment.objects.create( + release=release_2, project=project_b, environment=staging, new_issues_count=1 + ) + + # 1. Serialize Release 1.0.0 with production filter + result = release_serializer( + releases=[release_1], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[production.id], + projects=[project_a, project_b], + ) + assert len(result) == 1 + serialized_release = result[0] + assert serialized_release["version"] == "1.0.0" + assert serialized_release["newGroups"] == 5 + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 3 + assert projects[project_b.id]["newGroups"] == 2 + + # 2. Serialize Release 2.0.0 with production filter + result = release_serializer( + releases=[release_2], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[production.id], + projects=[project_a, project_b], + ) + assert len(result) == 1 + serialized_release = result[0] + assert serialized_release["version"] == "2.0.0" + assert serialized_release["newGroups"] == 5 + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 1 + assert projects[project_b.id]["newGroups"] == 4 + + # 3. Serialize both releases together with production filter + result = release_serializer( + releases=[release_1, release_2], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[production.id], + projects=[project_a, project_b], + ) + assert len(result) == 2 + serialized_releases = {r["version"]: r for r in result} + serialized_release_1 = serialized_releases["1.0.0"] + serialized_release_2 = serialized_releases["2.0.0"] + assert serialized_release_1["newGroups"] == 5 + assert serialized_release_2["newGroups"] == 5 + projects_1 = {p["id"]: p for p in serialized_release_1["projects"]} + projects_2 = {p["id"]: p for p in serialized_release_2["projects"]} + assert projects_1[project_a.id]["newGroups"] == 3 + assert projects_1[project_b.id]["newGroups"] == 2 + assert projects_2[project_a.id]["newGroups"] == 1 + assert projects_2[project_b.id]["newGroups"] == 4 + + # 5. Serialize Release 1.0.0 with no environment filter + result = release_serializer( + releases=[release_1], + user=self.user, + organization_id=project_a.organization_id, + environment_ids=[], + projects=[project_a, project_b], + ) + assert len(result) == 1 + serialized_release = result[0] + assert serialized_release["newGroups"] == 6 + projects = {p["id"]: p for p in serialized_release["projects"]} + assert projects[project_a.id]["newGroups"] == 4 + assert projects[project_b.id]["newGroups"] == 2 From 2fc65d89a08477d16a8b3a45759aa3084005ebe0 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:05:32 -0700 Subject: [PATCH 05/12] cleaning up --- src/sentry/releases/use_cases/release.py | 35 ------------------------ 1 file changed, 35 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 8e8fccfb867013..3b7e40acbf20d3 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -74,14 +74,8 @@ def serialize( fetch_owners=lambda owner_ids: fetch_owners(user, owner_ids), ) - # project_map = get_projects_old(projects, new_groups_map.values(), fetch_project_platforms) project_map = get_projects(projects, fetch_project_platforms) - # release_projects_map = { - # release_id: [project_map[project_id] for project_id in mapping.keys()] - # for release_id, mapping in new_groups_map.items() - # } - release_projects_map = { release_id: [ {**project_map[project_id], "newGroups": count} for project_id, count in mapping.items() @@ -147,35 +141,6 @@ def get_projects( } -@sentry_sdk.trace -def get_projects_old( - projects: Iterable[Project], - project_group_counts: Iterable[dict[int, int]], - fetch_platforms: Callable[[Iterable[int]], list[tuple[int, str]]], -) -> dict[int, SerializedProject]: - platforms = defaultdict(list) - for project_id, platform in fetch_platforms([p.id for p in projects]): - platforms[project_id].append(platform) - - new_groups: defaultdict[int, int] = defaultdict(int) - for mapping in project_group_counts: - for project_id, count in mapping.items(): - new_groups[project_id] += count - - return { - project.id: { - "id": project.id, - "slug": project.slug, - "name": project.name, - "platform": project.platform, - "newGroups": new_groups[project.id], - "platforms": platforms[project.id], - "hasHealthData": False, - } - for project in projects - } - - @sentry_sdk.trace def get_release_adoption_stages( environment_ids: list[int], From 66ca8dc403e8f04146a0f08b8ac143d3011ec742 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:22:00 -0700 Subject: [PATCH 06/12] only include projs with nonzero counts --- src/sentry/releases/use_cases/release.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 3b7e40acbf20d3..74e1392bf586e1 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -76,12 +76,15 @@ def serialize( project_map = get_projects(projects, fetch_project_platforms) - release_projects_map = { - release_id: [ - {**project_map[project_id], "newGroups": count} for project_id, count in mapping.items() - ] - for release_id, mapping in new_groups_map.items() - } + release_projects_map = {} + for release_id, mapping in new_groups_map.items(): + projects_for_release = [] + for project_id, count in mapping.items(): + if count is not None and count > 0: # Only include projects with actual new groups + project_data = project_map[project_id].copy() + project_data["newGroups"] = count + projects_for_release.append(project_data) + release_projects_map[release_id] = projects_for_release adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -134,6 +137,7 @@ def get_projects( "slug": project.slug, "name": project.name, "platform": project.platform, + "newGroups": 0, # Default value, will be overridden per release "platforms": platforms[project.id], "hasHealthData": False, } From 2be5a3be74eeed599bed5968952a16c4d83cc279 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:26:45 -0700 Subject: [PATCH 07/12] make release projects map helper --- src/sentry/releases/use_cases/release.py | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 74e1392bf586e1..7c987bf92bbdef 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -76,15 +76,7 @@ def serialize( project_map = get_projects(projects, fetch_project_platforms) - release_projects_map = {} - for release_id, mapping in new_groups_map.items(): - projects_for_release = [] - for project_id, count in mapping.items(): - if count is not None and count > 0: # Only include projects with actual new groups - project_data = project_map[project_id].copy() - project_data["newGroups"] = count - projects_for_release.append(project_data) - release_projects_map[release_id] = projects_for_release + release_projects_map = get_release_projects_map(new_groups_map, project_map) adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -145,6 +137,23 @@ def get_projects( } +@sentry_sdk.trace +def get_release_projects_map( + new_groups_map: dict[int, dict[int, int | None]], + project_map: dict[int, SerializedProject], +) -> dict[int, list[SerializedProject]]: + release_projects_map = {} + for release_id, mapping in new_groups_map.items(): + projects_for_release = [] + for project_id, count in mapping.items(): + if count is not None and count > 0: # Only include projects with actual new groups + project_data = project_map[project_id].copy() + project_data["newGroups"] = count + projects_for_release.append(project_data) + release_projects_map[release_id] = projects_for_release + return release_projects_map + + @sentry_sdk.trace def get_release_adoption_stages( environment_ids: list[int], From e34bba1cdb84b69c8b7232a92aa3c35faf48a8d7 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:34:03 -0700 Subject: [PATCH 08/12] fix nonzero count stuff --- src/sentry/releases/use_cases/release.py | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index 7c987bf92bbdef..ed3817b147e506 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -76,7 +76,14 @@ def serialize( project_map = get_projects(projects, fetch_project_platforms) - release_projects_map = get_release_projects_map(new_groups_map, project_map) + release_projects_map = { + release_id: [ + {**project_map[project_id], "newGroups": count} + for project_id, count in mapping.items() + if count is not None and count > 0 + ] + for release_id, mapping in new_groups_map.items() + } adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -137,23 +144,6 @@ def get_projects( } -@sentry_sdk.trace -def get_release_projects_map( - new_groups_map: dict[int, dict[int, int | None]], - project_map: dict[int, SerializedProject], -) -> dict[int, list[SerializedProject]]: - release_projects_map = {} - for release_id, mapping in new_groups_map.items(): - projects_for_release = [] - for project_id, count in mapping.items(): - if count is not None and count > 0: # Only include projects with actual new groups - project_data = project_map[project_id].copy() - project_data["newGroups"] = count - projects_for_release.append(project_data) - release_projects_map[release_id] = projects_for_release - return release_projects_map - - @sentry_sdk.trace def get_release_adoption_stages( environment_ids: list[int], From 70cae108a22162ab4c708fe70b48752fd7c62715 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 15:59:07 -0700 Subject: [PATCH 09/12] fix zero count --- src/sentry/releases/use_cases/release.py | 4 +--- tests/sentry/releases/use_cases/test_release_serializer.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index ed3817b147e506..e96316f9afa3d5 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -78,9 +78,7 @@ def serialize( release_projects_map = { release_id: [ - {**project_map[project_id], "newGroups": count} - for project_id, count in mapping.items() - if count is not None and count > 0 + {**project_map[project_id], "newGroups": count} for project_id, count in mapping.items() ] for release_id, mapping in new_groups_map.items() } diff --git a/tests/sentry/releases/use_cases/test_release_serializer.py b/tests/sentry/releases/use_cases/test_release_serializer.py index 039899b487c1fd..5ae9b72be28045 100644 --- a/tests/sentry/releases/use_cases/test_release_serializer.py +++ b/tests/sentry/releases/use_cases/test_release_serializer.py @@ -178,6 +178,9 @@ def test_new_groups_environment_filtering(self): ReleaseProjectEnvironment.objects.create( release=release, project=project_b, environment=production, new_issues_count=2 ) + ReleaseProjectEnvironment.objects.create( + release=release, project=project_b, environment=staging, new_issues_count=0 + ) # 1. No environment filter result = release_serializer( @@ -221,7 +224,7 @@ def test_new_groups_environment_filtering(self): serialized_release = result[0] projects = {p["id"]: p for p in serialized_release["projects"]} assert projects[project_a.id]["newGroups"] == 1 - assert project_b.id not in projects + assert projects[project_b.id]["newGroups"] == 0 assert serialized_release["newGroups"] == 1 # 4. Filter by both environments From c13d571944e88d2974d460a6146945ab4fb12b33 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 3 Oct 2025 16:58:26 -0700 Subject: [PATCH 10/12] fix typing errors --- src/sentry/releases/use_cases/release.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index e96316f9afa3d5..e1eb70f01e329e 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -1,7 +1,7 @@ from collections import defaultdict from collections.abc import Callable, Iterable, Mapping from datetime import datetime, timezone -from typing import Any +from typing import Any, cast import sentry_sdk from django.contrib.auth.models import AnonymousUser @@ -76,9 +76,10 @@ def serialize( project_map = get_projects(projects, fetch_project_platforms) - release_projects_map = { + release_projects_map: dict[int, list[SerializedProject]] = { release_id: [ - {**project_map[project_id], "newGroups": count} for project_id, count in mapping.items() + cast(SerializedProject, {**project_map[project_id], "newGroups": count}) + for project_id, count in mapping.items() ] for release_id, mapping in new_groups_map.items() } @@ -396,8 +397,10 @@ def fetch_issue_count( qs1 = ReleaseProjectEnvironment.objects.filter(release_id__in=release_ids) qs1 = qs1.filter(environment_id__in=environment_ids) qs1 = qs1.filter(project_id__in=project_ids) - qs1 = qs1.values("project_id", "release_id").annotate(new_groups=Sum("new_issues_count")) - return list(qs1.values_list("project_id", "release_id", "new_groups")) + annotated_qs = qs1.values("project_id", "release_id").annotate( + new_groups=Sum("new_issues_count") + ) + return list(annotated_qs.values_list("project_id", "release_id", "new_groups")) else: qs2 = ReleaseProject.objects.filter(release_id__in=release_ids) qs2 = qs2.filter(project_id__in=project_ids) From 122ed556164e59a53bd2c8bdfac9226b1f368579 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 6 Oct 2025 10:13:01 -0700 Subject: [PATCH 11/12] make into helper --- src/sentry/releases/use_cases/release.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index e1eb70f01e329e..db1bff371cff6b 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -1,7 +1,7 @@ from collections import defaultdict from collections.abc import Callable, Iterable, Mapping from datetime import datetime, timezone -from typing import Any, cast +from typing import Any import sentry_sdk from django.contrib.auth.models import AnonymousUser @@ -76,13 +76,7 @@ def serialize( project_map = get_projects(projects, fetch_project_platforms) - release_projects_map: dict[int, list[SerializedProject]] = { - release_id: [ - cast(SerializedProject, {**project_map[project_id], "newGroups": count}) - for project_id, count in mapping.items() - ] - for release_id, mapping in new_groups_map.items() - } + release_projects_map = get_release_projects(new_groups_map, project_map) adoption_stage_map: dict[int, dict[str, AdoptionStage]] = { rid: {project_map[pid]["slug"]: adoption_stage for pid, adoption_stage in mapping} @@ -120,6 +114,17 @@ def serialize( ) +def get_release_projects( + new_groups_map: defaultdict[int, dict[int, int]], project_map: dict[int, SerializedProject] +) -> defaultdict[int, list[SerializedProject]]: + release_projects_map: defaultdict[int, list[SerializedProject]] = defaultdict(list) + for release_id, mapping in new_groups_map.items(): + for project_id, count in mapping.items(): + project_data: SerializedProject = {**project_map[project_id], "newGroups": count} + release_projects_map[release_id].append(project_data) + return release_projects_map + + @sentry_sdk.trace def get_projects( projects: Iterable[Project], From 4a94c1660c9fd8460f8ed5348fea5e7bbe79b736 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 6 Oct 2025 10:20:48 -0700 Subject: [PATCH 12/12] cleaning up --- src/sentry/releases/use_cases/release.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry/releases/use_cases/release.py b/src/sentry/releases/use_cases/release.py index db1bff371cff6b..f798c42271ddfa 100644 --- a/src/sentry/releases/use_cases/release.py +++ b/src/sentry/releases/use_cases/release.py @@ -120,8 +120,7 @@ def get_release_projects( release_projects_map: defaultdict[int, list[SerializedProject]] = defaultdict(list) for release_id, mapping in new_groups_map.items(): for project_id, count in mapping.items(): - project_data: SerializedProject = {**project_map[project_id], "newGroups": count} - release_projects_map[release_id].append(project_data) + release_projects_map[release_id].append({**project_map[project_id], "newGroups": count}) return release_projects_map