From fe97b9aac9113ea1eba2b4f1c0a4b5bd2d7a8a9b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:32:05 +0000 Subject: [PATCH] ref(sourcemaps): Read has_debug_ids in the source map debugger When none of an event's debug IDs matched, project_has_some_artifact_with_a_debug_id now checks the has_debug_ids flag on the project's ProjectArtifactBundle links. The partial index answers this with one lookup, whether or not the project has such a link, so the check no longer reads DebugIdArtifactBundle and no longer needs the debug-id-check-max-bundles cap. The option stays registered until the automator stops setting it. Co-Authored-By: Jan Michael Auer <1433023+jan-auer@users.noreply.github.com> Co-Authored-By: Lorenzo Cian --- src/sentry/api/endpoints/source_map_debug.py | 42 +---------- src/sentry/options/defaults.py | 6 +- .../api/endpoints/test_source_map_debug.py | 75 +++---------------- 3 files changed, 14 insertions(+), 109 deletions(-) diff --git a/src/sentry/api/endpoints/source_map_debug.py b/src/sentry/api/endpoints/source_map_debug.py index 3909a7654a12..55c19d234e95 100644 --- a/src/sentry/api/endpoints/source_map_debug.py +++ b/src/sentry/api/endpoints/source_map_debug.py @@ -3,7 +3,6 @@ from typing import Annotated, Any, Literal, NamedTuple, TypedDict, cast import sentry_sdk -from django.db.models import Exists, OuterRef from django.utils.encoding import force_bytes, force_str from drf_spectacular.utils import extend_schema from packaging.version import Version @@ -70,10 +69,6 @@ def _sourcemap_header(headers: Mapping[str, object]) -> str | None: # This number will equate to an upper bound of file lookups/downloads ARTIFACT_INDEX_LOOKUP_LIMIT = 25 -# Upper bound for `sourcemaps.source-map-debug.debug-id-check-max-bundles`, whose bundle ids end -# up in a single query. -DEBUG_ID_CHECK_MAX_BUNDLES_LIMIT = 10_000 - # Upper bound on the number of bundles whose files `get_release_bundle_urls` reads. URL_MATCH_MAX_BUNDLES = 1000 @@ -964,42 +959,9 @@ def event_has_debug_ids(event_data: Mapping[str, Any]) -> bool: def project_has_some_artifact_with_a_debug_id(project: Project) -> bool: """ Whether any artifact bundle of the project contains a file with a debug ID. - - Only the project's newest bundles are checked, up to - `sourcemaps.source-map-debug.debug-id-check-max-bundles`, so a project whose debug IDs are all - in older bundles counts as having none. With the option at 0, this reads the organization's - debug-ID rows until one of them is in a bundle of the project, which for a project without - any means all of them. """ - max_bundles = min( - options.get("sourcemaps.source-map-debug.debug-id-check-max-bundles"), - DEBUG_ID_CHECK_MAX_BUNDLES_LIMIT, - ) - if max_bundles <= 0: - return ( - DebugIdArtifactBundle.objects.filter( - organization_id=project.organization_id, - ) - .filter( - Exists( - ProjectArtifactBundle.objects.filter( - artifact_bundle_id=OuterRef("artifact_bundle_id"), - project_id=project.id, - ) - ) - ) - .exists() - ) - - # Bundle ids follow upload order, so this reads the `(project_id, artifact_bundle_id)` index - # backwards and stops after `max_bundles` rows. - newest_bundle_ids = list( - ProjectArtifactBundle.objects.filter(project_id=project.id) - .order_by("-artifact_bundle_id") - .values_list("artifact_bundle_id", flat=True)[:max_bundles] - ) - # One lookup per bundle in the debug-ID rows' `artifact_bundle_id` index. - return DebugIdArtifactBundle.objects.filter(artifact_bundle_id__in=newest_bundle_ids).exists() + # One lookup in the partial index of flagged links, which finds nothing for a project without any. + return ProjectArtifactBundle.objects.filter(project_id=project.id, has_debug_ids=True).exists() def get_sdk_debug_id_support( diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 7ff126575525..f14f564b8fb1 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -1268,10 +1268,8 @@ default=False, flags=FLAG_MODIFIABLE_BOOL | FLAG_AUTOMATOR_MODIFIABLE, ) -# When none of an event's debug IDs were uploaded, the source map debugger checks whether the -# project has uploaded any file with a debug ID. It checks only the project's newest bundles, up -# to this many. 0 reads the organization's debug-ID rows instead, until one is in a bundle of the -# project, which for a project without any means all of them. Capped at 10,000. +# No longer read: the source map debugger checks `ProjectArtifactBundle.has_debug_ids` instead. +# Stays registered until the options automator no longer sets it. register( "sourcemaps.source-map-debug.debug-id-check-max-bundles", type=Int, diff --git a/tests/sentry/api/endpoints/test_source_map_debug.py b/tests/sentry/api/endpoints/test_source_map_debug.py index d8b9db626ff0..216cc520a22f 100644 --- a/tests/sentry/api/endpoints/test_source_map_debug.py +++ b/tests/sentry/api/endpoints/test_source_map_debug.py @@ -315,6 +315,7 @@ def test_project_has_some_artifact_bundle_with_a_debug_id_positive(self) -> None organization_id=self.organization.id, project_id=self.project.id, artifact_bundle=artifact_bundle, + has_debug_ids=True, ) event = self.store_event( @@ -363,12 +364,13 @@ def create_project_artifact_bundle(self, project_id: int, with_debug_id: bool) - organization_id=self.organization.id, project_id=project_id, artifact_bundle=artifact_bundle, + has_debug_ids=with_debug_id, ) - @override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 2}) - def test_project_has_some_artifact_bundle_with_a_debug_id_in_newest_bundles(self) -> None: + def test_project_has_some_artifact_bundle_with_a_debug_id_only_in_older_bundles(self) -> None: self.create_project_artifact_bundle(self.project.id, with_debug_id=True) self.create_project_artifact_bundle(self.project.id, with_debug_id=False) + self.create_project_artifact_bundle(self.project.id, with_debug_id=False) event = self.store_event( data=create_event(), @@ -383,61 +385,6 @@ def test_project_has_some_artifact_bundle_with_a_debug_id_in_newest_bundles(self assert resp.data["has_uploaded_some_artifact_with_a_debug_id"] - def test_project_has_some_artifact_bundle_with_a_debug_id_only_in_older_bundles(self) -> None: - self.create_project_artifact_bundle(self.project.id, with_debug_id=True) - self.create_project_artifact_bundle(self.project.id, with_debug_id=False) - self.create_project_artifact_bundle(self.project.id, with_debug_id=False) - - event = self.store_event( - data=create_event(), - project_id=self.project.id, - ) - - with override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 2}): - resp = self.get_success_response( - self.organization.slug, - self.project.slug, - event.event_id, - ) - assert not resp.data["has_uploaded_some_artifact_with_a_debug_id"] - - with override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 3}): - resp = self.get_success_response( - self.organization.slug, - self.project.slug, - event.event_id, - ) - assert resp.data["has_uploaded_some_artifact_with_a_debug_id"] - - @override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 0}) - def test_project_has_some_artifact_bundle_with_a_debug_id_unbounded(self) -> None: - self.create_project_artifact_bundle(self.project.id, with_debug_id=True) - self.create_project_artifact_bundle(self.project.id, with_debug_id=False) - - event = self.store_event( - data=create_event(), - project_id=self.project.id, - ) - - with CaptureQueriesContext( - connections[router.db_for_read(DebugIdArtifactBundle)] - ) as queries: - resp = self.get_success_response( - self.organization.slug, - self.project.slug, - event.event_id, - ) - - assert resp.data["has_uploaded_some_artifact_with_a_debug_id"] - debug_id_queries = [ - query["sql"] - for query in queries.captured_queries - if 'FROM "sentry_debugidartifactbundle"' in query["sql"] - ] - assert len(debug_id_queries) == 1 - assert '"sentry_debugidartifactbundle"."organization_id"' in debug_id_queries[0] - - @override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 2}) def test_project_has_some_artifact_bundle_with_a_debug_id_in_other_project(self) -> None: other_project = self.create_project(organization=self.organization) self.create_project_artifact_bundle(other_project.id, with_debug_id=True) @@ -456,8 +403,9 @@ def test_project_has_some_artifact_bundle_with_a_debug_id_in_other_project(self) assert not resp.data["has_uploaded_some_artifact_with_a_debug_id"] - @override_options({"sourcemaps.source-map-debug.debug-id-check-max-bundles": 2}) - def test_project_has_some_artifact_bundle_with_a_debug_id_reads_project_bundles(self) -> None: + def test_project_has_some_artifact_bundle_with_a_debug_id_does_not_read_debug_ids( + self, + ) -> None: self.create_project_artifact_bundle(self.project.id, with_debug_id=False) event = self.store_event( @@ -475,15 +423,12 @@ def test_project_has_some_artifact_bundle_with_a_debug_id_reads_project_bundles( ) assert not resp.data["has_uploaded_some_artifact_with_a_debug_id"] - # The organization's debug-ID rows aren't read, only those of the project's bundles. - debug_id_queries = [ + # The links' `has_debug_ids` answers it, without reading any debug-ID rows. + assert not [ query["sql"] for query in queries.captured_queries - if 'FROM "sentry_debugidartifactbundle"' in query["sql"] + if '"sentry_debugidartifactbundle"' in query["sql"] ] - assert len(debug_id_queries) == 1 - assert '"sentry_debugidartifactbundle"."organization_id"' not in debug_id_queries[0] - assert '"sentry_debugidartifactbundle"."artifact_bundle_id" IN' in debug_id_queries[0] def test_multiple_exceptions(self) -> None: event = self.store_event(