Skip to content

ref(releases): Remove DISTINCT query modifier and use EXISTS subqueries to join #95229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

cmanallen
Copy link
Member

Problem statement:

Long running queries are being canceled due to timeouts. Somewhat amazingly DataDog does not report a cost associated with the sort and distinct nodes. I find this hard to believe so I've removed the DISTINCT modifier by using EXISTS subqueries to keep the source dataset de-duplicated. The EXISTS subquery should be a lateral move in terms of performance because both query patterns use a 'Nested Loop Join'. Removing the DISTINCT modifier should be a net benefit all things considered.

Additional Notes:

There are multiple query patterns present in this endpoint. I've document two samples below. This change always lowers the minimum cost to execute the query while mostly not changing the maximum cost. However in some cases the maximum cost is higher. This is not as clear of a win as we saw in #95135. We'll let it ride for a while and monitor its performance. If we see noticeable outliers we may revert but it would have to be in excess of the outliers we see with the current query.

Previous Query Plan Sample (test database):

Unique  (cost=19.01..19.08 rows=1 width=1762)
  Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent, sentry_release.date_added
  ->  Sort  (cost=19.01..19.02 rows=1 width=1762)
        Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent, sentry_release.date_added
        Sort Key: sentry_release.date_added DESC, sentry_release.id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent
        ->  Nested Loop  (cost=0.29..19.00 rows=1 width=1762)
              Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent, sentry_release.date_added
              Inner Unique: true
              ->  Index Scan Backward using sentry_rele_organiz_4ed947_idx on public.sentry_release  (cost=0.14..8.16 rows=1 width=1754)
                    Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent
                    Index Cond: (sentry_release.organization_id = '4556390080839680'::bigint)
                    Filter: ((sentry_release.status = 0) OR (sentry_release.status IS NULL))
              ->  Index Only Scan using sentry_release_project_project_id_release_id_44ff55de_uniq on public.sentry_release_project  (cost=0.15..8.17 rows=1 width=8)
                    Output: sentry_release_project.project_id, sentry_release_project.release_id
                    Index Cond: ((sentry_release_project.project_id = '4556390080905217'::bigint) AND (sentry_release_project.release_id = sentry_release.id))

New Query Plan Sample (test database):

Nested Loop  (cost=0.29..19.00 rows=1 width=1762)
  Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent, sentry_release.date_added
  Inner Unique: true
  ->  Index Scan Backward using sentry_rele_organiz_4ed947_idx on public.sentry_release  (cost=0.14..8.16 rows=1 width=1754)
        Output: sentry_release.id, sentry_release.organization_id, sentry_release.status, sentry_release.version, sentry_release.ref, sentry_release.url, sentry_release.date_added, sentry_release.date_started, sentry_release.date_released, sentry_release.data, sentry_release.owner_id, sentry_release.commit_count, sentry_release.last_commit_id, sentry_release.authors, sentry_release.total_deploys, sentry_release.last_deploy_id, sentry_release.package, sentry_release.major, sentry_release.minor, sentry_release.patch, sentry_release.revision, sentry_release.prerelease, sentry_release.build_code, sentry_release.build_number, sentry_release.user_agent
        Index Cond: (sentry_release.organization_id = '4556390078349312'::bigint)
        Filter: ((sentry_release.status = 0) OR (sentry_release.status IS NULL))
  ->  Index Only Scan using sentry_release_project_project_id_release_id_44ff55de_uniq on public.sentry_release_project u0  (cost=0.15..8.17 rows=1 width=8)
        Output: u0.project_id, u0.release_id
        Index Cond: ((u0.project_id = '4556390078349314'::bigint) AND (u0.release_id = sentry_release.id))

@cmanallen cmanallen requested review from a team as code owners July 10, 2025 13:35
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Jul 10, 2025
@cmanallen cmanallen changed the title ref(releases): Optimize slow query ref(releases): Remove DISTINCT query modifier and use EXISTS subqueries to join Jul 10, 2025
cursor[bot]

This comment was marked as outdated.

Copy link

codecov bot commented Jul 10, 2025

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
27192 2 27190 242
View the top 2 failed test(s) by shortest run time
tests.sentry.test_devimports::test_startup_imports[sentry]
Stack Traces | 10.8s run time
#x1B[1m#x1B[31mtests/sentry/test_devimports.py#x1B[0m:111: in test_startup_imports
    validate_package(pkg, EXCLUDED, XFAIL)
#x1B[1m#x1B[31mtests/sentry/test_devimports.py#x1B[0m:106: in validate_package
    raise AssertionError(ret.stdout)
