Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 2 additions & 40 deletions src/sentry/api/endpoints/source_map_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
6 changes: 2 additions & 4 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
75 changes: 10 additions & 65 deletions tests/sentry/api/endpoints/test_source_map_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(),
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Loading