#x1B[1m#x1B[31mE   AssertionError: Traceback (most recent call last):#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 19, in <module>#x1B[0m
#x1B[1m#x1B[31mE       import sentry.conf.server_mypy#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/conf/server_mypy.py", line 3, in <module>#x1B[0m
#x1B[1m#x1B[31mE       configure(skip_service_validation=True)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/__init__.py", line 33, in configure#x1B[0m
#x1B[1m#x1B[31mE       _configure(ctx, py, yaml, skip_service_validation)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/settings.py", line 124, in configure#x1B[0m
#x1B[1m#x1B[31mE       initialize_app(#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~~^#x1B[0m
#x1B[1m#x1B[31mE           {"config_path": py, "settings": settings, "options": yaml},#x1B[0m
#x1B[1m#x1B[31mE           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE           skip_service_validation=skip_service_validation,#x1B[0m
#x1B[1m#x1B[31mE           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE       )#x1B[0m
#x1B[1m#x1B[31mE       ^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/initializer.py", line 363, in initialize_app#x1B[0m
#x1B[1m#x1B[31mE       django.setup()#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../site-packages/django/__init__.py", line 24, in setup#x1B[0m
#x1B[1m#x1B[31mE       apps.populate(settings.INSTALLED_APPS)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../django/apps/registry.py", line 116, in populate#x1B[0m
#x1B[1m#x1B[31mE       app_config.import_models()#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~~~~~~~~~~~~^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../django/apps/config.py", line 269, in import_models#x1B[0m
#x1B[1m#x1B[31mE       self.models_module = import_module(models_module_name)#x1B[0m
#x1B[1m#x1B[31mE                            ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/importlib/__init__.py", line 88, in import_module#x1B[0m
#x1B[1m#x1B[31mE       return _bootstrap._gcd_import(name[level:], package, level)#x1B[0m
#x1B[1m#x1B[31mE              ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/__init__.py", line 38, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from .group import *  # NOQA#x1B[0m
#x1B[1m#x1B[31mE       ^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/group.py", line 24, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry import eventstore, eventtypes, options, tagstore#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/__init__.py", line 3, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from .base import EventStorage, Filter  # NOQA#x1B[0m
#x1B[1m#x1B[31mE       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/base.py", line 12, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.eventstore.models import Event, GroupEvent#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/models.py", line 21, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.variants import BaseVariant#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/variants.py", line 7, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.component import (#x1B[0m
#x1B[1m#x1B[31mE       ...<4 lines>...#x1B[0m
#x1B[1m#x1B[31mE       )#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/component.py", line 9, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.utils import hash_from_values#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/utils.py", line 13, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.stacktraces.processing import get_crash_frame_from_event_data#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/stacktraces/processing.py", line 12, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.models.release import Release#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/release.py", line 16, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from django_stubs_ext import QuerySetAny#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 13, in _import#x1B[0m
#x1B[1m#x1B[31mE       raise ImportError(f'disallowed dev import: {name}')#x1B[0m
#x1B[1m#x1B[31mE   ImportError: disallowed dev import: django_stubs_ext#x1B[0m
tests.sentry.test_devimports::test_startup_imports[sentry_plugins]
Stack Traces | 11.5s run time
#x1B[1m#x1B[31mtests/sentry/test_devimports.py#x1B[0m:111: in test_startup_imports
    validate_package(pkg, EXCLUDED, XFAIL)
#x1B[1m#x1B[31mtests/sentry/test_devimports.py#x1B[0m:106: in validate_package
    raise AssertionError(ret.stdout)
#x1B[1m#x1B[31mE   AssertionError: Traceback (most recent call last):#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 19, in <module>#x1B[0m
#x1B[1m#x1B[31mE       import sentry.conf.server_mypy#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/conf/server_mypy.py", line 3, in <module>#x1B[0m
#x1B[1m#x1B[31mE       configure(skip_service_validation=True)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/__init__.py", line 33, in configure#x1B[0m
#x1B[1m#x1B[31mE       _configure(ctx, py, yaml, skip_service_validation)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/settings.py", line 124, in configure#x1B[0m
#x1B[1m#x1B[31mE       initialize_app(#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~~^#x1B[0m
#x1B[1m#x1B[31mE           {"config_path": py, "settings": settings, "options": yaml},#x1B[0m
#x1B[1m#x1B[31mE           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE           skip_service_validation=skip_service_validation,#x1B[0m
#x1B[1m#x1B[31mE           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE       )#x1B[0m
#x1B[1m#x1B[31mE       ^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/runner/initializer.py", line 363, in initialize_app#x1B[0m
#x1B[1m#x1B[31mE       django.setup()#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../site-packages/django/__init__.py", line 24, in setup#x1B[0m
#x1B[1m#x1B[31mE       apps.populate(settings.INSTALLED_APPS)#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../django/apps/registry.py", line 116, in populate#x1B[0m
#x1B[1m#x1B[31mE       app_config.import_models()#x1B[0m
#x1B[1m#x1B[31mE       ~~~~~~~~~~~~~~~~~~~~~~~~^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/sentry/.venv/lib/python3.13.../django/apps/config.py", line 269, in import_models#x1B[0m
#x1B[1m#x1B[31mE       self.models_module = import_module(models_module_name)#x1B[0m
#x1B[1m#x1B[31mE                            ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/importlib/__init__.py", line 88, in import_module#x1B[0m
#x1B[1m#x1B[31mE       return _bootstrap._gcd_import(name[level:], package, level)#x1B[0m
#x1B[1m#x1B[31mE              ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/__init__.py", line 38, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from .group import *  # NOQA#x1B[0m
#x1B[1m#x1B[31mE       ^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/group.py", line 24, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry import eventstore, eventtypes, options, tagstore#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/__init__.py", line 3, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from .base import EventStorage, Filter  # NOQA#x1B[0m
#x1B[1m#x1B[31mE       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/base.py", line 12, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.eventstore.models import Event, GroupEvent#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/eventstore/models.py", line 21, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.variants import BaseVariant#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/variants.py", line 7, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.component import (#x1B[0m
#x1B[1m#x1B[31mE       ...<4 lines>...#x1B[0m
#x1B[1m#x1B[31mE       )#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/component.py", line 9, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.grouping.utils import hash_from_values#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/grouping/utils.py", line 13, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.stacktraces.processing import get_crash_frame_from_event_data#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/stacktraces/processing.py", line 12, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from sentry.models.release import Release#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 15, in _import#x1B[0m
#x1B[1m#x1B[31mE       return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)#x1B[0m
#x1B[1m#x1B[31mE     File ".../sentry/models/release.py", line 16, in <module>#x1B[0m
#x1B[1m#x1B[31mE       from django_stubs_ext import QuerySetAny#x1B[0m
#x1B[1m#x1B[31mE     File "<string>", line 13, in _import#x1B[0m
#x1B[1m#x1B[31mE       raise ImportError(f'disallowed dev import: {name}')#x1B[0m
#x1B[1m#x1B[31mE   ImportError: disallowed dev import: django_stubs_ext#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

cursor[bot]

This comment was marked as outdated.

@cmanallen
Copy link
Member Author

Re Cursor:

If get_filter_params fails to populate environment_objects correctly, filtering will silently fail, returning unfiltered results.

This is wrong. If environments can not be fetched the resource aborts 404. Even if it didn't abort environment names are only appended to the filter_params if all the environments are returned.

This also changes behavior: an empty environment list now skips filtering entirely, whereas previously it would filter out all results.

This is wrong. Previous query pattern checked if the environments key existed prior to applying filters.

For the non-flatten case, the new filter_releases_by_projects() function now returns all releases when the project list is empty, instead of correctly returning no results.

This is wrong. Project ids will always be populated in this context. If not an exception is raised:

if not projects:
raise NoProjects

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: API Breaks on Empty Environment Filters

The filter_releases_by_environments function incorrectly skips filtering when environment_ids is empty. Previously, an empty environment filter would return no results (filtering out all releases). Now, it returns unfiltered results, which is a breaking change in API behavior for the OrganizationReleasesEndpoint and OrganizationReleasesStatsEndpoint. This can lead to unexpected data exposure when users apply empty or invalid environment filters.

src/sentry/models/release.py#L839-L857

def filter_releases_by_environments(
queryset: QuerySetAny,
project_ids: list[int],
environment_ids: list[int],
):
"""Return a release queryset filtered by environments."""
if not environment_ids:
return queryset
return queryset.filter(
Exists(
ReleaseProjectEnvironment.objects.filter(
release=OuterRef("pk"),
environment_id__in=environment_ids,
project_id__in=project_ids,
)
)
)

src/sentry/api/endpoints/organization_releases.py#L315-L320

queryset = Release.objects.filter(organization_id=organization.id)
queryset = filter_releases_by_environments(
queryset,
filter_params["project_id"],
[e.id for e in filter_params.get("environment_objects", [])],
)

src/sentry/api/endpoints/organization_releases.py#L678-L683

queryset = filter_releases_by_projects(queryset, filter_params["project_id"])
queryset = filter_releases_by_environments(
queryset,
filter_params["project_id"],
[e.id for e in filter_params.get("environment_objects", [])],
)

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Backend Automatically applied to PRs that change backend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant