diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index ccc06ad9b0d2c5..b6247777dae29f 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -2132,7 +2132,6 @@ tests/sentry/models/test_commitfilechange.py tests/sentry/models/test_dashboard.py tests/sentry/models/test_debugfile.py tests/sentry/models/test_deploy.py -tests/sentry/models/test_dynamicsampling.py tests/sentry/models/test_environment.py tests/sentry/models/test_eventattachment.py tests/sentry/models/test_eventerror.py diff --git a/eslint.config.ts b/eslint.config.ts index f2f08d52d3aeb3..55e81a0cff8237 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -539,8 +539,13 @@ export default typescript.config([ '@tanstack/query': pluginQuery, }, rules: { - ...pluginQuery.configs.recommended.rules, + ...pluginQuery.configs.recommendedStrict.rules, + '@tanstack/query/prefer-query-options': 'off', '@tanstack/query/no-rest-destructuring': 'error', + '@tanstack/query/exhaustive-deps': [ + 'error', + {allowlist: {variables: ['api'], types: ['Client']}}, + ], }, }, { diff --git a/package.json b/package.json index 4c341ab8ef6ee9..ff38e7f980eac3 100644 --- a/package.json +++ b/package.json @@ -121,15 +121,15 @@ "@stripe/react-stripe-js": "^3.9.2", "@stripe/stripe-js": "^5.10.0", "@swc/plugin-emotion": "14.3.0", - "@tanstack/query-async-storage-persister": "5.83.1", + "@tanstack/query-async-storage-persister": "5.96.0", "@tanstack/react-devtools": "0.9.9", "@tanstack/react-form": "1.28.6", "@tanstack/react-form-devtools": "0.2.20", "@tanstack/react-pacer": "^0.17.0", "@tanstack/react-pacer-devtools": "0.5.3", - "@tanstack/react-query": "5.85.0", - "@tanstack/react-query-devtools": "5.85.0", - "@tanstack/react-query-persist-client": "5.85.0", + "@tanstack/react-query": "5.96.0", + "@tanstack/react-query-devtools": "5.96.0", + "@tanstack/react-query-persist-client": "5.96.0", "@tanstack/react-virtual": "^3.13.6", "@types/gtag.js": "^0.0.12", "@types/history": "^3.2.5", @@ -239,7 +239,7 @@ "@sentry/jest-environment": "6.1.0", "@sentry/profiling-node": "10.41.0-beta.0", "@styled/typescript-styled-plugin": "^1.0.1", - "@tanstack/eslint-plugin-query": "5.83.1", + "@tanstack/eslint-plugin-query": "5.96.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02428225b1b1b0..2f4a3bc04787ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,8 +226,8 @@ importers: specifier: 14.3.0 version: 14.3.0 '@tanstack/query-async-storage-persister': - specifier: 5.83.1 - version: 5.83.1 + specifier: 5.96.0 + version: 5.96.0 '@tanstack/react-devtools': specifier: 0.9.9 version: 0.9.9(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11) @@ -244,14 +244,14 @@ importers: specifier: 0.5.3 version: 0.5.3(@tanstack/pacer@0.16.0)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11) '@tanstack/react-query': - specifier: 5.85.0 - version: 5.85.0(react@19.2.3) + specifier: 5.96.0 + version: 5.96.0(react@19.2.3) '@tanstack/react-query-devtools': - specifier: 5.85.0 - version: 5.85.0(@tanstack/react-query@5.85.0(react@19.2.3))(react@19.2.3) + specifier: 5.96.0 + version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.3))(react@19.2.3) '@tanstack/react-query-persist-client': - specifier: 5.85.0 - version: 5.85.0(@tanstack/react-query@5.85.0(react@19.2.3))(react@19.2.3) + specifier: 5.96.0 + version: 5.96.0(@tanstack/react-query@5.96.0(react@19.2.3))(react@19.2.3) '@tanstack/react-virtual': specifier: ^3.13.6 version: 3.13.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -575,8 +575,8 @@ importers: specifier: ^1.0.1 version: 1.0.1 '@tanstack/eslint-plugin-query': - specifier: 5.83.1 - version: 5.83.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + specifier: 5.96.0 + version: 5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -3684,10 +3684,14 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/eslint-plugin-query@5.83.1': - resolution: {integrity: sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==} + '@tanstack/eslint-plugin-query@5.96.0': + resolution: {integrity: sha512-iPxSM1lNBzz63scYaudGeJk4yqb51MpvnX2GUmBjEvoLLwcaCTZg+OQmjmaoe5o3TApQZK8INYnmYhN4p4uxuQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: ^5.4.0 + peerDependenciesMeta: + typescript: + optional: true '@tanstack/form-core@1.28.6': resolution: {integrity: sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==} @@ -3711,17 +3715,17 @@ packages: resolution: {integrity: sha512-0tvLUQcUW2bqdsjf2IUeBgYgxWMjC+XNDpN2kovcfrOAv5baFBaenUtsMxi0QxhWmnjdrMPyuTAI0ZIzpkcmpw==} engines: {node: '>=18'} - '@tanstack/query-async-storage-persister@5.83.1': - resolution: {integrity: sha512-P3QzGBn9/nxaCst1hNYDQbbJDKFxTBsGTMjf0YtsKs7sSeNp7wVgUKjOVJpbvhdpFKHqTDYOdjOf2Qeb4bJ9tQ==} + '@tanstack/query-async-storage-persister@5.96.0': + resolution: {integrity: sha512-Xlzt5UFyAkSDkZ7DwlgpPU6AHMRwn6RKp+fbyuburrPhy94/oHbX+X4jNoq+fdPzSYC8U1Y6yOfpkvRAov7ZaQ==} - '@tanstack/query-core@5.83.1': - resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} + '@tanstack/query-core@5.96.0': + resolution: {integrity: sha512-sfO3uQeol1BU7cRP6NYY7nAiX3GiNY20lI/dtSbKLwcIkYw/X+w/tEsQAkc544AfIhBX/IvH/QYtPHrPhyAKGw==} - '@tanstack/query-devtools@5.84.0': - resolution: {integrity: sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==} + '@tanstack/query-devtools@5.96.0': + resolution: {integrity: sha512-MEdO1M/9ItB62OtTqVo8AIj/G6vJemA642N56bw8aIqpXKIj5VG/3xWgh2piw76NmoCIlapxjjWp1MMLmrvKJw==} - '@tanstack/query-persist-client-core@5.83.1': - resolution: {integrity: sha512-GPWt1tj8kmo3LA1WPpSmJA3JGCdQfaggb1LheFEfr3RuwbTchWd09xD/fZ40m9ai0pJupvyguLiWF8On8sQWPw==} + '@tanstack/query-persist-client-core@5.96.0': + resolution: {integrity: sha512-iSqqhWtQ7sZqucmOIbC+eU+npaP36ZARtyjEfaDL/9A8f8P70NHe3RJ3N7fkxbkc36pQd+duANo5TTnq8yCZSg==} '@tanstack/react-devtools@0.9.9': resolution: {integrity: sha512-w5J5n3tPLfGVRSwV5S05Fg1awa7SJssdyNh/3KPDHU72DPUvH9v5mPGRrbTxsoFWvh3djFTtF9tCX1tCnyoOuQ==} @@ -3762,20 +3766,20 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-query-devtools@5.85.0': - resolution: {integrity: sha512-Q/lmGAY2I3KkhxSJKLKQUeUBOc9Mv/OrCTw4CfUCq2Za+XhDsB5ZfVTOANAJyDZ+SiUu27Cw1eHNE+xJdACJiw==} + '@tanstack/react-query-devtools@5.96.0': + resolution: {integrity: sha512-P0WFX0s3iYii4oZTSCK9T3/PBQ9uY/SVTzcZFbyzCo5ujeIAsqos3HjBWoF/lhJXWVe8UXkjAmgXr3TUD11q2A==} peerDependencies: - '@tanstack/react-query': ^5.85.0 + '@tanstack/react-query': ^5.96.0 react: ^18 || ^19 - '@tanstack/react-query-persist-client@5.85.0': - resolution: {integrity: sha512-ZzkOwWa1k9KNCjiet70Kg447qCxImmXsRdYuIHuc3ctgyq+o8n4RXDfza23tA0riM1iR5ouYeSRVoDZNegXZ/w==} + '@tanstack/react-query-persist-client@5.96.0': + resolution: {integrity: sha512-lXOIHU2i+GAG7Gm3OEMmw3xmD51H/8i99tIFaBcGW4mF0Wq91q3Q78xf5q7Cu0NI8WRcbxi7Dn6h1Fs9zMNw0A==} peerDependencies: - '@tanstack/react-query': ^5.85.0 + '@tanstack/react-query': ^5.96.0 react: ^18 || ^19 - '@tanstack/react-query@5.85.0': - resolution: {integrity: sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw==} + '@tanstack/react-query@5.96.0': + resolution: {integrity: sha512-6qbjdm1K5kizVKv9TNqhIN3doq2anRhdF2XaFMFSn4m8L22S69RV+FilvlyVT4RoJyMxtPU5rs4RpdFa/PEC7A==} peerDependencies: react: ^18 || ^19 @@ -13399,13 +13403,14 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.83.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/utils': 8.56.1(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.34.0(jiti@2.6.1) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript '@tanstack/form-core@1.28.6': dependencies: @@ -13453,17 +13458,18 @@ snapshots: '@tanstack/devtools-event-client': 0.3.4 '@tanstack/store': 0.7.7 - '@tanstack/query-async-storage-persister@5.83.1': + '@tanstack/query-async-storage-persister@5.96.0': dependencies: - '@tanstack/query-persist-client-core': 5.83.1 + '@tanstack/query-core': 5.96.0 + '@tanstack/query-persist-client-core': 5.96.0 - '@tanstack/query-core@5.83.1': {} + '@tanstack/query-core@5.96.0': {} - '@tanstack/query-devtools@5.84.0': {} + '@tanstack/query-devtools@5.96.0': {} - '@tanstack/query-persist-client-core@5.83.1': + '@tanstack/query-persist-client-core@5.96.0': dependencies: - '@tanstack/query-core': 5.83.1 + '@tanstack/query-core': 5.96.0 '@tanstack/react-devtools@0.9.9(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11)': dependencies: @@ -13520,21 +13526,21 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@tanstack/react-query-devtools@5.85.0(@tanstack/react-query@5.85.0(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-devtools@5.96.0(@tanstack/react-query@5.96.0(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-devtools': 5.84.0 - '@tanstack/react-query': 5.85.0(react@19.2.3) + '@tanstack/query-devtools': 5.96.0 + '@tanstack/react-query': 5.96.0(react@19.2.3) react: 19.2.3 - '@tanstack/react-query-persist-client@5.85.0(@tanstack/react-query@5.85.0(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-persist-client@5.96.0(@tanstack/react-query@5.96.0(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-persist-client-core': 5.83.1 - '@tanstack/react-query': 5.85.0(react@19.2.3) + '@tanstack/query-persist-client-core': 5.96.0 + '@tanstack/react-query': 5.96.0(react@19.2.3) react: 19.2.3 - '@tanstack/react-query@5.85.0(react@19.2.3)': + '@tanstack/react-query@5.96.0(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.83.1 + '@tanstack/query-core': 5.96.0 react: 19.2.3 '@tanstack/react-store@0.7.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': diff --git a/src/sentry/dynamic_sampling/__init__.py b/src/sentry/dynamic_sampling/__init__.py index e43f7e7c529f25..6431f4973034e8 100644 --- a/src/sentry/dynamic_sampling/__init__.py +++ b/src/sentry/dynamic_sampling/__init__.py @@ -14,7 +14,6 @@ RuleType, get_enabled_user_biases, get_redis_client_for_ds, - get_rule_hash, get_supported_biases_ids, get_user_biases, ) @@ -25,7 +24,6 @@ "get_user_biases", "get_enabled_user_biases", "get_redis_client_for_ds", - "get_rule_hash", "record_latest_release", "RuleType", "ExtendedBoostedRelease", diff --git a/src/sentry/dynamic_sampling/rules/utils.py b/src/sentry/dynamic_sampling/rules/utils.py index d8c51a96c636be..8329911463ae7c 100644 --- a/src/sentry/dynamic_sampling/rules/utils.py +++ b/src/sentry/dynamic_sampling/rules/utils.py @@ -3,14 +3,14 @@ from enum import Enum from typing import Literal, NotRequired, TypedDict, Union -import orjson from django.conf import settings from redis import StrictRedis -from sentry.models.dynamicsampling import CUSTOM_RULE_START from sentry.relay.types import RuleCondition from sentry.utils import redis +CUSTOM_RULE_START = 3000 + BOOSTED_RELEASES_LIMIT = 10 LATEST_RELEASES_BOOST_FACTOR = 1.5 @@ -117,23 +117,6 @@ class DecayingRule(Rule): PolymorphicRule = Union[Rule, DecayingRule] -def get_rule_hash(rule: PolymorphicRule) -> int: - # We want to be explicit in what we use for computing the hash. In addition, we need to remove certain fields like - # the sampleRate. - return ( - orjson.dumps( - { - "id": rule["id"], - "type": rule["type"], - "condition": rule["condition"], - }, - option=orjson.OPT_SORT_KEYS, - ) - .decode() - .__hash__() - ) - - def get_user_biases(user_set_biases: list[ActivatableBias] | None) -> list[ActivatableBias]: if user_set_biases is None: return DEFAULT_BIASES diff --git a/src/sentry/hybridcloud/apigateway_async/proxy.py b/src/sentry/hybridcloud/apigateway_async/proxy.py index 254ab6bf59dfac..bb65b4034dadce 100644 --- a/src/sentry/hybridcloud/apigateway_async/proxy.py +++ b/src/sentry/hybridcloud/apigateway_async/proxy.py @@ -12,6 +12,7 @@ import httpx from asgiref.sync import sync_to_async from django.conf import settings +from django.core.exceptions import RequestAborted from django.http import HttpRequest, HttpResponse, JsonResponse, StreamingHttpResponse from django.http.response import HttpResponseBase @@ -196,7 +197,10 @@ async def proxy_cell_request( metrics.incr("apigateway.proxy.request_failed", tags=metric_tags) circuitbreaker.incr_failures() return _adapt_response(resp, target_url) - except (httpx.TimeoutException, asyncio.CancelledError): + except asyncio.CancelledError: + metrics.incr("apigateway.proxy.request_aborted", tags=metric_tags) + raise RequestAborted() + except httpx.TimeoutException: metrics.incr("apigateway.proxy.request_timeout", tags=metric_tags) circuitbreaker.incr_failures() # remote silo timeout. Use DRF timeout instead diff --git a/src/sentry/models/dynamicsampling.py b/src/sentry/models/dynamicsampling.py index c4e48774fd0398..9ed171a1bc2524 100644 --- a/src/sentry/models/dynamicsampling.py +++ b/src/sentry/models/dynamicsampling.py @@ -1,67 +1,12 @@ from __future__ import annotations -import hashlib -from collections.abc import Mapping, Sequence -from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any - -from django.db import models, router, transaction -from django.db.models import F, IntegerField, Max, Q, Subquery, Value -from django.db.models.functions import Coalesce +from django.db import models +from django.db.models import Q from django.utils import timezone from sentry.backup.scopes import RelocationScope -from sentry.constants import ObjectStatus from sentry.db.models import FlexibleForeignKey, Model, cell_silo_model from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.utils import json, metrics - -if TYPE_CHECKING: - from sentry.models.organization import Organization - from sentry.models.project import Project - -# max number of custom rules that can be created per organization -MAX_CUSTOM_RULES = 2000 -CUSTOM_RULE_START = 3000 -MAX_CUSTOM_RULES_PER_PROJECT = 50 -CUSTOM_RULE_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" - - -class TooManyRules(ValueError): - """ - Raised when a there is already the max number of rules active for an organization - """ - - -def get_rule_hash(condition: Any, project_ids: Sequence[int]) -> str: - """ - Returns the hash of the rule based on the condition and projects - """ - condition_string = to_order_independent_string(condition) - project_string = to_order_independent_string(list(project_ids)) - rule_string = f"{condition_string}-{project_string}" - # make it a bit shorter - return hashlib.sha1(rule_string.encode("utf-8")).hexdigest() - - -def to_order_independent_string(val: Any) -> str: - """ - Converts a value in an order independent string and then hashes it - - Note: this will insure the same repr is generated for ['x', 'y'] and ['y', 'x'] - Also the same repr is generated for {'x': 1, 'y': 2} and {'y': 2, 'x': 1} - """ - ret_val = "" - if isinstance(val, Mapping): - for key in sorted(val.keys()): - ret_val += f"{key}:{to_order_independent_string(val[key])}-" - elif isinstance(val, (list, tuple)): - vals = sorted([to_order_independent_string(item) for item in val]) - for item in vals: - ret_val += f"{item}-" - else: - ret_val = str(val) - return ret_val @cell_silo_model @@ -114,16 +59,6 @@ class CustomDynamicSamplingRule(Model): created_by_id = HybridCloudForeignKey("sentry.User", on_delete="CASCADE", null=True, blank=True) notification_sent = models.BooleanField(null=True, blank=True) - @property - def external_rule_id(self) -> int: - """ - Returns the external rule id - - For external users, i.e. Relay, we need to shift the ids since the slot we - have allocated starts at the offset specified in RESERVED_IDS. - """ - return self.rule_id + CUSTOM_RULE_START - class Meta: app_label = "sentry" db_table = "sentry_customdynamicsamplingrule" @@ -137,230 +72,3 @@ class Meta: fields=["condition_hash"], name="condition_hash_idx", condition=Q(is_active=True) ), ] - - @staticmethod - def get_rule_for_org( - condition: Any, - organization_id: int, - project_ids: Sequence[int], - ) -> CustomDynamicSamplingRule | None: - """ - Returns an active rule for the given condition and organization if it exists otherwise None - - Note: There should not be more than one active rule for a given condition and organization - This function doesn't verify this condition, it just returns the first one. - """ - rule_hash = get_rule_hash(condition, project_ids) - rules = CustomDynamicSamplingRule.objects.filter( - organization_id=organization_id, - condition_hash=rule_hash, - is_active=True, - end_date__gt=timezone.now(), - )[:1] - - return rules[0] if rules else None - - @staticmethod - def update_or_create( - condition: Any, - start: datetime, - end: datetime, - project_ids: Sequence[int], - organization_id: int, - num_samples: int, - sample_rate: float, - query: str, - created_by_id: int | None = None, - ) -> CustomDynamicSamplingRule: - from sentry.models.organization import Organization - from sentry.models.project import Project - - with transaction.atomic(router.db_for_write(CustomDynamicSamplingRule)): - # check if rule already exists for this organization - existing_rule = CustomDynamicSamplingRule.get_rule_for_org( - condition, organization_id, project_ids - ) - - if existing_rule is not None: - # we already have an active rule for this condition and this organization - # update the expiration date and ensure that our projects are included - existing_rule.end_date = max(end, existing_rule.end_date) - existing_rule.num_samples = max(num_samples, existing_rule.num_samples) - existing_rule.sample_rate = max(sample_rate, existing_rule.sample_rate) - - # for org rules we don't need to do anything with the projects - existing_rule.save() - return existing_rule - else: - projects = Project.objects.get_many_from_cache(project_ids) - projects = list(projects) - organization = Organization.objects.get_from_cache(id=organization_id) - - if CustomDynamicSamplingRule.per_project_limit_reached(projects, organization): - raise TooManyRules() - - # create a new rule - rule_hash = get_rule_hash(condition, project_ids) - is_org_level = len(project_ids) == 0 - condition_str = json.dumps(condition) - rule = CustomDynamicSamplingRule.objects.create( - organization_id=organization_id, - condition=condition_str, - sample_rate=sample_rate, - start_date=start, - end_date=end, - num_samples=num_samples, - condition_hash=rule_hash, - is_active=True, - is_org_level=is_org_level, - query=query, - notification_sent=False, - created_by_id=created_by_id, - ) - - rule.save() - # now try to assign a rule id - id = rule.assign_rule_id() - if id > MAX_CUSTOM_RULES: - # we have too many rules, delete this one - rule.delete() - raise TooManyRules() - - # set the projects if not org level - for project in projects: - CustomDynamicSamplingRuleProject.objects.create( - custom_dynamic_sampling_rule=rule, project=project - ) - return rule - - def assign_rule_id(self) -> int: - """ - Assigns the smallest rule id that is not taken in the - current organization. - """ - if self.id is None: - raise ValueError("Cannot assign rule id to unsaved object") - if self.rule_id != 0: - raise ValueError("Cannot assign rule id to object that already has a rule id") - - now = timezone.now() - - base_qs = CustomDynamicSamplingRule.objects.filter( - organization_id=self.organization.id, end_date__gt=now, is_active=True - ) - - # We want to find the smallest free rule id. We do this by self-joining with rule_id + 1 and excluding the existing rule_ids. - # We then order by rule_id_plus_one and take the first value. - # This also works for the first rule, as it is pre-initialized with 0, and will thus end up with 1. - new_rule_id_subquery = Subquery( - base_qs.annotate(rule_id_plus_one=F("rule_id") + 1) - .exclude(rule_id_plus_one__in=base_qs.values_list("rule_id", flat=True)) - .order_by("rule_id_plus_one") - .values("rule_id_plus_one")[:1] - ) - - max_rule_id = base_qs.aggregate(Max("rule_id"))["rule_id__max"] or 0 - fallback_value = Value(max_rule_id + 1, output_field=IntegerField()) - - safe_new_rule_id = Coalesce(new_rule_id_subquery, fallback_value) - - # Update this instance with the new rule_id - CustomDynamicSamplingRule.objects.filter(id=self.id).update(rule_id=safe_new_rule_id) - self.refresh_from_db() - return self.rule_id - - @staticmethod - def deactivate_old_rules() -> None: - """ - Deactivates all rules expired rules (this is just an optimization to remove old rules from indexes). - - This should be called periodically to clean up old rules (it is not necessary to call it for correctness, - just for performance) - """ - CustomDynamicSamplingRule.objects.filter( - # give it a minute grace period to make sure we don't deactivate rules that are still active - end_date__lt=timezone.now() - timedelta(minutes=1), - ).update(is_active=False) - - @staticmethod - def get_project_rules( - project: Project, - ) -> Sequence[CustomDynamicSamplingRule]: - """ - Returns all active project rules - """ - now = timezone.now() - # org rules ( apply to all projects in the org) - org_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=True, - organization=project.organization, - end_date__gt=now, - start_date__lt=now, - )[: MAX_CUSTOM_RULES_PER_PROJECT + 1] - - # project rules - project_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - projects__in=[project], - end_date__gt=now, - start_date__lt=now, - )[: MAX_CUSTOM_RULES_PER_PROJECT + 1] - - rules = list(project_rules.union(org_rules)[: MAX_CUSTOM_RULES_PER_PROJECT + 1]) - - if len(rules) > MAX_CUSTOM_RULES_PER_PROJECT: - metrics.incr("dynamic_sampling.custom_rules.overflow") - - return rules[:MAX_CUSTOM_RULES_PER_PROJECT] - - @staticmethod - def deactivate_expired_rules() -> None: - """ - Deactivates all rules that have expired - """ - CustomDynamicSamplingRule.objects.filter( - end_date__lt=timezone.now(), is_active=True - ).update(is_active=False) - - @staticmethod - def num_active_rules_for_project(project: Project) -> int: - """ - Returns the number of active rules for the given project - """ - now = timezone.now() - - num_org_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=True, - organization=project.organization, - end_date__gt=now, - start_date__lte=now, - ).count() - - num_proj_rules = CustomDynamicSamplingRule.objects.filter( - is_active=True, - is_org_level=False, - projects__in=[project], - end_date__gt=now, - start_date__lte=now, - ).count() - - return num_proj_rules + num_org_rules - - @staticmethod - def per_project_limit_reached(projects: Sequence[Project], organization: Organization) -> bool: - """ - Returns True if the rule limit is reached for any of the given projects (or all - the projects in the organization if org level rule) - """ - projects = list(projects) - if len(projects) == 0: - # an org rule check all the org projects - org_projects = organization.project_set.filter(status=ObjectStatus.ACTIVE) - projects = list(org_projects) - for project in projects: - num_rules = CustomDynamicSamplingRule.num_active_rules_for_project(project) - if num_rules >= MAX_CUSTOM_RULES_PER_PROJECT: - return True - return False diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 51c6cadabfdd8b..3b980ccdc62eef 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -77,7 +77,10 @@ DashboardWidgetQueryOnDemand, DashboardWidgetTypes, ) -from sentry.models.dynamicsampling import CustomDynamicSamplingRule +from sentry.models.dynamicsampling import ( + CustomDynamicSamplingRule, + CustomDynamicSamplingRuleProject, +) from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewProject @@ -520,17 +523,6 @@ def create_exhaustive_organization( sent_initial_email_date=timezone.now(), sent_final_email_date=timezone.now(), ) - CustomDynamicSamplingRule.update_or_create( - created_by_id=owner_id, - condition={"op": "equals", "name": "environment", "value": "prod"}, - start=timezone.now(), - end=timezone.now() + timedelta(hours=1), - project_ids=[project.id], - organization_id=org.id, - num_samples=100, - sample_rate=0.5, - query="environment:prod event.type:transaction", - ) # Environment* self.create_environment(project=project) @@ -827,6 +819,20 @@ def create_exhaustive_organization( overrides={"write_key": "test_override_write_key"}, ) + custom_rule = CustomDynamicSamplingRule.objects.create( + organization=org, + created_by_id=owner_id, + condition='{"op":"and","inner":[]}', + end_date=timezone.now() + timedelta(days=1), + num_samples=100, + condition_hash="abc123def456abc123def456abc123def4560000", + sample_rate=0.5, + ) + CustomDynamicSamplingRuleProject.objects.create( + custom_dynamic_sampling_rule=custom_rule, + project=project, + ) + return org @assume_test_silo_mode(SiloMode.CONTROL) diff --git a/static/app/actionCreators/events.tsx b/static/app/actionCreators/events.tsx index ff976397dbeca9..aa2b7bd0999908 100644 --- a/static/app/actionCreators/events.tsx +++ b/static/app/actionCreators/events.tsx @@ -317,7 +317,7 @@ export const useDeleteEventAttachmentOptimistic = ( {method: 'DELETE'} ); }, - onMutate: async variables => { + onMutate: async (variables, context) => { await queryClient.cancelQueries({ queryKey: makeFetchEventAttachmentsQueryKey(variables), }); @@ -339,22 +339,22 @@ export const useDeleteEventAttachmentOptimistic = ( } ); - incomingOptions.onMutate?.(variables); + incomingOptions.onMutate?.(variables, context); return {previous}; }, - onError: (error, variables, context) => { + onError: (error, variables, onMutateResult, context) => { addErrorMessage(t('An error occurred while deleting the attachment')); - if (context) { + if (onMutateResult) { setApiQueryData( queryClient, makeFetchEventAttachmentsQueryKey(variables), - context.previous + onMutateResult.previous ); } - incomingOptions.onError?.(error, variables, context); + incomingOptions.onError?.(error, variables, onMutateResult, context); }, }; diff --git a/static/app/components/arithmeticBuilder/index.spec.tsx b/static/app/components/arithmeticBuilder/index.spec.tsx index 41b4d53794eed7..1513315d4e8a6e 100644 --- a/static/app/components/arithmeticBuilder/index.spec.tsx +++ b/static/app/components/arithmeticBuilder/index.spec.tsx @@ -206,8 +206,9 @@ describe('ArithmeticBuilder', () => { const expression = '( sum(span.duration) + count_if(span.op,equals,db) )'; render(); - const rows = screen.getAllByRole('row'); - expect(rows).toHaveLength(11); + await waitFor(() => { + expect(screen.getAllByRole('row')).toHaveLength(11); + }); // the combobox inside the free text tokens will get the focus const freeTextTokens = screen.getAllByRole('combobox', {name: 'Add a term'}); diff --git a/static/app/components/backendJsonFormAdapter/choiceMapperAdapter.spec.tsx b/static/app/components/backendJsonFormAdapter/choiceMapperAdapter.spec.tsx index fcf575fa72e503..ec3ce98392f640 100644 --- a/static/app/components/backendJsonFormAdapter/choiceMapperAdapter.spec.tsx +++ b/static/app/components/backendJsonFormAdapter/choiceMapperAdapter.spec.tsx @@ -189,9 +189,12 @@ describe('ChoiceMapperAdapter', () => { await userEvent.click(await screen.findByText('Closed')); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - status_mapping: {repo1: {on_resolve: 'closed'}}, - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + status_mapping: {repo1: {on_resolve: 'closed'}}, + }, + expect.anything() + ); }); }); @@ -249,9 +252,12 @@ describe('ChoiceMapperAdapter', () => { await userEvent.click(await screen.findByText('Reopened')); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - status_mapping: {repo1: {on_resolve: 'closed', on_unresolve: 'reopened'}}, - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + status_mapping: {repo1: {on_resolve: 'closed', on_unresolve: 'reopened'}}, + }, + expect.anything() + ); }); }); @@ -286,9 +292,12 @@ describe('ChoiceMapperAdapter', () => { await userEvent.click(screen.getByRole('button', {name: 'Delete'})); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - status_mapping: {}, - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + status_mapping: {}, + }, + expect.anything() + ); }); }); @@ -326,9 +335,12 @@ describe('ChoiceMapperAdapter', () => { await userEvent.click(await screen.findByText('Open')); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - status_mapping: {repo1: {on_resolve: 'open'}}, - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + status_mapping: {repo1: {on_resolve: 'open'}}, + }, + expect.anything() + ); }); }); diff --git a/static/app/components/backendJsonFormAdapter/index.spec.tsx b/static/app/components/backendJsonFormAdapter/index.spec.tsx index 6a8881ca125e69..f7549915548824 100644 --- a/static/app/components/backendJsonFormAdapter/index.spec.tsx +++ b/static/app/components/backendJsonFormAdapter/index.spec.tsx @@ -109,9 +109,10 @@ describe('BackendJsonFormAdapter', () => { await userEvent.click(screen.getByRole('checkbox')); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - sync_enabled: true, - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + {sync_enabled: true}, + expect.anything() + ); }); }); diff --git a/static/app/components/backendJsonFormAdapter/projectMapperAdapter.spec.tsx b/static/app/components/backendJsonFormAdapter/projectMapperAdapter.spec.tsx index 05d3feca6c48f2..3856985cd66bd9 100644 --- a/static/app/components/backendJsonFormAdapter/projectMapperAdapter.spec.tsx +++ b/static/app/components/backendJsonFormAdapter/projectMapperAdapter.spec.tsx @@ -132,9 +132,12 @@ describe('ProjectMapperAdapter', () => { await userEvent.click(screen.getByRole('button', {name: 'Add project'})); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - project_mappings: [[101, 'proj-1']], - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + project_mappings: [[101, 'proj-1']], + }, + expect.anything() + ); }); }); @@ -156,9 +159,12 @@ describe('ProjectMapperAdapter', () => { await userEvent.click(deleteButtons[0]!); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - project_mappings: [[102, 'proj-2']], - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + project_mappings: [[102, 'proj-2']], + }, + expect.anything() + ); }); }); diff --git a/static/app/components/backendJsonFormAdapter/tableAdapter.spec.tsx b/static/app/components/backendJsonFormAdapter/tableAdapter.spec.tsx index 337560ae5b5014..cfd9a062e1d58b 100644 --- a/static/app/components/backendJsonFormAdapter/tableAdapter.spec.tsx +++ b/static/app/components/backendJsonFormAdapter/tableAdapter.spec.tsx @@ -128,9 +128,14 @@ describe('TableAdapter', () => { await userEvent.click(document.body); // blur await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - service_table: [{id: '1', service: 'Updated Service', integration_key: 'abc123'}], - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + service_table: [ + {id: '1', service: 'Updated Service', integration_key: 'abc123'}, + ], + }, + expect.anything() + ); }); }); @@ -173,9 +178,12 @@ describe('TableAdapter', () => { await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - service_table: [{id: '2', service: 'Other Service', integration_key: 'def456'}], - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + service_table: [{id: '2', service: 'Other Service', integration_key: 'def456'}], + }, + expect.anything() + ); }); }); @@ -218,9 +226,14 @@ describe('TableAdapter', () => { await userEvent.keyboard('{Enter}'); await waitFor(() => { - expect(mutationOptions.mutationFn).toHaveBeenCalledWith({ - service_table: [{id: '1', service: 'Updated Service', integration_key: 'abc123'}], - }); + expect(mutationOptions.mutationFn).toHaveBeenCalledWith( + { + service_table: [ + {id: '1', service: 'Updated Service', integration_key: 'abc123'}, + ], + }, + expect.anything() + ); }); }); diff --git a/static/app/components/contextPickerModal.spec.tsx b/static/app/components/contextPickerModal.spec.tsx index b98b2c1305de3c..db044f9d2fe50d 100644 --- a/static/app/components/contextPickerModal.spec.tsx +++ b/static/app/components/contextPickerModal.spec.tsx @@ -286,7 +286,7 @@ describe('ContextPickerModal', () => { throw new Error('Integration domainName is null'); } - await selectEvent.select(screen.getByRole('textbox'), integration.domainName); + await selectEvent.select(await screen.findByRole('textbox'), integration.domainName); expect(onFinish).toHaveBeenCalledWith( `/settings/${org.slug}/integrations/github/${integration.id}/` ); @@ -367,7 +367,9 @@ describe('ContextPickerModal', () => { expect(fetchGithubConfigs).toHaveBeenCalled(); }); - expect(onFinish).toHaveBeenCalledWith(`/settings/${org.slug}/integrations/github/`); + await waitFor(() => { + expect(onFinish).toHaveBeenCalledWith(`/settings/${org.slug}/integrations/github/`); + }); }); it('preserves path object query parameters', async () => { @@ -391,9 +393,11 @@ describe('ContextPickerModal', () => { ); await waitFor(() => expect(fetchProjectsForOrg).toHaveBeenCalled()); - expect(onFinish).toHaveBeenLastCalledWith({ - pathname: '/test/org2/path/project2/', - query: {referrer: 'onboarding_task'}, + await waitFor(() => { + expect(onFinish).toHaveBeenLastCalledWith({ + pathname: '/test/org2/path/project2/', + query: {referrer: 'onboarding_task'}, + }); }); }); diff --git a/static/app/components/core/form/field/baseField.spec.tsx b/static/app/components/core/form/field/baseField.spec.tsx index b92980ffdd1dd5..962a4517505ed2 100644 --- a/static/app/components/core/form/field/baseField.spec.tsx +++ b/static/app/components/core/form/field/baseField.spec.tsx @@ -187,7 +187,7 @@ describe('BaseField indicator', () => { expect( await screen.findByRole('status', {name: 'Saving testField'}) ).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({testField: 'changed'}); + expect(mutationFn).toHaveBeenCalledWith({testField: 'changed'}, expect.anything()); }); it('shows checkmark when auto-save succeeds', async () => { @@ -201,7 +201,7 @@ describe('BaseField indicator', () => { await userEvent.tab(); // blur triggers auto-save expect(await screen.findByTestId('icon-check-mark')).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({testField: 'changed'}); + expect(mutationFn).toHaveBeenCalledWith({testField: 'changed'}, expect.anything()); }); it('shows warning icon when field has validation errors', async () => { diff --git a/static/app/components/core/form/field/radioField.spec.tsx b/static/app/components/core/form/field/radioField.spec.tsx index 9049bdd2c23048..bd81ebde9e7a49 100644 --- a/static/app/components/core/form/field/radioField.spec.tsx +++ b/static/app/components/core/form/field/radioField.spec.tsx @@ -171,7 +171,7 @@ describe('RadioField auto-save', () => { expect( await screen.findByRole('status', {name: 'Saving priority'}) ).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({priority: 'high'}); + expect(mutationFn).toHaveBeenCalledWith({priority: 'high'}, expect.anything()); }); it('shows checkmark when auto-save succeeds', async () => { @@ -183,7 +183,7 @@ describe('RadioField auto-save', () => { await userEvent.click(radios[2]!); // click high expect(await screen.findByTestId('icon-check-mark')).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({priority: 'high'}); + expect(mutationFn).toHaveBeenCalledWith({priority: 'high'}, expect.anything()); }); it('disables radios while auto-save is pending', async () => { diff --git a/static/app/components/core/form/field/selectField.spec.tsx b/static/app/components/core/form/field/selectField.spec.tsx index ba8f76c1c53941..cea9f125720f74 100644 --- a/static/app/components/core/form/field/selectField.spec.tsx +++ b/static/app/components/core/form/field/selectField.spec.tsx @@ -439,7 +439,7 @@ describe('SelectField auto-save', () => { await userEvent.click(screen.getByRole('menuitemradio', {name: 'Banana'})); expect(await screen.findByRole('status', {name: 'Saving fruit'})).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({fruit: 'banana'}); + expect(mutationFn).toHaveBeenCalledWith({fruit: 'banana'}, expect.anything()); }); it('shows checkmark when auto-save succeeds', async () => { @@ -451,7 +451,7 @@ describe('SelectField auto-save', () => { await userEvent.click(screen.getByRole('menuitemradio', {name: 'Banana'})); expect(await screen.findByTestId('icon-check-mark')).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({fruit: 'banana'}); + expect(mutationFn).toHaveBeenCalledWith({fruit: 'banana'}, expect.anything()); }); it('disables select while auto-save is pending', async () => { @@ -790,7 +790,7 @@ describe('SelectField multiple auto-save', () => { await userEvent.click(removeButtons[0]!); await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({tags: ['tag2']}); + expect(mutationFn).toHaveBeenCalledWith({tags: ['tag2']}, expect.anything()); }); }); @@ -805,7 +805,7 @@ describe('SelectField multiple auto-save', () => { await userEvent.click(screen.getByLabelText('Clear choices')); await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({tags: []}); + expect(mutationFn).toHaveBeenCalledWith({tags: []}, expect.anything()); }); }); @@ -842,7 +842,10 @@ describe('SelectField multiple auto-save', () => { await userEvent.keyboard('{Escape}'); await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({tags: ['tag1', 'tag2']}); + expect(mutationFn).toHaveBeenCalledWith( + {tags: ['tag1', 'tag2']}, + expect.anything() + ); }); }); @@ -864,7 +867,7 @@ describe('SelectField multiple auto-save', () => { expect(mutationFn).toHaveBeenCalledTimes(1); }); - expect(mutationFn).toHaveBeenCalledWith({tags: ['tag2', 'tag3']}); + expect(mutationFn).toHaveBeenCalledWith({tags: ['tag2', 'tag3']}, expect.anything()); }); it('shows spinner when auto-save is pending for multi-select', async () => { diff --git a/static/app/components/core/form/field/switchField.spec.tsx b/static/app/components/core/form/field/switchField.spec.tsx index eb96d4ff25c7b8..f8320de3a13be6 100644 --- a/static/app/components/core/form/field/switchField.spec.tsx +++ b/static/app/components/core/form/field/switchField.spec.tsx @@ -155,7 +155,7 @@ describe('SwitchField auto-save', () => { expect( await screen.findByRole('status', {name: 'Saving enabled'}) ).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); it('shows checkmark when auto-save succeeds', async () => { @@ -172,7 +172,7 @@ describe('SwitchField auto-save', () => { const checkmarks = screen.getAllByTestId('icon-check-mark'); expect(checkmarks.length).toBeGreaterThan(1); }); - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); it('disables switch while auto-save is pending', async () => { @@ -198,14 +198,14 @@ describe('SwitchField auto-save', () => { // First click: OFF → ON, triggers mutation await userEvent.click(checkbox); expect(mutationFn).toHaveBeenCalledTimes(1); - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); // After save succeeds, form.reset() resets to initialValue (false/OFF). // Second click: OFF → ON again, triggers another mutation. mutationFn.mockClear(); await userEvent.click(checkbox); expect(mutationFn).toHaveBeenCalledTimes(1); - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); it('does not hang when mutation fails', async () => { @@ -221,7 +221,7 @@ describe('SwitchField auto-save', () => { // Mutation should be called await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); // Error handler should be invoked by TanStack Query @@ -416,7 +416,7 @@ describe('SwitchField with confirm', () => { // Mutation should be called immediately await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({enabled: false}); + expect(mutationFn).toHaveBeenCalledWith({enabled: false}, expect.anything()); }); }); @@ -451,7 +451,7 @@ describe('SwitchField with confirm', () => { // Mutation should be called after confirmation await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); }); @@ -487,7 +487,7 @@ describe('SwitchField with confirm', () => { // Mutation should be called await waitFor(() => { - expect(mutationFn).toHaveBeenCalledWith({enabled: true}); + expect(mutationFn).toHaveBeenCalledWith({enabled: true}, expect.anything()); }); // Error handler should be invoked by TanStack Query diff --git a/static/app/components/core/form/field/textAreaField.spec.tsx b/static/app/components/core/form/field/textAreaField.spec.tsx index be417f82ce7627..a001eebadc2caf 100644 --- a/static/app/components/core/form/field/textAreaField.spec.tsx +++ b/static/app/components/core/form/field/textAreaField.spec.tsx @@ -141,7 +141,7 @@ describe('TextAreaField auto-save', () => { await userEvent.tab(); expect(await screen.findByRole('status', {name: 'Saving bio'})).toBeInTheDocument(); - expect(mutationFn).toHaveBeenCalledWith({bio: 'test'}); + expect(mutationFn).toHaveBeenCalledWith({bio: 'test'}, expect.anything()); }); it('shows checkmark when auto-save succeeds', async () => { @@ -157,7 +157,7 @@ describe('TextAreaField auto-save', () => { await waitFor(() => { expect(screen.getByTestId('icon-check-mark')).toBeInTheDocument(); }); - expect(mutationFn).toHaveBeenCalledWith({bio: 'test'}); + expect(mutationFn).toHaveBeenCalledWith({bio: 'test'}, expect.anything()); }); it('disables textarea while auto-save is pending', async () => { diff --git a/static/app/components/events/eventAttachments.spec.tsx b/static/app/components/events/eventAttachments.spec.tsx index 6857c94a1fdb32..9e1317d48ed4f7 100644 --- a/static/app/components/events/eventAttachments.spec.tsx +++ b/static/app/components/events/eventAttachments.spec.tsx @@ -50,9 +50,7 @@ describe('EventAttachments', () => { expect(await screen.findByText('Attachments (0)')).toBeInTheDocument(); - await tick(); - - expect(screen.getByRole('link', {name: 'View crashes'})).toHaveAttribute( + expect(await screen.findByRole('link', {name: 'View crashes'})).toHaveAttribute( 'href', '/organizations/org-slug/issues/1/attachments/?attachmentFilter=onlyCrash' ); diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.spec.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.spec.tsx index 70c98a6643ff79..736f9b86dbdef5 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.spec.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.spec.tsx @@ -205,7 +205,7 @@ describe('eventDisplay', () => { ); expect(await screen.findByText('event1')).toBeInTheDocument(); - expect(screen.getByText('mock-value-for-event1')).toBeInTheDocument(); + expect(await screen.findByText('mock-value-for-event1')).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Previous Event'})).toBeDisabled(); await userEvent.click(screen.getByRole('button', {name: 'Next Event'})); diff --git a/static/app/components/events/eventTags/eventTagsTree.spec.tsx b/static/app/components/events/eventTags/eventTagsTree.spec.tsx index a06936d5c02729..36c50af2375500 100644 --- a/static/app/components/events/eventTags/eventTagsTree.spec.tsx +++ b/static/app/components/events/eventTags/eventTagsTree.spec.tsx @@ -76,9 +76,8 @@ describe('EventTagsTree', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - tags.forEach(({value}) => { + expect(await screen.findByText(tags[0]!.value)).toBeInTheDocument(); + tags.slice(1).forEach(({value}) => { expect(screen.getByText(value)).toBeInTheDocument(); }); @@ -131,9 +130,7 @@ describe('EventTagsTree', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const versionText = screen.getByText< + const versionText = await screen.findByText< HTMLElement & {parentElement: HTMLAnchorElement} >(releaseVersion); const anchorLink = versionText.parentElement; @@ -188,9 +185,7 @@ describe('EventTagsTree', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const dropdown = screen.getByLabelText('Tag Actions Menu'); + const dropdown = await screen.findByLabelText('Tag Actions Menu'); await userEvent.click(dropdown); expect(screen.getByLabelText(labelText)).toBeInTheDocument(); await (validateLink as () => Promise)(); @@ -235,10 +230,8 @@ describe('EventTagsTree', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - // Should only be one dropdown, others have errors - const dropdown = screen.getByLabelText('Tag Actions Menu'); + const dropdown = await screen.findByLabelText('Tag Actions Menu'); expect(dropdown).toBeInTheDocument(); const errorRows = screen.queryAllByTestId('tag-tree-row-errors'); @@ -257,9 +250,7 @@ describe('EventTagsTree', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - expect(screen.getByText('boring-tag', {selector: 'div'})).toBeInTheDocument(); + expect(await screen.findByText('boring-tag', {selector: 'div'})).toBeInTheDocument(); expect(screen.getByText('boring tag')).toBeInTheDocument(); expect(screen.queryByText('null tag')).not.toBeInTheDocument(); expect(screen.queryByText('undefined tag')).not.toBeInTheDocument(); @@ -282,11 +273,9 @@ describe('EventTagsTree', () => { organization, }); expect(mockHighlightProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const normalTagRow = screen - .getByText('useless-tag', {selector: 'div'}) - .closest('div[data-test-id=tag-tree-row]') as HTMLElement; + const normalTagRow = ( + await screen.findByText('useless-tag', {selector: 'div'}) + ).closest('div[data-test-id=tag-tree-row]') as HTMLElement; const normalTagDropdown = within(normalTagRow).getByLabelText('Tag Actions Menu'); await userEvent.click(normalTagDropdown); expect(screen.getByLabelText('Add to event highlights')).toBeInTheDocument(); @@ -321,11 +310,9 @@ describe('EventTagsTree', () => { organization: readAccessOrganization, }); expect(mockHighlightProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const normalTagRow = screen - .getByText('useless-tag', {selector: 'div'}) - .closest('div[data-test-id=tag-tree-row]') as HTMLElement; + const normalTagRow = ( + await screen.findByText('useless-tag', {selector: 'div'}) + ).closest('div[data-test-id=tag-tree-row]') as HTMLElement; const normalTagDropdown = within(normalTagRow).getByLabelText('Tag Actions Menu'); await userEvent.click(normalTagDropdown); expect(screen.queryByLabelText('Add to event highlights')).not.toBeInTheDocument(); @@ -346,11 +333,9 @@ describe('EventTagsTree', () => { route: '/organizations/:orgId/issues/:groupId/', }, }); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const normalTagRow = screen - .getByText('useless-tag', {selector: 'div'}) - .closest('div[data-test-id=tag-tree-row]') as HTMLElement; + const normalTagRow = ( + await screen.findByText('useless-tag', {selector: 'div'}) + ).closest('div[data-test-id=tag-tree-row]') as HTMLElement; const normalTagDropdown = within(normalTagRow).getByLabelText('Tag Actions Menu'); await userEvent.click(normalTagDropdown); expect(await screen.findByLabelText('Tag breakdown')).toBeInTheDocument(); diff --git a/static/app/components/events/eventTags/index.spec.tsx b/static/app/components/events/eventTags/index.spec.tsx index cae0272c4e7bf1..9fd8ea92808d19 100644 --- a/static/app/components/events/eventTags/index.spec.tsx +++ b/static/app/components/events/eventTags/index.spec.tsx @@ -58,9 +58,8 @@ describe('event tags', () => { }); render(, {organization}); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - expect(screen.getByText('device.family')).toBeInTheDocument(); + expect(await screen.findByText('device.family')).toBeInTheDocument(); expect(screen.getByText('iOS')).toBeInTheDocument(); expect(screen.getByText('app.device')).toBeInTheDocument(); @@ -88,9 +87,8 @@ describe('event tags', () => { }); render(, {organization}); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - expect(screen.getByText('mytransaction')).toBeInTheDocument(); + expect(await screen.findByText('mytransaction')).toBeInTheDocument(); expect(screen.getByRole('link')).toHaveAttribute( 'href', `/organizations/${organization.slug}/insights/summary/?project=1&referrer=event-tags-table&transaction=mytransaction` diff --git a/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx b/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx index 8111bcf90a3a48..da1af98ea2dd11 100644 --- a/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx @@ -157,8 +157,7 @@ describe('EventTagsAndScreenshot', () => { ); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await tagsContainer.findByTestId('loading-indicator')).not.toBeInTheDocument(); - expect(screen.getByTestId('event-tags-tree')).toBeInTheDocument(); + expect(await screen.findByTestId('event-tags-tree')).toBeInTheDocument(); } /** @@ -224,9 +223,7 @@ describe('EventTagsAndScreenshot', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - let rows = screen.getAllByTestId('tag-tree-row'); + let rows = await screen.findAllByTestId('tag-tree-row'); expect(rows).toHaveLength(allTags.length); await userEvent.click(screen.getByTestId(TagFilter.APPLICATION)); @@ -263,9 +260,7 @@ describe('EventTagsAndScreenshot', () => { organization, }); expect(mockDetailedProject).toHaveBeenCalled(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); - - const rows = screen.getAllByTestId('tag-tree-row'); + const rows = await screen.findAllByTestId('tag-tree-row'); expect(rows).toHaveLength(applicationTags.length); expect(screen.queryByTestId(TagFilter.CLIENT)).not.toBeInTheDocument(); diff --git a/static/app/components/events/highlights/highlightsDataSection.spec.tsx b/static/app/components/events/highlights/highlightsDataSection.spec.tsx index 9b85d94beca0ca..35b31ceb76e70e 100644 --- a/static/app/components/events/highlights/highlightsDataSection.spec.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.spec.tsx @@ -83,8 +83,9 @@ describe('HighlightsDataSection', () => { render(, { organization, }); - expect(screen.getByText('Highlights')).toBeInTheDocument(); - expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument(); + expect(await screen.findByText('Highlights')).toBeInTheDocument(); + // Wait for the project detail API data to load and render tags + expect(await screen.findByText('environment', {selector: 'div'})).toBeInTheDocument(); for (const tagKey of highlightTags) { const row = screen .getByText(tagKey, {selector: 'div'}) diff --git a/static/app/components/events/interfaces/debugMeta/index.spec.tsx b/static/app/components/events/interfaces/debugMeta/index.spec.tsx index 3e4da62b70af35..2df17bdd596856 100644 --- a/static/app/components/events/interfaces/debugMeta/index.spec.tsx +++ b/static/app/components/events/interfaces/debugMeta/index.spec.tsx @@ -144,7 +144,9 @@ describe('DebugMeta', () => { const imageName = image?.debug_file as string; const codeFile = image?.code_file as string; - expect(screen.getByRole('region', {name: 'Images Loaded'})).toBeInTheDocument(); + expect( + await screen.findByRole('region', {name: 'Images Loaded'}) + ).toBeInTheDocument(); const imageNode = screen.getByText(imageName); expect(imageNode).toBeInTheDocument(); diff --git a/static/app/components/events/interfaces/frame/context.tsx b/static/app/components/events/interfaces/frame/context.tsx index f4fa48a2893fab..6b2a6a2f9fcf84 100644 --- a/static/app/components/events/interfaces/frame/context.tsx +++ b/static/app/components/events/interfaces/frame/context.tsx @@ -5,6 +5,7 @@ import keyBy from 'lodash/keyBy'; import {ClippedBox} from 'sentry/components/clippedBox'; import {useLineCoverageContext} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext'; import {parseAssembly} from 'sentry/components/events/interfaces/utils'; +import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {IconFlag} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Event, Frame} from 'sentry/types/event'; @@ -27,7 +28,9 @@ import {ContextLineNumber} from './contextLineNumber'; import {FrameRegisters} from './frameRegisters'; import {FrameVariables} from './frameVariables'; import {usePrismTokensSourceContext} from './usePrismTokensSourceContext'; +import {useSourceContext} from './useSourceContext'; import {useStacktraceCoverage} from './useStacktraceCoverage'; +import {hasPotentialSourceContext} from './utils'; type Props = { components: Array>; @@ -41,6 +44,7 @@ type Props = { hasContextRegisters?: boolean; hasContextSource?: boolean; hasContextVars?: boolean; + hasScmSourceContext?: boolean; isExpanded?: boolean; isFirst?: boolean; platform?: PlatformKey; @@ -69,6 +73,7 @@ export function Context({ isExpanded = false, hasAssembly = false, emptySourceNotation = false, + hasScmSourceContext = false, registers, frame, event, @@ -89,6 +94,34 @@ export function Context({ [projects, event] ); + const shouldFetchSourceContext = + hasScmSourceContext && + defined(project) && + !hasContextSource && + isExpanded && + hasPotentialSourceContext(frame); + + const {data: sourceContextData, isPending: isLoadingSourceContext} = useSourceContext( + { + event, + frame, + orgSlug: organization?.slug || '', + projectSlug: project?.slug, + }, + {enabled: shouldFetchSourceContext} + ); + + const scmContext: Frame['context'] | undefined = useMemo(() => { + if (!sourceContextData?.context?.length) { + return undefined; + } + return sourceContextData.context; + }, [sourceContextData]); + + // Use SCM-fetched context when the frame has no embedded context + const effectiveContext = hasContextSource ? frame?.context : scmContext; + const effectiveHasContextSource = hasContextSource || !!scmContext?.length; + const {data: coverage, isPending: isLoadingCoverage} = useStacktraceCoverage( { event, @@ -110,8 +143,8 @@ export function Context({ */ const activeLineNumber = frame.lineNo; const contextLines = isExpanded - ? frame?.context - : frame?.context?.filter(l => l[0] === activeLineNumber); + ? effectiveContext + : effectiveContext?.filter(l => l[0] === activeLineNumber); const hasCoverageData = !isLoadingCoverage && coverage?.status === CodecovStatusCode.COVERAGE_EXISTS; @@ -135,14 +168,22 @@ export function Context({ : {} ); - const fileExtension = getFileExtension(frame.filename || '') ?? ''; + const fileExtension = getFileExtension(frame.filename || frame.absPath || '') ?? ''; const lines = usePrismTokensSourceContext({ contextLines, lineNo: frame.lineNo, fileExtension, }); - if (!hasContextSource && !hasContextVars && !hasContextRegisters && !hasAssembly) { + const isLoadingScmContext = shouldFetchSourceContext && isLoadingSourceContext; + + if ( + !isLoadingScmContext && + !effectiveHasContextSource && + !hasContextVars && + !hasContextRegisters && + !hasAssembly + ) { return emptySourceNotation ? ( @@ -151,7 +192,7 @@ export function Context({ ) : null; } - const startLineNo = hasContextSource ? frame.context[0]![0] : 0; + const startLineNo = effectiveHasContextSource ? (effectiveContext?.[0]?.[0] ?? 0) : 0; const prismClassName = fileExtension ? `language-${fileExtension}` : ''; @@ -162,12 +203,17 @@ export function Context({ className={`${className} context ${isExpanded ? 'expanded' : ''}`} data-test-id="frame-context" > - {frame.context && lines.length > 0 ? ( + {isLoadingScmContext ? ( + + + {t('Loading source context…')} + + ) : effectiveContext && lines.length > 0 ? (
             
               {lines.map((line, i) => {
-                const contextLine = contextLines[i]!;
+                const contextLine = contextLines![i]!;
                 const isActive = activeLineNumber === contextLine[0];
 
                 return (
diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.tsx
index 8f666540d12f9b..ecab4232f3f4e9 100644
--- a/static/app/components/events/interfaces/frame/deprecatedLine.tsx
+++ b/static/app/components/events/interfaces/frame/deprecatedLine.tsx
@@ -14,6 +14,7 @@ import {LeadHint} from 'sentry/components/events/interfaces/frame/leadHint';
 import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink';
 import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
 import {SourceMapsDebuggerModal} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
+import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext';
 import {getThreadById} from 'sentry/components/events/interfaces/utils';
 import {StrictClick} from 'sentry/components/strictClick';
 import {IconChevron, IconFix, IconRefresh} from 'sentry/icons';
@@ -39,6 +40,7 @@ import {
   hasContextRegisters,
   hasContextSource,
   hasContextVars,
+  hasPotentialSourceContext,
   isPotentiallyThirdPartyFrame,
 } from './utils';
 
@@ -109,6 +111,7 @@ function DeprecatedLine({
   components,
 }: Props) {
   const organization = useOrganization();
+  const {hasScmSourceContext} = useStacktraceContext();
   const [isHovering, setIsHovering] = useState(false);
   const [isExpanded, setIsExpanded] = useState(initialExpanded ?? false);
   const platform = getPlatform(data.platform, propPlatform ?? 'other');
@@ -119,9 +122,10 @@ function DeprecatedLine({
       (hasContextSource(data) && data.context) ||
       hasContextVars(data) ||
       hasContextRegisters(registers) ||
-      hasAssembly(data, platform)
+      hasAssembly(data, platform) ||
+      (hasScmSourceContext && hasPotentialSourceContext(data))
     );
-  }, [data, registers, platform]);
+  }, [data, registers, platform, hasScmSourceContext]);
 
   const toggleContext = (evt?: React.MouseEvent) => {
     evt?.preventDefault();
@@ -344,6 +348,7 @@ function DeprecatedLine({
         hasContextRegisters={hasContextRegisters(registers)}
         emptySourceNotation={emptySourceNotation}
         hasAssembly={hasAssembly(data, platform)}
+        hasScmSourceContext={hasScmSourceContext}
         isExpanded={isExpanded}
         registersMeta={registersMeta}
         frameMeta={frameMeta}
diff --git a/static/app/components/events/interfaces/frame/useSourceContext.spec.tsx b/static/app/components/events/interfaces/frame/useSourceContext.spec.tsx
new file mode 100644
index 00000000000000..2c081cc3fa913c
--- /dev/null
+++ b/static/app/components/events/interfaces/frame/useSourceContext.spec.tsx
@@ -0,0 +1,60 @@
+import {EventFixture} from 'sentry-fixture/event';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import type {SourceContextResponse} from 'sentry/components/events/interfaces/frame/useSourceContext';
+import {useSourceContext} from 'sentry/components/events/interfaces/frame/useSourceContext';
+
+describe('useSourceContext', () => {
+  const project = ProjectFixture();
+  const event = EventFixture({projectID: project.id});
+  const frame = {
+    filename: 'src/app.py',
+    lineNo: 10,
+    absPath: '/path/to/src/app.py',
+    function: 'test_func',
+    module: null,
+    package: null,
+  };
+
+  beforeEach(() => {
+    MockApiClient.clearMockResponses();
+  });
+
+  it('fetches source context when enabled', async () => {
+    const mockResponse: SourceContextResponse = {
+      context: [
+        [8, 'def helper():'],
+        [9, '    pass'],
+        [10, 'def test_func():'],
+        [11, '    return "result"'],
+        [12, ''],
+      ],
+      sourceUrl: 'https://github.com/example/repo/blob/main/src/app.py',
+      error: null,
+    };
+
+    MockApiClient.addMockResponse({
+      url: `/projects/org-slug/${project.slug}/stacktrace-source-context/`,
+      body: mockResponse,
+    });
+
+    const {result} = renderHookWithProviders(useSourceContext, {
+      initialProps: {
+        event,
+        frame,
+        orgSlug: 'org-slug',
+        projectSlug: project.slug,
+      },
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toBeDefined();
+    });
+
+    expect(result.current.data?.context).toEqual(mockResponse.context);
+    expect(result.current.data?.sourceUrl).toBe(mockResponse.sourceUrl);
+    expect(result.current.data?.error).toBeNull();
+  });
+});
diff --git a/static/app/components/events/interfaces/frame/useSourceContext.tsx b/static/app/components/events/interfaces/frame/useSourceContext.tsx
new file mode 100644
index 00000000000000..e970e9f2164aa5
--- /dev/null
+++ b/static/app/components/events/interfaces/frame/useSourceContext.tsx
@@ -0,0 +1,55 @@
+import {buildStacktraceLinkQuery} from 'sentry/components/events/interfaces/frame/useStacktraceLink';
+import type {Event, Frame} from 'sentry/types/event';
+import {getApiUrl} from 'sentry/utils/api/getApiUrl';
+import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient';
+import {useApiQuery} from 'sentry/utils/queryClient';
+
+export interface SourceContextResponse {
+  context: Array<[number, string]>;
+  error: string | null;
+  sourceUrl: string | null;
+}
+
+interface UseSourceContextProps {
+  event: Partial>;
+  frame: Partial<
+    Pick
+  >;
+  orgSlug: string;
+  projectSlug: string | undefined;
+}
+
+const sourceContextQueryKey = (
+  orgSlug: string,
+  projectSlug: string | undefined,
+  query: ReturnType
+): ApiQueryKey => [
+  getApiUrl(
+    '/projects/$organizationIdOrSlug/$projectIdOrSlug/stacktrace-source-context/',
+    {
+      path: {
+        organizationIdOrSlug: orgSlug,
+        projectIdOrSlug: projectSlug!,
+      },
+    }
+  ),
+  {query},
+];
+
+export function useSourceContext(
+  {event, frame, orgSlug, projectSlug}: UseSourceContextProps,
+  options: Partial> = {}
+) {
+  const query = {
+    ...buildStacktraceLinkQuery(event, frame),
+    file: (frame.filename || frame.absPath)!,
+  };
+  return useApiQuery(
+    sourceContextQueryKey(orgSlug, projectSlug, query),
+    {
+      staleTime: Infinity,
+      retry: false,
+      ...options,
+    }
+  );
+}
diff --git a/static/app/components/events/interfaces/frame/utils.tsx b/static/app/components/events/interfaces/frame/utils.tsx
index 272ee15376b495..a53f22cbe50e86 100644
--- a/static/app/components/events/interfaces/frame/utils.tsx
+++ b/static/app/components/events/interfaces/frame/utils.tsx
@@ -43,25 +43,36 @@ export function hasAssembly(frame: Frame, platform?: string) {
   );
 }
 
+/**
+ * Returns true if the frame has enough information to potentially fetch
+ * source context from an SCM integration (filename + line number + in-app).
+ */
+export function hasPotentialSourceContext(frame: Frame) {
+  return !!frame.inApp && !!frame.lineNo && !!(frame.filename || frame.absPath);
+}
+
 export function isExpandable({
   frame,
   registers,
   emptySourceNotation,
   platform,
   isOnlyFrame,
+  hasScmSourceContext,
 }: {
   frame: Frame;
   registers: StacktraceType['registers'];
   emptySourceNotation?: boolean;
+  hasScmSourceContext?: boolean;
   isOnlyFrame?: boolean;
   platform?: string;
 }) {
-  return (
+  return !!(
     (!isOnlyFrame && emptySourceNotation) ||
     hasContextSource(frame) ||
     hasContextVars(frame) ||
     hasContextRegisters(registers) ||
-    hasAssembly(frame, platform)
+    hasAssembly(frame, platform) ||
+    (hasScmSourceContext && hasPotentialSourceContext(frame))
   );
 }
 
diff --git a/static/app/components/events/interfaces/nativeFrame.tsx b/static/app/components/events/interfaces/nativeFrame.tsx
index e356ebbfca0552..c5bf5419df6064 100644
--- a/static/app/components/events/interfaces/nativeFrame.tsx
+++ b/static/app/components/events/interfaces/nativeFrame.tsx
@@ -104,7 +104,7 @@ function NativeFrame({
   const isDartAsyncSuspensionFrame =
     frame.filename === '' ||
     frame.absPath === '';
-  const {displayOptions, stackView} = useStacktraceContext();
+  const {displayOptions, stackView, hasScmSourceContext} = useStacktraceContext();
 
   const {sectionData} = useIssueDetails();
   const debugSectionConfig = sectionData[SectionKey.DEBUGMETA];
@@ -136,6 +136,7 @@ function NativeFrame({
     registers,
     platform,
     emptySourceNotation,
+    hasScmSourceContext,
   });
 
   const inlineFrame =
@@ -282,7 +283,7 @@ function NativeFrame({
     
       
         ({
   stackType: StackType.ORIGINAL,
   displayOptions: [],
   setDisplayOptions: () => {},
+  hasScmSourceContext: false,
   isFullStackTrace: true,
   forceFullStackTrace: false,
   setIsFullStackTrace: () => {},
@@ -94,6 +101,13 @@ export function StacktraceContext({
   defaultIsNewestFramesFirst = true,
 }: StackTraceContextOptions) {
   const organization = useOrganization();
+  const hasScmFeature = organization.features.includes('scm-source-context');
+  const {data: detailedProject} = useDetailedProject(
+    {orgSlug: organization.slug, projectSlug: projectSlug ?? ''},
+    {enabled: hasScmFeature && defined(projectSlug)}
+  );
+  const hasScmSourceContext = hasScmFeature && !!detailedProject?.scmSourceContextEnabled;
+
   const [isFullStackTrace, setIsFullStackTrace] = useState(false);
   const [isNewestFramesFirst, setIsNewestFramesFirst] = useState(
     defaultIsNewestFramesFirst
@@ -123,6 +137,7 @@ export function StacktraceContext({
       setIsNewestFramesFirst,
       displayOptions,
       setDisplayOptions,
+      hasScmSourceContext,
       stackView,
       stackType,
       forceFullStackTrace,
@@ -133,6 +148,7 @@ export function StacktraceContext({
       isNewestFramesFirst,
       displayOptions,
       setDisplayOptions,
+      hasScmSourceContext,
       stackView,
       stackType,
     ]
diff --git a/static/app/components/externalIssues/externalIssueForm.spec.tsx b/static/app/components/externalIssues/externalIssueForm.spec.tsx
index ff66f46c6e278a..509b1468287eac 100644
--- a/static/app/components/externalIssues/externalIssueForm.spec.tsx
+++ b/static/app/components/externalIssues/externalIssueForm.spec.tsx
@@ -70,8 +70,7 @@ describe('ExternalIssueForm', () => {
       />,
       {organization}
     );
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-    await userEvent.click(screen.getByText(action));
+    await userEvent.click(await screen.findByText(action));
     return wrapper;
   };
 
@@ -196,7 +195,7 @@ describe('ExternalIssueForm', () => {
         />,
         {organization}
       );
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
+      await screen.findByRole('textbox', {name: 'Project'});
       expect(initialQuery).toHaveBeenCalled();
 
       // Initial query may only have a few fields
diff --git a/static/app/components/externalIssues/ticketRuleModal.spec.tsx b/static/app/components/externalIssues/ticketRuleModal.spec.tsx
index c70a79164209ed..961f2a2db5ba26 100644
--- a/static/app/components/externalIssues/ticketRuleModal.spec.tsx
+++ b/static/app/components/externalIssues/ticketRuleModal.spec.tsx
@@ -103,7 +103,7 @@ describe('ProjectAlerts -> TicketRuleModal', () => {
         organization,
       }
     );
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
+    await screen.findByRole('button', {name: 'Apply Changes'});
     return wrapper;
   };
 
diff --git a/static/app/components/frontendVersionContext.spec.tsx b/static/app/components/frontendVersionContext.spec.tsx
index c20fc7a3f65ef0..6e054b2810981d 100644
--- a/static/app/components/frontendVersionContext.spec.tsx
+++ b/static/app/components/frontendVersionContext.spec.tsx
@@ -1,4 +1,4 @@
-import {act, render, screen} from 'sentry-test/reactTestingLibrary';
+import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import * as constants from 'sentry/constants';
 import {ConfigStore} from 'sentry/stores/configStore';
@@ -55,9 +55,11 @@ describe('FrontendVersionProvider', () => {
     // Advance past the initial delay before version checking starts
     act(() => jest.advanceTimersByTime(ONE_HOUR));
 
-    expect(await screen.findByTestId('state')).toHaveTextContent('current');
-    expect(await screen.findByTestId('deployed-version')).toHaveTextContent(commitSha);
-    expect(await screen.findByTestId('running-version')).toHaveTextContent(commitSha);
+    await waitFor(() => {
+      expect(screen.getByTestId('state')).toHaveTextContent('current');
+    });
+    expect(screen.getByTestId('deployed-version')).toHaveTextContent(commitSha);
+    expect(screen.getByTestId('running-version')).toHaveTextContent(commitSha);
   });
 
   it('provides state="stale" when server version differs from current version', async () => {
@@ -79,13 +81,11 @@ describe('FrontendVersionProvider', () => {
     // Advance past the initial delay before version checking starts
     act(() => jest.advanceTimersByTime(ONE_HOUR));
 
-    expect(await screen.findByTestId('state')).toHaveTextContent('stale');
-    expect(await screen.findByTestId('deployed-version')).toHaveTextContent(
-      serverVersion
-    );
-    expect(await screen.findByTestId('running-version')).toHaveTextContent(
-      currentCommitSha
-    );
+    await waitFor(() => {
+      expect(screen.getByTestId('state')).toHaveTextContent('stale');
+    });
+    expect(screen.getByTestId('deployed-version')).toHaveTextContent(serverVersion);
+    expect(screen.getByTestId('running-version')).toHaveTextContent(currentCommitSha);
   });
 
   it('provides state="unknown" when server returns null version', async () => {
diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx
index f5431ba6941677..c678cce9a43667 100644
--- a/static/app/components/onboarding/productSelection.tsx
+++ b/static/app/components/onboarding/productSelection.tsx
@@ -432,6 +432,12 @@ export const platformProductAvailability = {
     ProductSolution.LOGS,
     ProductSolution.METRICS,
   ],
+  'python-litestar': [
+    ProductSolution.PERFORMANCE_MONITORING,
+    ProductSolution.PROFILING,
+    ProductSolution.LOGS,
+    ProductSolution.METRICS,
+  ],
   'python-gcpfunctions': [
     ProductSolution.PERFORMANCE_MONITORING,
     ProductSolution.PROFILING,
diff --git a/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx b/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx
index 114813e0f6c2dc..cab89cad70fb51 100644
--- a/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx
+++ b/static/app/components/repositories/useBulkUpdateRepositorySettings.tsx
@@ -43,7 +43,7 @@ export function useBulkUpdateRepositorySettings(
       });
     },
     ...options,
-    onSettled: (data, error, variables, context) => {
+    onSettled: (data, error, variables, onMutateResult, context) => {
       queryClient.invalidateQueries({
         queryKey: [`/organizations/${organization.slug}/repos/`],
       });
@@ -52,7 +52,7 @@ export function useBulkUpdateRepositorySettings(
         queryClient.invalidateQueries({queryKey});
         queryClient.setQueryData(queryKey, [repo, undefined, undefined]);
       });
-      options?.onSettled?.(data, error, variables, context);
+      options?.onSettled?.(data, error, variables, onMutateResult, context);
     },
   });
 }
diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx
index a3959f5464d116..419a880bbdeb1c 100644
--- a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx
+++ b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.tsx
@@ -154,8 +154,8 @@ export function AskSeerComboBox({
     isError,
   } = useMutation({
     ...props.askSeerMutationOptions,
-    onError: (error, variables, context) => {
-      props.askSeerMutationOptions.onError?.(error, variables, context);
+    onError: (error, variables, onMutateResult, context) => {
+      props.askSeerMutationOptions.onError?.(error, variables, onMutateResult, context);
       addErrorMessage(t('Failed to process AI query: %(error)s', {error: error.message}));
     },
   });
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
index ab8cdef9324ba4..ea17fb443d75f5 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
@@ -399,6 +399,7 @@ function useFilterSuggestions({
   const isDebouncing = baseQueryKey !== queryKey;
 
   // TODO(malwilley): Display error states
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
   const {data, isFetching} = useQuery({
     queryKey,
     queryFn: ctx => getTagValues(...ctx.queryKey[1]),
diff --git a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
index 69e29e805c17f1..b5f13e47207a65 100644
--- a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
@@ -178,8 +178,8 @@ export function useSortedFilterKeyItems({
   // Async key fetching with debounce when getTagKeys is provided
   const shouldFetchAsync = !!getTagKeys;
   const debouncedFilterValue = useDebouncedValue(filterValue);
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
   const {data: asyncKeys, isLoading: isQueryLoading} = useQuery({
-    // eslint-disable-next-line @tanstack/query/exhaustive-deps
     queryKey: ['search-query-builder-tag-keys', debouncedFilterValue],
     queryFn: ctx => getTagKeys!(ctx.queryKey[1] ?? ''),
     enabled: shouldFetchAsync,
diff --git a/static/app/data/forms/projectGeneralSettings.tsx b/static/app/data/forms/projectGeneralSettings.tsx
index e53f21434fbe14..a821257dec5093 100644
--- a/static/app/data/forms/projectGeneralSettings.tsx
+++ b/static/app/data/forms/projectGeneralSettings.tsx
@@ -167,6 +167,20 @@ export const fields = {
     label: t('Enable JavaScript source fetching'),
     help: t('Allow Sentry to scrape missing JavaScript source context when possible'),
   },
+  scmSourceContextEnabled: {
+    name: 'scmSourceContextEnabled',
+    type: 'boolean',
+    label: t('Enable SCM Source Context'),
+    help: t(
+      "Fetch source code from your connected SCM integration (e.g. GitHub, GitLab) to display in stack traces. When enabled, any project member can view source code for files matched by this project's code mappings."
+    ),
+    visible: ({features}) => features.has('scm-source-context'),
+    confirm: {
+      true: t(
+        'Enabling this will allow all members with access to this project to view source code from the connected SCM integration via code mappings. Are you sure you want to enable this?'
+      ),
+    },
+  },
   securityToken: {
     name: 'securityToken',
     type: 'string',
diff --git a/static/app/data/platformCategories.tsx b/static/app/data/platformCategories.tsx
index 5792f74e567355..2eddce4e84d412 100644
--- a/static/app/data/platformCategories.tsx
+++ b/static/app/data/platformCategories.tsx
@@ -118,6 +118,7 @@ export const backend: PlatformKey[] = [
   'python-falcon',
   'python-fastapi',
   'python-flask',
+  'python-litestar',
   'python-pylons',
   'python-pymongo',
   'python-pyramid',
@@ -384,6 +385,7 @@ export const withLoggingOnboarding = new Set([
   'python-fastapi',
   'python-flask',
   'python-gcpfunctions',
+  'python-litestar',
   'python-pylons',
   'python-pyramid',
   'python-quart',
@@ -480,6 +482,7 @@ export const withMetricsOnboarding = new Set([
   'python-fastapi',
   'python-flask',
   'python-gcpfunctions',
+  'python-litestar',
   'python-pyramid',
   'python-quart',
   'python-rq',
@@ -578,6 +581,7 @@ export const profiling: PlatformKey[] = [
   'python-fastapi',
   'python-flask',
   'python-gcpfunctions',
+  'python-litestar',
   'python-pylons',
   'python-pyramid',
   'python-quart',
@@ -693,6 +697,7 @@ export const replayBackendPlatforms: readonly PlatformKey[] = [
   'python-falcon',
   'python-fastapi',
   'python-flask',
+  'python-litestar',
   'python-pyramid',
   'python-quart',
   'python-sanic',
diff --git a/static/app/data/platformPickerCategories.tsx b/static/app/data/platformPickerCategories.tsx
index c7cb8aad3bd28b..c88566fdd745bf 100644
--- a/static/app/data/platformPickerCategories.tsx
+++ b/static/app/data/platformPickerCategories.tsx
@@ -103,6 +103,7 @@ const server = new Set([
   'python-falcon',
   'python-fastapi',
   'python-flask',
+  'python-litestar',
   'python-pyramid',
   'python-quart',
   'python-rq',
diff --git a/static/app/data/platforms.tsx b/static/app/data/platforms.tsx
index ca144b10ff0e67..887a04db2e2882 100644
--- a/static/app/data/platforms.tsx
+++ b/static/app/data/platforms.tsx
@@ -625,6 +625,13 @@ export const platforms: PlatformIntegration[] = [
     language: 'python',
     link: 'https://docs.sentry.io/platforms/python/guides/flask/',
   },
+  {
+    id: 'python-litestar',
+    name: 'Litestar',
+    type: 'framework',
+    language: 'python',
+    link: 'https://docs.sentry.io/platforms/python/integrations/litestar/',
+  },
   {
     id: 'python-gcpfunctions',
     name: 'Google Cloud Functions (Python)',
diff --git a/static/app/gettingStartedDocs/apple-ios/logs.tsx b/static/app/gettingStartedDocs/apple-ios/logs.tsx
index dc0f9b2adc4af0..1452286ac61eb7 100644
--- a/static/app/gettingStartedDocs/apple-ios/logs.tsx
+++ b/static/app/gettingStartedDocs/apple-ios/logs.tsx
@@ -79,7 +79,7 @@ pod update`,
 SentrySDK.start { options in
     options.dsn = "${params.dsn.public}"
     // Enable logs to be sent to Sentry
-    options.experimental.enableLogs = true
+    options.enableLogs = true
 }`,
             },
             {
@@ -90,7 +90,7 @@ SentrySDK.start { options in
 [SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
     options.dsn = @"${params.dsn.public}";
     // Enable logs to be sent to Sentry
-    options.experimental.enableLogs = YES;
+    options.enableLogs = YES;
 }];`,
             },
           ],
diff --git a/static/app/gettingStartedDocs/apple-macos/logs.tsx b/static/app/gettingStartedDocs/apple-macos/logs.tsx
index da0816b6f39140..b7126428b08412 100644
--- a/static/app/gettingStartedDocs/apple-macos/logs.tsx
+++ b/static/app/gettingStartedDocs/apple-macos/logs.tsx
@@ -79,7 +79,7 @@ pod update`,
 SentrySDK.start { options in
     options.dsn = "${params.dsn.public}"
     // Enable logs to be sent to Sentry
-    options.experimental.enableLogs = true
+    options.enableLogs = true
 }`,
             },
             {
@@ -90,7 +90,7 @@ SentrySDK.start { options in
 [SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
     options.dsn = @"${params.dsn.public}";
     // Enable logs to be sent to Sentry
-    options.experimental.enableLogs = YES;
+    options.enableLogs = YES;
 }];`,
             },
           ],
diff --git a/static/app/gettingStartedDocs/python-litestar/index.tsx b/static/app/gettingStartedDocs/python-litestar/index.tsx
new file mode 100644
index 00000000000000..ac15cdf42754fe
--- /dev/null
+++ b/static/app/gettingStartedDocs/python-litestar/index.tsx
@@ -0,0 +1,27 @@
+import type {Docs} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {
+  feedbackOnboardingJsLoader,
+  replayOnboardingJsLoader,
+} from 'sentry/gettingStartedDocs/javascript/jsLoader';
+import {agentMonitoring} from 'sentry/gettingStartedDocs/python/agentMonitoring';
+import {crashReport} from 'sentry/gettingStartedDocs/python/crashReport';
+import {featureFlag} from 'sentry/gettingStartedDocs/python/featureFlag';
+import {logs} from 'sentry/gettingStartedDocs/python/logs';
+import {mcp} from 'sentry/gettingStartedDocs/python/mcp';
+import {metrics} from 'sentry/gettingStartedDocs/python/metrics';
+import {profiling} from 'sentry/gettingStartedDocs/python/profiling';
+
+import {onboarding} from './onboarding';
+
+export const docs: Docs = {
+  onboarding,
+  replayOnboardingJsLoader,
+  profilingOnboarding: profiling(),
+  crashReportOnboarding: crashReport,
+  featureFlagOnboarding: featureFlag,
+  feedbackOnboardingJsLoader,
+  agentMonitoringOnboarding: agentMonitoring,
+  mcpOnboarding: mcp,
+  logsOnboarding: logs(),
+  metricsOnboarding: metrics(),
+};
diff --git a/static/app/gettingStartedDocs/python-litestar/onboarding.spec.tsx b/static/app/gettingStartedDocs/python-litestar/onboarding.spec.tsx
new file mode 100644
index 00000000000000..52297632b3d33b
--- /dev/null
+++ b/static/app/gettingStartedDocs/python-litestar/onboarding.spec.tsx
@@ -0,0 +1,132 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {renderWithOnboardingLayout} from 'sentry-test/onboarding/renderWithOnboardingLayout';
+import {screen} from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
+
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
+
+import {docs} from '.';
+
+describe('litestar onboarding docs', () => {
+  it('renders doc correctly', () => {
+    renderWithOnboardingLayout(docs);
+
+    // Renders main headings
+    expect(screen.getByRole('heading', {name: 'Install'})).toBeInTheDocument();
+    expect(screen.getByRole('heading', {name: 'Configure SDK'})).toBeInTheDocument();
+    expect(screen.getByRole('heading', {name: 'Verify'})).toBeInTheDocument();
+  });
+
+  it('renders without tracing', () => {
+    renderWithOnboardingLayout(docs, {
+      selectedProducts: [],
+    });
+
+    // Does not render config option
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/profiles_sample_rate=1\.0,/))
+    ).not.toBeInTheDocument();
+
+    // Does not render config option
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/traces_sample_rate=1\.0,/))
+    ).not.toBeInTheDocument();
+  });
+
+  it('renders transaction profiling', () => {
+    renderWithOnboardingLayout(docs);
+
+    // Does not render continuous profiling config
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/profile_session_sample_rate=1\.0,/))
+    ).not.toBeInTheDocument();
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/profile_lifecycle="trace",/))
+    ).not.toBeInTheDocument();
+
+    // Does render transaction profiling config
+    const matches = screen.getAllByText(
+      textWithMarkupMatcher(/profiles_sample_rate=1\.0,/)
+    );
+    expect(matches.length).toBeGreaterThan(0);
+    matches.forEach(match => expect(match).toBeInTheDocument());
+  });
+
+  it('renders continuous profiling', () => {
+    const organization = OrganizationFixture({
+      features: ['continuous-profiling'],
+    });
+
+    renderWithOnboardingLayout(
+      docs,
+      {},
+      {
+        organization,
+      }
+    );
+
+    // Does not render transaction profiling config
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/profiles_sample_rate=1\.0,/))
+    ).not.toBeInTheDocument();
+
+    // Does render continuous profiling config
+    const sampleRateMatches = screen.getAllByText(
+      textWithMarkupMatcher(/profile_session_sample_rate=1\.0,/)
+    );
+    expect(sampleRateMatches.length).toBeGreaterThan(0);
+    sampleRateMatches.forEach(match => expect(match).toBeInTheDocument());
+    const lifecycleMatches = screen.getAllByText(
+      textWithMarkupMatcher(/profile_lifecycle="trace",/)
+    );
+    expect(lifecycleMatches.length).toBeGreaterThan(0);
+    lifecycleMatches.forEach(match => expect(match).toBeInTheDocument());
+  });
+
+  it('renders with logs', () => {
+    renderWithOnboardingLayout(docs, {
+      selectedProducts: [ProductSolution.LOGS],
+    });
+
+    const logMatches = screen.getAllByText(textWithMarkupMatcher(/enable_logs=True,/));
+    expect(logMatches.length).toBeGreaterThan(0);
+    logMatches.forEach(match => expect(match).toBeInTheDocument());
+  });
+
+  it('renders without logs', () => {
+    renderWithOnboardingLayout(docs, {
+      selectedProducts: [],
+    });
+
+    expect(
+      screen.queryByText(textWithMarkupMatcher(/enable_logs=True,/))
+    ).not.toBeInTheDocument();
+  });
+
+  it('renders metrics configuration when metrics are selected', () => {
+    renderWithOnboardingLayout(docs, {
+      selectedProducts: [ProductSolution.METRICS],
+    });
+
+    // Renders metrics verification steps
+    expect(
+      screen.getByText(
+        'Send test metrics from your app to verify metrics are arriving in Sentry.'
+      )
+    ).toBeInTheDocument();
+  });
+
+  it('renders without metrics configuration when metrics are not selected', () => {
+    renderWithOnboardingLayout(docs, {
+      selectedProducts: [],
+    });
+
+    // Does not render metrics verification steps
+    expect(
+      screen.queryByText(
+        'Send test metrics from your app to verify metrics are arriving in Sentry.'
+      )
+    ).not.toBeInTheDocument();
+  });
+});
diff --git a/static/app/gettingStartedDocs/python-litestar/onboarding.tsx b/static/app/gettingStartedDocs/python-litestar/onboarding.tsx
new file mode 100644
index 00000000000000..67bef7e077f9e8
--- /dev/null
+++ b/static/app/gettingStartedDocs/python-litestar/onboarding.tsx
@@ -0,0 +1,163 @@
+import {ExternalLink} from '@sentry/scraps/link';
+
+import {
+  StepType,
+  type DocsParams,
+  type OnboardingConfig,
+} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {logsVerify} from 'sentry/gettingStartedDocs/python/logs';
+import {metricsVerify} from 'sentry/gettingStartedDocs/python/metrics';
+import {alternativeProfiling} from 'sentry/gettingStartedDocs/python/profiling';
+import {getPythonInstallCodeBlock} from 'sentry/gettingStartedDocs/python/utils';
+import {t, tct} from 'sentry/locale';
+
+const getSdkSetupSnippet = (params: DocsParams) => `
+import sentry_sdk
+
+sentry_sdk.init(
+    dsn="${params.dsn.public}",
+    # Add data like request headers and IP for users,
+    # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
+    send_default_pii=True,${
+      params.isLogsSelected
+        ? `
+    # Enable sending logs to Sentry
+    enable_logs=True,`
+        : ''
+    }${
+      params.isPerformanceSelected
+        ? `
+    # Set traces_sample_rate to 1.0 to capture 100%
+    # of transactions for tracing.
+    traces_sample_rate=1.0,`
+        : ''
+    }${
+      params.isProfilingSelected &&
+      params.profilingOptions?.defaultProfilingMode !== 'continuous'
+        ? `
+    # Set profiles_sample_rate to 1.0 to profile 100%
+    # of sampled transactions.
+    # We recommend adjusting this value in production.
+    profiles_sample_rate=1.0,`
+        : params.isProfilingSelected &&
+            params.profilingOptions?.defaultProfilingMode === 'continuous'
+          ? `
+    # Set profile_session_sample_rate to 1.0 to profile 100%
+    # of profile sessions.
+    profile_session_sample_rate=1.0,
+    # Set profile_lifecycle to "trace" to automatically
+    # run the profiler on when there is an active transaction
+    profile_lifecycle="trace",`
+          : ''
+    }
+)
+`;
+
+export const onboarding: OnboardingConfig = {
+  introduction: () =>
+    tct('The Litestar integration adds support for the [link:Litestar Web Framework].', {
+      link: ,
+    }),
+  install: () => [
+    {
+      type: StepType.INSTALL,
+      content: [
+        {
+          type: 'text',
+          text: tct('Install [code:sentry-sdk] from PyPI:', {
+            code: ,
+          }),
+        },
+        getPythonInstallCodeBlock({additionalPackage: 'litestar'}),
+      ],
+    },
+  ],
+  configure: (params: DocsParams) => [
+    {
+      type: StepType.CONFIGURE,
+      content: [
+        {
+          type: 'text',
+          text: tct(
+            'If you have the [codeLitestar:litestar] package in your dependencies, the Litestar integration will be enabled automatically when you initialize the Sentry SDK. Initialize the Sentry SDK before your app has been initialized:',
+            {
+              codeLitestar: ,
+            }
+          ),
+        },
+        {
+          type: 'code',
+          language: 'python',
+          code: `
+${getSdkSetupSnippet(params)}
+from litestar import Litestar, get
+
+@get("/hello")
+async def hello_world() -> str:
+    return "Hello!"
+
+app = Litestar(route_handlers=[hello_world])
+      `,
+        },
+        alternativeProfiling(params),
+      ],
+    },
+  ],
+  verify: (params: DocsParams) => [
+    {
+      type: StepType.VERIFY,
+      content: [
+        {
+          type: 'text',
+          text: t('To verify that everything is working, trigger an error on purpose:'),
+        },
+        {
+          type: 'code',
+          language: 'python',
+          code: `
+${getSdkSetupSnippet(params)}
+from litestar import Litestar, get
+
+@get("/hello")
+async def hello_world() -> str:
+    1 / 0  # raises an error
+    return "Hello!"
+
+app = Litestar(route_handlers=[hello_world])
+`,
+        },
+        logsVerify(params),
+        metricsVerify(params),
+        {
+          type: 'text',
+          text: [
+            tct(
+              'When you point your browser to [link:http://localhost:8000/hello] a transaction in the Performance section of Sentry will be created.',
+              {
+                link: ,
+              }
+            ),
+            t(
+              'Additionally, an error event will be sent to Sentry and will be connected to the transaction.'
+            ),
+            t('It takes a couple of moments for the data to appear in Sentry.'),
+          ],
+        },
+      ],
+    },
+  ],
+  nextSteps: (params: DocsParams) => {
+    const steps = [] as any[];
+    if (params.isLogsSelected) {
+      steps.push({
+        id: 'logs',
+        name: t('Logging Integrations'),
+        description: t(
+          'Add logging integrations to automatically capture logs from your application.'
+        ),
+        link: 'https://docs.sentry.io/platforms/python/logs/#integrations',
+      });
+    }
+    return steps;
+  },
+};
diff --git a/static/app/types/project.tsx b/static/app/types/project.tsx
index 9ffb1f739e6f01..91692954341d04 100644
--- a/static/app/types/project.tsx
+++ b/static/app/types/project.tsx
@@ -91,6 +91,7 @@ export type Project = {
   preprodSnapshotStatusChecksEnabled?: boolean;
   preprodSnapshotStatusChecksFailOnAdded?: boolean;
   preprodSnapshotStatusChecksFailOnRemoved?: boolean;
+  scmSourceContextEnabled?: boolean;
   securityToken?: string;
   securityTokenHeader?: string;
   seerScannerAutomation?: boolean;
@@ -298,6 +299,7 @@ export type PlatformKey =
   | 'python-fastapi'
   | 'python-flask'
   | 'python-gcpfunctions'
+  | 'python-litestar'
   | 'python-pylons'
   | 'python-pymongo'
   | 'python-pyramid'
diff --git a/static/app/utils/discover/genericDiscoverQuery.tsx b/static/app/utils/discover/genericDiscoverQuery.tsx
index f621325d8ecced..b0668f774041ca 100644
--- a/static/app/utils/discover/genericDiscoverQuery.tsx
+++ b/static/app/utils/discover/genericDiscoverQuery.tsx
@@ -406,8 +406,8 @@ export function useGenericDiscoverQuery(props: Props) {
   const apiPayload = getPayload(props);
   const additionalQueryKey = props.options?.additionalQueryKey ?? [];
 
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
   const res = useQuery<[T, string | undefined, ResponseMeta | undefined], QueryError>({
-    // eslint-disable-next-line @tanstack/query/exhaustive-deps
     queryKey: [...additionalQueryKey, route, apiPayload],
     queryFn: ({signal: _signal}) =>
       doDiscoverQuery(api, url, apiPayload, {
diff --git a/static/app/utils/profiling/hooks/useProfileEvents.spec.tsx b/static/app/utils/profiling/hooks/useProfileEvents.spec.tsx
index 379321ca99e73e..b8d32674f21711 100644
--- a/static/app/utils/profiling/hooks/useProfileEvents.spec.tsx
+++ b/static/app/utils/profiling/hooks/useProfileEvents.spec.tsx
@@ -42,7 +42,7 @@ describe('useProfileEvents', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual(body);
   });
 
diff --git a/static/app/utils/profiling/hooks/useProfileEventsStats.spec.tsx b/static/app/utils/profiling/hooks/useProfileEventsStats.spec.tsx
index 86671b8e695fe5..487464ebe25271 100644
--- a/static/app/utils/profiling/hooks/useProfileEventsStats.spec.tsx
+++ b/static/app/utils/profiling/hooks/useProfileEventsStats.spec.tsx
@@ -27,7 +27,7 @@ describe('useProfileEvents', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual({
       data: [],
       meta: {
@@ -74,7 +74,7 @@ describe('useProfileEvents', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual({
       data: [{axis: 'count()', values: [1, 2]}],
       meta: {
@@ -135,7 +135,7 @@ describe('useProfileEvents', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual({
       data: [
         {axis: 'count()', values: [1, 2]},
@@ -187,7 +187,7 @@ describe('useProfileEvents', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual({
       data: [{axis: 'count()', values: [1, 2]}],
       meta: {
diff --git a/static/app/utils/useProjectSdkNeedsUpdate.spec.tsx b/static/app/utils/useProjectSdkNeedsUpdate.spec.tsx
index 9519eeccc68433..adf0bf1977b87c 100644
--- a/static/app/utils/useProjectSdkNeedsUpdate.spec.tsx
+++ b/static/app/utils/useProjectSdkNeedsUpdate.spec.tsx
@@ -43,9 +43,9 @@ describe('useProjectSdkNeedsUpdate', () => {
     });
 
     await waitFor(() => {
-      expect(result.current.isError).toBeFalsy();
+      expect(result.current.isFetching).toBeFalsy();
     });
-    expect(result.current.isFetching).toBeFalsy();
+    expect(result.current.isError).toBeFalsy();
     expect(result.current.needsUpdate).toBeFalsy();
   });
 
@@ -65,9 +65,9 @@ describe('useProjectSdkNeedsUpdate', () => {
       },
     });
     await waitFor(() => {
-      expect(result.current.isError).toBeFalsy();
+      expect(result.current.isFetching).toBeFalsy();
     });
-    expect(result.current.isFetching).toBeFalsy();
+    expect(result.current.isError).toBeFalsy();
     expect(result.current.needsUpdate).toBeTruthy();
   });
 
@@ -92,9 +92,9 @@ describe('useProjectSdkNeedsUpdate', () => {
     });
 
     await waitFor(() => {
-      expect(result.current.isError).toBeFalsy();
+      expect(result.current.isFetching).toBeFalsy();
     });
-    expect(result.current.isFetching).toBeFalsy();
+    expect(result.current.isError).toBeFalsy();
     expect(result.current.needsUpdate).toBeTruthy();
   });
 
@@ -119,9 +119,9 @@ describe('useProjectSdkNeedsUpdate', () => {
     });
 
     await waitFor(() => {
-      expect(result.current.isError).toBeFalsy();
+      expect(result.current.isFetching).toBeFalsy();
     });
-    expect(result.current.isFetching).toBeFalsy();
+    expect(result.current.isError).toBeFalsy();
     expect(result.current.needsUpdate).toBeFalsy();
   });
 });
diff --git a/static/app/views/acceptOrganizationInvite/index.spec.tsx b/static/app/views/acceptOrganizationInvite/index.spec.tsx
index 6d6d816ae84519..e3fe6227cbee3d 100644
--- a/static/app/views/acceptOrganizationInvite/index.spec.tsx
+++ b/static/app/views/acceptOrganizationInvite/index.spec.tsx
@@ -140,8 +140,7 @@ describe('AcceptOrganizationInvite', () => {
       initialRouterConfig: defaultRouterConfig,
     });
 
-    await waitFor(() => expect(getJoinButton()).not.toBeInTheDocument());
-    expect(screen.getByTestId('action-info-general')).toBeInTheDocument();
+    expect(await screen.findByTestId('action-info-general')).toBeInTheDocument();
     expect(screen.queryByTestId('action-info-sso')).not.toBeInTheDocument();
 
     expect(
@@ -167,8 +166,7 @@ describe('AcceptOrganizationInvite', () => {
       initialRouterConfig: defaultRouterConfig,
     });
 
-    await waitFor(() => expect(getJoinButton()).not.toBeInTheDocument());
-    expect(screen.getByTestId('action-info-general')).toBeInTheDocument();
+    expect(await screen.findByTestId('action-info-general')).toBeInTheDocument();
     expect(screen.getByTestId('action-info-sso')).toBeInTheDocument();
 
     expect(screen.getByRole('button', {name: 'Join with SSO'})).toBeInTheDocument();
@@ -195,9 +193,8 @@ describe('AcceptOrganizationInvite', () => {
       initialRouterConfig: defaultRouterConfig,
     });
 
-    await waitFor(() => expect(getJoinButton()).not.toBeInTheDocument());
+    expect(await screen.findByTestId('action-info-sso')).toBeInTheDocument();
     expect(screen.queryByTestId('action-info-general')).not.toBeInTheDocument();
-    expect(screen.getByTestId('action-info-sso')).toBeInTheDocument();
 
     expect(screen.getByRole('button', {name: 'Join with SSO'})).toBeInTheDocument();
     expect(
@@ -223,9 +220,8 @@ describe('AcceptOrganizationInvite', () => {
       initialRouterConfig: defaultRouterConfig,
     });
 
-    await waitFor(() => expect(getJoinButton()).not.toBeInTheDocument());
+    expect(await screen.findByTestId('action-info-sso')).toBeInTheDocument();
     expect(screen.queryByTestId('action-info-general')).not.toBeInTheDocument();
-    expect(screen.getByTestId('action-info-sso')).toBeInTheDocument();
 
     expect(screen.getByRole('button', {name: 'Join with SSO'})).toBeInTheDocument();
     expect(
diff --git a/static/app/views/admin/installWizard/index.spec.tsx b/static/app/views/admin/installWizard/index.spec.tsx
index eafd524f7a3c0d..3c1a109f01616f 100644
--- a/static/app/views/admin/installWizard/index.spec.tsx
+++ b/static/app/views/admin/installWizard/index.spec.tsx
@@ -40,9 +40,8 @@ describe('InstallWizard', () => {
       }),
     });
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
-      screen.getByRole('radio', {
+      await screen.findByRole('radio', {
         name: 'Please keep my usage information anonymous',
       })
     ).not.toBeChecked();
@@ -72,9 +71,8 @@ describe('InstallWizard', () => {
       }),
     });
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
-      screen.getByRole('radio', {
+      await screen.findByRole('radio', {
         name: 'Please keep my usage information anonymous',
       })
     ).not.toBeChecked();
diff --git a/static/app/views/alerts/create.spec.tsx b/static/app/views/alerts/create.spec.tsx
index 03569250367aea..2c452890a9c3a6 100644
--- a/static/app/views/alerts/create.spec.tsx
+++ b/static/app/views/alerts/create.spec.tsx
@@ -542,9 +542,12 @@ describe('ProjectAlertsCreate', () => {
         );
       });
       expect(
-        screen.getByText('4 issues would have triggered this rule in the past 14 days', {
-          exact: false,
-        })
+        await screen.findByText(
+          '4 issues would have triggered this rule in the past 14 days',
+          {
+            exact: false,
+          }
+        )
       ).toBeInTheDocument();
       for (const group of groups) {
         expect(screen.getByText(group.shortId)).toBeInTheDocument();
@@ -591,7 +594,9 @@ describe('ProjectAlertsCreate', () => {
         expect(mock).toHaveBeenCalled();
       });
       expect(
-        screen.getByText("We couldn't find any issues that would've triggered your rule")
+        await screen.findByText(
+          "We couldn't find any issues that would've triggered your rule"
+        )
       ).toBeInTheDocument();
     });
   });
diff --git a/static/app/views/alerts/rules/issue/index.spec.tsx b/static/app/views/alerts/rules/issue/index.spec.tsx
index 9999c214af60b8..45336dd82e881a 100644
--- a/static/app/views/alerts/rules/issue/index.spec.tsx
+++ b/static/app/views/alerts/rules/issue/index.spec.tsx
@@ -500,11 +500,13 @@ describe('IssueRuleEditor', () => {
         body: {uuid},
       });
       const {router} = createWrapper();
+      // Flush the initial debounced preview fetch (500ms debounce)
+      await act(() => jest.advanceTimersByTimeAsync(600));
       await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
         delay: null,
       });
 
-      act(() => jest.advanceTimersByTime(1000));
+      await act(() => jest.advanceTimersByTimeAsync(1000));
       await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
       await waitFor(() => expect(addSuccessMessage).toHaveBeenCalledTimes(1));
       await waitFor(() => expect(mockSuccess).toHaveBeenCalledTimes(1));
@@ -524,12 +526,15 @@ describe('IssueRuleEditor', () => {
         statusCode: 202,
         body: {uuid},
       });
+
       createWrapper();
+      // Flush the initial debounced preview fetch (500ms debounce)
+      await act(() => jest.advanceTimersByTimeAsync(600));
       await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
         delay: null,
       });
 
-      act(() => jest.advanceTimersByTime(1000));
+      await act(() => jest.advanceTimersByTimeAsync(1000));
       expect(addLoadingMessage).toHaveBeenCalledTimes(2);
       expect(pollingMock).toHaveBeenCalledTimes(1);
       expect(await screen.findByTestId('loading-mask')).toBeInTheDocument();
@@ -547,11 +552,13 @@ describe('IssueRuleEditor', () => {
         body: {uuid},
       });
       createWrapper();
+      // Flush the initial debounced preview fetch (500ms debounce)
+      await act(() => jest.advanceTimersByTimeAsync(600));
       await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
         delay: null,
       });
 
-      act(() => jest.advanceTimersByTime(1000));
+      await act(() => jest.advanceTimersByTimeAsync(1000));
       await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
       await waitFor(() => expect(mockFailed).toHaveBeenCalledTimes(1));
       expect(addErrorMessage).toHaveBeenCalledTimes(1);
diff --git a/static/app/views/alerts/rules/metric/details/index.spec.tsx b/static/app/views/alerts/rules/metric/details/index.spec.tsx
index 29769db474f4f0..38204ab4891e08 100644
--- a/static/app/views/alerts/rules/metric/details/index.spec.tsx
+++ b/static/app/views/alerts/rules/metric/details/index.spec.tsx
@@ -234,7 +234,7 @@ describe('MetricAlertDetails', () => {
 
     expect(await screen.findByText(rule.name)).toBeInTheDocument();
 
-    const button = screen.getByRole('button', {name: 'Open in Discover'});
+    const button = await screen.findByRole('button', {name: 'Open in Discover'});
     expect(button).toBeInTheDocument();
     expect(button).toBeEnabled();
     expect(button).toHaveAttribute('href', expect.stringContaining('dataset=errors'));
diff --git a/static/app/views/alerts/rules/metric/details/ongoingIssues.spec.tsx b/static/app/views/alerts/rules/metric/details/ongoingIssues.spec.tsx
index b5bebda989a42c..6e1cd66a162c0d 100644
--- a/static/app/views/alerts/rules/metric/details/ongoingIssues.spec.tsx
+++ b/static/app/views/alerts/rules/metric/details/ongoingIssues.spec.tsx
@@ -40,7 +40,7 @@ describe('MetricAlertOngoingIssues', () => {
 
     render(, {organization});
 
-    expect(await screen.findByTestId('group')).toBeInTheDocument();
+    expect(await screen.findByTestId('group', {}, {timeout: 5000})).toBeInTheDocument();
     expect(detectorRequest).toHaveBeenCalled();
     expect(issuesRequest).toHaveBeenCalled();
   });
diff --git a/static/app/views/automations/components/automationBuilderDrawerForm.spec.tsx b/static/app/views/automations/components/automationBuilderDrawerForm.spec.tsx
index 30d7078fca9e12..d9b48a0be0747b 100644
--- a/static/app/views/automations/components/automationBuilderDrawerForm.spec.tsx
+++ b/static/app/views/automations/components/automationBuilderDrawerForm.spec.tsx
@@ -125,7 +125,10 @@ describe('AutomationBuilderDrawerForm', () => {
     );
 
     // Add an action
-    await selectEvent.select(screen.getByRole('textbox', {name: 'Add action'}), 'Slack');
+    await selectEvent.select(
+      await screen.findByRole('textbox', {name: 'Add action'}),
+      'Slack'
+    );
     await userEvent.type(screen.getByRole('textbox', {name: 'Target'}), '#alerts');
 
     // Fill in the automation name - clear first to override auto-generated name
@@ -152,5 +155,5 @@ describe('AutomationBuilderDrawerForm', () => {
     await waitFor(() => {
       expect(onSuccess).toHaveBeenCalledWith('123');
     });
-  });
+  }, 10_000);
 });
diff --git a/static/app/views/automations/components/dataConditionNodeList.spec.tsx b/static/app/views/automations/components/dataConditionNodeList.spec.tsx
index 3249cf99b3ea02..8519ad788c6098 100644
--- a/static/app/views/automations/components/dataConditionNodeList.spec.tsx
+++ b/static/app/views/automations/components/dataConditionNodeList.spec.tsx
@@ -2,7 +2,13 @@ import {DataConditionFixture} from 'sentry-fixture/automations';
 import {OrganizationFixture} from 'sentry-fixture/organization';
 import {DataConditionHandlerFixture} from 'sentry-fixture/workflowEngine';
 
-import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+  within,
+} from 'sentry-test/reactTestingLibrary';
 
 import {IssueType} from 'sentry/types/group';
 import type {DataConditionHandler} from 'sentry/types/workflowEngine/dataConditions';
@@ -165,11 +171,16 @@ describe('DataConditionNodeList', () => {
       {organization}
     );
 
-    // Wait until the request for tags is completed
+    // Wait until the request for tags is completed and the select is no longer disabled
     const tagInput = await screen.findByRole('textbox', {name: 'Tag'});
+    await waitFor(() => {
+      expect(tagInput).toBeEnabled();
+    });
     await userEvent.type(tagInput, 'names{enter}');
-    expect(mockUpdateCondition).toHaveBeenCalledWith('1', {
-      comparison: {key: 'names', match: MatchType.CONTAINS, value: 'moo deng'},
+    await waitFor(() => {
+      expect(mockUpdateCondition).toHaveBeenCalledWith('1', {
+        comparison: {key: 'names', match: MatchType.CONTAINS, value: 'moo deng'},
+      });
     });
   });
 
diff --git a/static/app/views/automations/hooks/index.tsx b/static/app/views/automations/hooks/index.tsx
index 5917799825fa1e..6b93a4ee4037fc 100644
--- a/static/app/views/automations/hooks/index.tsx
+++ b/static/app/views/automations/hooks/index.tsx
@@ -394,7 +394,7 @@ export function useSendTestNotification(
         }
       ),
     ...options,
-    onSuccess: (data, variables, context) => {
+    onSuccess: (data, variables, onMutateResult, context) => {
       queryClient.invalidateQueries({
         queryKey: [
           getApiUrl('/organizations/$organizationIdOrSlug/workflows/', {
@@ -405,16 +405,16 @@ export function useSendTestNotification(
       addSuccessMessage(
         tn('Notification fired!', 'Notifications sent!', variables.length)
       );
-      options?.onSuccess?.(data, variables, context);
+      options?.onSuccess?.(data, variables, onMutateResult, context);
     },
-    onError: (error, variables, context) => {
+    onError: (error, variables, onMutateResult, context) => {
       const detail = error.responseJSON?.detail;
       const message = typeof detail === 'string' ? detail : detail?.message;
 
       addErrorMessage(
         message || tn('Notification failed', 'Notifications failed', variables.length)
       );
-      options?.onError?.(error, variables, context);
+      options?.onError?.(error, variables, onMutateResult, context);
     },
   });
 }
diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx
index b0b0495338f670..002d58c5a47a2d 100644
--- a/static/app/views/dashboards/detail.spec.tsx
+++ b/static/app/views/dashboards/detail.spec.tsx
@@ -503,7 +503,7 @@ describe('Dashboards > Detail', () => {
       await waitFor(() => expect(mockVisit).toHaveBeenCalledTimes(1));
 
       // Enter edit mode.
-      await userEvent.click(screen.getByRole('button', {name: 'edit-dashboard'}));
+      await userEvent.click(await screen.findByRole('button', {name: 'edit-dashboard'}));
 
       // Remove the second and third widgets
       await userEvent.click(
@@ -1347,8 +1347,7 @@ describe('Dashboards > Detail', () => {
         organization: testData.organization,
       });
 
-      await waitFor(() => expect(screen.queryAllByText('Loading\u2026')).toEqual([]));
-      await userEvent.click(screen.getByRole('button', {name: 'All Envs'}));
+      await userEvent.click(await screen.findByRole('button', {name: 'All Envs'}));
       expect(screen.getByRole('row', {name: 'alpha'})).toHaveAttribute(
         'aria-selected',
         'true'
diff --git a/static/app/views/dashboards/globalFilter/filterSelector.tsx b/static/app/views/dashboards/globalFilter/filterSelector.tsx
index 3338bed0de52e4..f658a3e0be69f4 100644
--- a/static/app/views/dashboards/globalFilter/filterSelector.tsx
+++ b/static/app/views/dashboards/globalFilter/filterSelector.tsx
@@ -183,6 +183,7 @@ export function FilterSelector({
   );
   const queryKey = useDebouncedValue(baseQueryKey);
 
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
   const queryResult = useQuery({
     queryKey,
     queryFn: async ctx => {
diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx
index 362425147c2e23..587fc27239448f 100644
--- a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx
+++ b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx
@@ -56,7 +56,7 @@ describe('EventsSearchBar', () => {
     await userEvent.paste('has:p', {delay: null});
 
     await userEvent.click(
-      screen.getByRole('button', {name: 'Edit value for filter: has'})
+      await screen.findByRole('button', {name: 'Edit value for filter: has'})
     );
 
     // Assert we actually have has: dropdown options before checking exclusions.
diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar.spec.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar.spec.tsx
index 4874824c977c0e..0ccb3c9dadf73d 100644
--- a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar.spec.tsx
+++ b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar.spec.tsx
@@ -134,9 +134,9 @@ describe('SpansSearchBar', () => {
     const searchInput = await screen.findByRole('combobox', {
       name: 'Add a search term',
     });
-    await userEvent.type(searchInput, 'span.op:');
+    await userEvent.type(searchInput, 'span.op:', {delay: null});
     await userEvent.keyboard('{enter}');
-    await userEvent.keyboard('function');
+    await userEvent.keyboard('function', {delay: null});
     await userEvent.keyboard('{enter}');
 
     await waitFor(() => {
diff --git a/static/app/views/dashboards/widgetCard/confidenceFooter.spec.tsx b/static/app/views/dashboards/widgetCard/confidenceFooter.spec.tsx
index 9e0c2bf770e64b..bc5705290cb14e 100644
--- a/static/app/views/dashboards/widgetCard/confidenceFooter.spec.tsx
+++ b/static/app/views/dashboards/widgetCard/confidenceFooter.spec.tsx
@@ -2,7 +2,7 @@ import {PageFiltersFixture} from 'sentry-fixture/pageFilters';
 import {WidgetFixture} from 'sentry-fixture/widget';
 import {WidgetQueryFixture} from 'sentry-fixture/widgetQuery';
 
-import {render, screen} from 'sentry-test/reactTestingLibrary';
+import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import {PageFiltersStore} from 'sentry/components/pageFilters/store';
 import type {Series, SeriesDataUnit} from 'sentry/types/echarts';
@@ -112,7 +112,7 @@ describe('WidgetCardConfidenceFooter', () => {
 
     const footer = await screen.findByText(/Estimated for top 2 groups from/i);
     expect(footer).toHaveTextContent('18 matches');
-    expect(footer).toHaveTextContent('500 spans');
+    await waitFor(() => expect(footer).toHaveTextContent('500 spans'));
   });
 
   it('excludes Other from spans top event metadata', async () => {
@@ -193,7 +193,7 @@ describe('WidgetCardConfidenceFooter', () => {
 
     const footer = await screen.findByText(/Estimated from/i);
     expect(footer).toHaveTextContent('10 matches');
-    expect(footer).toHaveTextContent('500 data points');
+    await waitFor(() => expect(footer).toHaveTextContent('500 data points'));
   });
 
   it('renders logs footer with raw counts from API', async () => {
@@ -233,7 +233,7 @@ describe('WidgetCardConfidenceFooter', () => {
 
     const footer = await screen.findByText(/Estimated from/i);
     expect(footer).toHaveTextContent('10 matches');
-    expect(footer).toHaveTextContent('500 logs');
+    await waitFor(() => expect(footer).toHaveTextContent('500 logs'));
   });
 
   it('does not render footer for unsupported widget types', () => {
diff --git a/static/app/views/detectors/components/details/cron/index.spec.tsx b/static/app/views/detectors/components/details/cron/index.spec.tsx
index 6d7d94a8f66e7a..2123a6a8c92cfc 100644
--- a/static/app/views/detectors/components/details/cron/index.spec.tsx
+++ b/static/app/views/detectors/components/details/cron/index.spec.tsx
@@ -134,7 +134,7 @@ describe('CronDetectorDetails - check-ins', () => {
 
       expect(await screen.findByText('Recent Check-Ins')).toBeInTheDocument();
       expect(
-        screen.getByText('No check-ins have been recorded for this time period.')
+        await screen.findByText('No check-ins have been recorded for this time period.')
       ).toBeInTheDocument();
     });
 
@@ -211,7 +211,7 @@ describe('CronDetectorDetails - check-ins', () => {
       // Wait for check-ins to load and find the table after the heading
       const recentCheckInsHeading = await screen.findByText('Recent Check-Ins');
       const container = recentCheckInsHeading.parentElement!.parentElement!;
-      const checkInTable = within(container).getByRole('table');
+      const checkInTable = await within(container).findByRole('table');
 
       // Find the "Started" column index
       const headers = within(checkInTable).getAllByRole('columnheader');
diff --git a/static/app/views/detectors/components/forms/automateSection.spec.tsx b/static/app/views/detectors/components/forms/automateSection.spec.tsx
index 3d45dd6ddec814..801595242a33d5 100644
--- a/static/app/views/detectors/components/forms/automateSection.spec.tsx
+++ b/static/app/views/detectors/components/forms/automateSection.spec.tsx
@@ -137,7 +137,7 @@ describe('AutomateSection', () => {
       
     );
 
-    expect(screen.getByText('Alert')).toBeInTheDocument();
+    expect(await screen.findByText('Alert')).toBeInTheDocument();
 
     await userEvent.click(screen.getByText('Connect Existing Alerts'));
 
@@ -225,7 +225,7 @@ describe('AutomateSection', () => {
     );
 
     // Click "Create New Alert" button
-    await userEvent.click(screen.getByRole('button', {name: 'Create New Alert'}));
+    await userEvent.click(await screen.findByRole('button', {name: 'Create New Alert'}));
 
     // Wait for the drawer to open
     const drawer = await screen.findByRole('complementary', {
diff --git a/static/app/views/detectors/detail.spec.tsx b/static/app/views/detectors/detail.spec.tsx
index 4745138f594030..f45895db0365f7 100644
--- a/static/app/views/detectors/detail.spec.tsx
+++ b/static/app/views/detectors/detail.spec.tsx
@@ -304,8 +304,8 @@ describe('DetectorDetails', () => {
 
       expect(await screen.findByText('Recent Check-Ins')).toBeInTheDocument();
 
-      // Verify check-in data is displayed
-      expect(screen.getAllByText('Uptime')).toHaveLength(4); // breadcrumb + section heading + timeline legend + check-in row
+      // Verify check-in data is displayed - wait for table to fully render
+      await waitFor(() => expect(screen.getAllByText('Uptime')).toHaveLength(4)); // breadcrumb + section heading + timeline legend + check-in row
       expect(screen.getByText('200')).toBeInTheDocument();
       expect(screen.getByText('US East')).toBeInTheDocument();
       expect(screen.getAllByText('Failure')).toHaveLength(2); // timeline legend + check-in row
diff --git a/static/app/views/detectors/hooks/useMetricDetectorAnomalies.spec.tsx b/static/app/views/detectors/hooks/useMetricDetectorAnomalies.spec.tsx
index 74bab682b9a1dd..26dc1ef8b72cff 100644
--- a/static/app/views/detectors/hooks/useMetricDetectorAnomalies.spec.tsx
+++ b/static/app/views/detectors/hooks/useMetricDetectorAnomalies.spec.tsx
@@ -94,7 +94,7 @@ describe('useMetricDetectorAnomalies', () => {
       );
     });
 
-    expect(result.current.data).toEqual(mockAnomalies);
+    await waitFor(() => expect(result.current.data).toEqual(mockAnomalies));
     expect(result.current.isLoading).toBe(false);
     expect(result.current.error).toBeNull();
   });
diff --git a/static/app/views/detectors/list/allMonitors.spec.tsx b/static/app/views/detectors/list/allMonitors.spec.tsx
index a4e7f8a024acb4..b4a35c824518ef 100644
--- a/static/app/views/detectors/list/allMonitors.spec.tsx
+++ b/static/app/views/detectors/list/allMonitors.spec.tsx
@@ -127,7 +127,7 @@ describe('DetectorsList', () => {
 
     render(, {organization});
     const row = await screen.findByTestId('detector-list-row');
-    expect(within(row).getByText('1 alert')).toBeInTheDocument();
+    expect(await within(row).findByText('1 alert')).toBeInTheDocument();
 
     // Tooltip should fetch and display the automation name/action
     await userEvent.hover(within(row).getByText('1 alert'));
diff --git a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx
index 0a0585c0ceed52..7b25ebe52adc0a 100644
--- a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx
+++ b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx
@@ -55,7 +55,7 @@ describe('ResultsSearchQueryBuilder', () => {
 
     // Check that "p50" (a function tag) is NOT in the dropdown
     expect(
-      within(screen.getByRole('listbox')).queryByText('p50')
+      within(await screen.findByRole('listbox')).queryByText('p50')
     ).not.toBeInTheDocument();
   });
 
diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx
index acebfc92ecddbc..23918fcec0e200 100644
--- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx
+++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx
@@ -55,7 +55,8 @@ export function useTraceItemAttributeKeys({
     query,
   });
 
-  const {data, isFetching, error} = useQuery({
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
+  const {data, isFetching, error} = useQuery({
     enabled,
     queryKey: [...queryKey, search],
     queryFn: () => getTraceItemAttributeKeys(search),
diff --git a/static/app/views/explore/hooks/useTraces.spec.tsx b/static/app/views/explore/hooks/useTraces.spec.tsx
index 6d8d30d8c9de2f..5f580aadd9fbf9 100644
--- a/static/app/views/explore/hooks/useTraces.spec.tsx
+++ b/static/app/views/explore/hooks/useTraces.spec.tsx
@@ -97,7 +97,7 @@ describe('useTraces', () => {
       },
     });
 
-    await waitFor(() => result.current.isSuccess);
+    await waitFor(() => expect(result.current.data).toBeDefined());
     expect(result.current.data).toEqual(body);
   });
 });
diff --git a/static/app/views/insights/database/components/noDataMessage.spec.tsx b/static/app/views/insights/database/components/noDataMessage.spec.tsx
index edca0bf1d57f78..350b8e11c81b8f 100644
--- a/static/app/views/insights/database/components/noDataMessage.spec.tsx
+++ b/static/app/views/insights/database/components/noDataMessage.spec.tsx
@@ -2,7 +2,7 @@ import {PageFiltersFixture} from 'sentry-fixture/pageFilters';
 import {ProjectFixture} from 'sentry-fixture/project';
 import {ProjectSdkUpdatesFixture} from 'sentry-fixture/projectSdkUpdates';
 
-import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import {usePageFilters as importedUsePageFilters} from 'sentry/components/pageFilters/usePageFilters';
@@ -53,17 +53,15 @@ describe('NoDataMessage', () => {
   });
 
   it('shows a no data message if there is no recent data', async () => {
-    const sdkMock = MockApiClient.addMockResponse({
+    MockApiClient.addMockResponse({
       url: '/organizations/org-slug/sdk-updates/',
       body: [],
     });
 
     render();
-    await waitFor(() => expect(sdkMock).toHaveBeenCalled());
-    await tick(); // There is no visual indicator, this awaits the promise resolve
 
     expect(
-      screen.getByText(textWithMarkupMatcher('No queries found.'))
+      await screen.findByText(textWithMarkupMatcher('No queries found.'))
     ).toBeInTheDocument();
     expect(
       screen.queryByText(
@@ -73,21 +71,18 @@ describe('NoDataMessage', () => {
   });
 
   it('shows a list of outdated SDKs if there is no data available and SDKs are outdated', async () => {
-    const sdkMock = MockApiClient.addMockResponse({
+    MockApiClient.addMockResponse({
       url: '/organizations/org-slug/sdk-updates/',
       body: [ProjectSdkUpdatesFixture({projectId: '2'})],
     });
 
     render();
 
-    await waitFor(() => expect(sdkMock).toHaveBeenCalled());
-    await tick(); // There is no visual indicator, this awaits the promise resolve
-
     expect(
-      screen.getByText(textWithMarkupMatcher('No queries found.'))
+      await screen.findByText(textWithMarkupMatcher('No queries found.'))
     ).toBeInTheDocument();
     expect(
-      screen.getByText(
+      await screen.findByText(
         textWithMarkupMatcher('You may be missing data due to outdated SDKs')
       )
     ).toBeInTheDocument();
diff --git a/static/app/views/integrationOrganizationLink/index.spec.tsx b/static/app/views/integrationOrganizationLink/index.spec.tsx
index 1f03adac1f750c..2a0525d29d04e9 100644
--- a/static/app/views/integrationOrganizationLink/index.spec.tsx
+++ b/static/app/views/integrationOrganizationLink/index.spec.tsx
@@ -87,13 +87,11 @@ describe('IntegrationOrganizationLink', () => {
     render(, {
       initialRouterConfig,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
     expect(getOrgsMock).toHaveBeenCalled();
     expect(getOrgMock).toHaveBeenCalled();
 
     // Select organization
-    await selectEvent.select(screen.getByRole('textbox'), org2.name);
+    await selectEvent.select(await screen.findByRole('textbox'), org2.name);
     expect(testableWindowLocation.assign).toHaveBeenCalledWith(
       generateOrgSlugUrl(org2.slug)
     );
@@ -116,10 +114,8 @@ describe('IntegrationOrganizationLink', () => {
     render(, {
       initialRouterConfig,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
     // Select the same organization as the domain
-    await selectEvent.select(screen.getByRole('textbox'), org2.name);
+    await selectEvent.select(await screen.findByRole('textbox'), org2.name);
     expect(testableWindowLocation.assign).not.toHaveBeenCalled();
 
     expect(screen.getByRole('button', {name: 'Install Vercel'})).toBeEnabled();
diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx
index 26c42fa94da65f..acd65fe34deba9 100644
--- a/static/app/views/issueDetails/actions/index.spec.tsx
+++ b/static/app/views/issueDetails/actions/index.spec.tsx
@@ -399,7 +399,7 @@ describe('GroupActions', () => {
       expect(groupFetchApi).toHaveBeenCalledTimes(1);
     });
 
-    await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
+    await userEvent.click(await screen.findByRole('button', {name: 'Resolve'}));
 
     expect(issuesApi).toHaveBeenCalledWith(
       `/projects/${organization.slug}/project/issues/`,
diff --git a/static/app/views/issueDetails/groupCheckIns.spec.tsx b/static/app/views/issueDetails/groupCheckIns.spec.tsx
index 0f0439fb81e8fd..4123e38911e257 100644
--- a/static/app/views/issueDetails/groupCheckIns.spec.tsx
+++ b/static/app/views/issueDetails/groupCheckIns.spec.tsx
@@ -61,7 +61,7 @@ describe('GroupCheckIns', () => {
     });
     expect(await screen.findByText('All Check-Ins')).toBeInTheDocument();
     expect(
-      screen.getByText('No check-ins have been recorded for this time period.')
+      await screen.findByText('No check-ins have been recorded for this time period.')
     ).toBeInTheDocument();
   });
 
@@ -78,7 +78,7 @@ describe('GroupCheckIns', () => {
     });
     expect(await screen.findByText('All Check-Ins')).toBeInTheDocument();
     expect(screen.queryByText('No matching check-ins found')).not.toBeInTheDocument();
-    expect(screen.getByText('Showing 1-1 matching check-ins')).toBeInTheDocument();
+    expect(await screen.findByText('Showing 1-1 matching check-ins')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Previous Page'})).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Next Page'})).toBeInTheDocument();
 
diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx
index 476255b49a2160..cf1da0915e4d6b 100644
--- a/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx
+++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx
@@ -543,7 +543,7 @@ describe('groupEventDetails', () => {
     expect(within(highlights).getByRole('button', {name: 'Edit'})).toBeInTheDocument();
     // No highlights setup
     expect(
-      within(highlights).getByRole('button', {name: 'Add Highlights'})
+      await within(highlights).findByRole('button', {name: 'Add Highlights'})
     ).toBeInTheDocument();
     expect(screen.getByText("There's nothing here...")).toBeInTheDocument();
   });
diff --git a/static/app/views/issueDetails/groupOpenPeriods.spec.tsx b/static/app/views/issueDetails/groupOpenPeriods.spec.tsx
index 66cd27b0bb4650..62ba51932cbde5 100644
--- a/static/app/views/issueDetails/groupOpenPeriods.spec.tsx
+++ b/static/app/views/issueDetails/groupOpenPeriods.spec.tsx
@@ -66,7 +66,7 @@ describe('GroupOpenPeriods', () => {
       expect(screen.getByText(column)).toBeInTheDocument();
     }
 
-    const rows = screen.getAllByTestId('grid-body-row');
+    const rows = await screen.findAllByTestId('grid-body-row');
 
     // There should be 2 rows: the first is the status changeevent , the second is the period open event
     expect(rows).toHaveLength(2);
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarIssuesDrawer.spec.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarIssuesDrawer.spec.tsx
index 307066dab3b724..4906edc0f57ef2 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarIssuesDrawer.spec.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarIssuesDrawer.spec.tsx
@@ -81,7 +81,9 @@ describe('SimilarIssuesDrawer', () => {
       await screen.findByRole('heading', {name: 'Similar Issues'})
     ).toBeInTheDocument();
 
-    expect(screen.getByText('Issues with a similar stack trace')).toBeInTheDocument();
+    expect(
+      await screen.findByText('Issues with a similar stack trace')
+    ).toBeInTheDocument();
     await waitFor(() => {
       expect(mockSimilarIssues).toHaveBeenCalled();
     });
diff --git a/static/app/views/issueDetails/streamline/eventGraph.spec.tsx b/static/app/views/issueDetails/streamline/eventGraph.spec.tsx
index 1c0796d6ee6322..9385d3c42e5536 100644
--- a/static/app/views/issueDetails/streamline/eventGraph.spec.tsx
+++ b/static/app/views/issueDetails/streamline/eventGraph.spec.tsx
@@ -190,7 +190,9 @@ describe('EventGraph', () => {
         },
       },
     });
-    expect(await screen.findByTestId('event-graph-loading')).not.toBeInTheDocument();
+    await waitFor(() => {
+      expect(screen.queryByTestId('event-graph-loading')).not.toBeInTheDocument();
+    });
 
     expect(mockEventStats).toHaveBeenCalledWith(
       '/organizations/org-slug/events-stats/',
diff --git a/static/app/views/issueDetails/streamline/issueCronCheckTimeline.spec.tsx b/static/app/views/issueDetails/streamline/issueCronCheckTimeline.spec.tsx
index a669d5446dbb92..2bfae0dd94d996 100644
--- a/static/app/views/issueDetails/streamline/issueCronCheckTimeline.spec.tsx
+++ b/static/app/views/issueDetails/streamline/issueCronCheckTimeline.spec.tsx
@@ -88,8 +88,7 @@ describe('IssueCronCheckTimeline', () => {
     });
     render(, {organization});
 
-    expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
-
+    expect(await screen.findByText(statusToText[CheckInStatus.OK])).toBeInTheDocument();
     const legend = screen.getByRole('caption');
     expect(within(legend).getByText(statusToText[CheckInStatus.OK])).toBeInTheDocument();
     expect(screen.getByRole('figure')).toBeInTheDocument();
@@ -132,7 +131,7 @@ describe('IssueCronCheckTimeline', () => {
       },
     });
     render(, {organization});
-    expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
+    expect(await screen.findByText(statusToText[CheckInStatus.OK])).toBeInTheDocument();
     const legend = screen.getByRole('caption');
     [
       statusToText[CheckInStatus.OK],
@@ -182,7 +181,7 @@ describe('IssueCronCheckTimeline', () => {
       },
     });
     render(, {organization});
-    expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
+    expect(await screen.findByText(statusToText[CheckInStatus.OK])).toBeInTheDocument();
     const legend = screen.getByRole('caption');
     // All statuses from both environment timelines should be present
     [
diff --git a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx
index 2693077d43830c..fcb3af37fb0db8 100644
--- a/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx
+++ b/static/app/views/issueDetails/streamline/issueUptimeCheckTimeline.spec.tsx
@@ -83,8 +83,10 @@ describe('IssueUptimeCheckTimeline', () => {
     });
     render(, {organization});
 
-    expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
-
+    // Wait for all legend items to render - MISSED_WINDOW ("Unknown") renders last
+    expect(
+      await screen.findByText(statusToText[CheckStatus.MISSED_WINDOW])
+    ).toBeInTheDocument();
     const legend = screen.getByRole('caption');
     expect(
       within(legend).getByText(statusToText[CheckStatus.SUCCESS])
@@ -134,8 +136,10 @@ describe('IssueUptimeCheckTimeline', () => {
       },
     });
     render(, {organization});
-    expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
 
+    expect(
+      await screen.findByText(statusToText[CheckStatus.SUCCESS])
+    ).toBeInTheDocument();
     const legend = screen.getByRole('caption');
     expect(
       within(legend).getByText(statusToText[CheckStatus.SUCCESS])
diff --git a/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.spec.tsx
index a6fb1cf0dbf01f..278c304070149d 100644
--- a/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.spec.tsx
+++ b/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.spec.tsx
@@ -256,16 +256,18 @@ describe('MetricDetectorTriggeredSection', () => {
       ).toBeInTheDocument();
     });
 
-    expect(contributingIssuesMock).toHaveBeenCalledWith(
-      expect.anything(),
-      expect.objectContaining({
-        query: expect.objectContaining({
-          query: 'issue.type:error event.type:error is:unresolved',
-          start: startDate,
-          end: openPeriodEndDate,
-        }),
-      })
-    );
+    await waitFor(() => {
+      expect(contributingIssuesMock).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.objectContaining({
+          query: expect.objectContaining({
+            query: 'issue.type:error event.type:error is:unresolved',
+            start: startDate,
+            end: openPeriodEndDate,
+          }),
+        })
+      );
+    });
 
     await screen.findByRole('link', {name: 'RequestError'});
   });
@@ -342,9 +344,11 @@ describe('MetricDetectorTriggeredSection', () => {
 
       await screen.findByRole('region', {name: 'Triggered Condition'});
 
-      expect(router.location.pathname).toMatch(
-        `/organizations/org-slug/issues/${defaultGroup.id}/events/${defaultEvent.id}/`
-      );
+      await waitFor(() => {
+        expect(router.location.pathname).toMatch(
+          `/organizations/org-slug/issues/${defaultGroup.id}/events/${defaultEvent.id}/`
+        );
+      });
       expect(router.location.query.statsPeriod).toBeDefined();
     });
 
diff --git a/static/app/views/issueDetails/useAssignIssueMutation.ts b/static/app/views/issueDetails/useAssignIssueMutation.ts
index 11f4d37215cf84..3ecc75b21ee3fb 100644
--- a/static/app/views/issueDetails/useAssignIssueMutation.ts
+++ b/static/app/views/issueDetails/useAssignIssueMutation.ts
@@ -66,14 +66,14 @@ export function useAssignIssueMutation(
         },
       });
     },
-    onMutate: async variables => {
+    onMutate: async (variables, context) => {
       const changeId = uniqueId();
       // TODO: Remove this when we no longer rely on GroupStore for updates
       GroupStore.onAssignTo(changeId, variables.groupId, {email: ''});
-      await options.onMutate?.(variables);
+      await options.onMutate?.(variables, context);
       return {changeId};
     },
-    onSuccess: (response, variables, context) => {
+    onSuccess: (response, variables, onMutateResult, context) => {
       // Update react query cache so that useGroup() reflects the new assignee
       setApiQueryData(
         queryClient,
@@ -86,16 +86,16 @@ export function useAssignIssueMutation(
       );
       // Dual-write to GroupStore
       // TODO: Remove this when we no longer rely on GroupStore for updates
-      GroupStore.onAssignToSuccess(context.changeId, variables.groupId, response);
-      options.onSuccess?.(response, variables, context);
+      GroupStore.onAssignToSuccess(onMutateResult.changeId, variables.groupId, response);
+      options.onSuccess?.(response, variables, onMutateResult, context);
     },
-    onError: (error, variables, context) => {
+    onError: (error, variables, onMutateResult, context) => {
       // TODO: Remove this when we no longer rely on GroupStore for updates
       // This will show an alert to the user, remember to replace that functionality
-      if (context) {
-        GroupStore.onAssignToError(context.changeId, variables.groupId, error);
+      if (onMutateResult) {
+        GroupStore.onAssignToError(onMutateResult.changeId, variables.groupId, error);
       }
-      options.onError?.(error, variables, context);
+      options.onError?.(error, variables, onMutateResult, context);
     },
   });
 }
diff --git a/static/app/views/issueList/issueViews/createIssueViewModal.spec.tsx b/static/app/views/issueList/issueViews/createIssueViewModal.spec.tsx
index 2a4a91613cd308..c6632c00f3a6d2 100644
--- a/static/app/views/issueList/issueViews/createIssueViewModal.spec.tsx
+++ b/static/app/views/issueList/issueViews/createIssueViewModal.spec.tsx
@@ -150,7 +150,9 @@ describe('CreateIssueViewModal', () => {
         jest.runAllTimers();
       });
 
-      expect(nameInput).toHaveValue('Generated View Title');
+      await waitFor(() => {
+        expect(nameInput).toHaveValue('Generated View Title');
+      });
     });
 
     it('does not override a pre-filled name', () => {
diff --git a/static/app/views/issueList/mutations/useCreateGroupSearchView.tsx b/static/app/views/issueList/mutations/useCreateGroupSearchView.tsx
index b0dc28ebc8dadb..b3e605fc9d315f 100644
--- a/static/app/views/issueList/mutations/useCreateGroupSearchView.tsx
+++ b/static/app/views/issueList/mutations/useCreateGroupSearchView.tsx
@@ -33,7 +33,7 @@ export function useCreateGroupSearchView(
         data,
       }),
     ...options,
-    onSuccess: (data, variables, context) => {
+    onSuccess: (data, variables, onMutateResult, context) => {
       if (variables.starred) {
         setApiQueryData(
           queryClient,
@@ -44,7 +44,7 @@ export function useCreateGroupSearchView(
         );
       }
 
-      options?.onSuccess?.(data, variables, context);
+      options?.onSuccess?.(data, variables, onMutateResult, context);
     },
   });
 }
diff --git a/static/app/views/issueList/mutations/useDeleteGroupSearchView.tsx b/static/app/views/issueList/mutations/useDeleteGroupSearchView.tsx
index 43e9cbf4a7528b..4915e659e62253 100644
--- a/static/app/views/issueList/mutations/useDeleteGroupSearchView.tsx
+++ b/static/app/views/issueList/mutations/useDeleteGroupSearchView.tsx
@@ -35,7 +35,7 @@ export const useDeleteGroupSearchView = (
           method: 'DELETE',
         }
       ),
-    onSuccess: (data, parameters, context) => {
+    onSuccess: (data, parameters, onMutateResult, context) => {
       // Invalidate the view in cache
       queryClient.invalidateQueries({
         queryKey: makeFetchGroupSearchViewKey({
@@ -52,11 +52,11 @@ export const useDeleteGroupSearchView = (
           return oldGroupSearchViews?.filter(view => view.id !== parameters.id) ?? [];
         }
       );
-      options.onSuccess?.(data, parameters, context);
+      options.onSuccess?.(data, parameters, onMutateResult, context);
     },
-    onError: (error, variables, context) => {
+    onError: (error, variables, onMutateResult, context) => {
       addErrorMessage(t('Failed to delete view'));
-      options.onError?.(error, variables, context);
+      options.onError?.(error, variables, onMutateResult, context);
     },
   });
 };
diff --git a/static/app/views/issueList/mutations/useUpdateGroupSearchView.tsx b/static/app/views/issueList/mutations/useUpdateGroupSearchView.tsx
index 36793e781ede53..8802de334a13ce 100644
--- a/static/app/views/issueList/mutations/useUpdateGroupSearchView.tsx
+++ b/static/app/views/issueList/mutations/useUpdateGroupSearchView.tsx
@@ -41,7 +41,7 @@ export const useUpdateGroupSearchView = (
         }
       ),
 
-    onMutate: variables => {
+    onMutate: (variables, context) => {
       const {optimistic, ...viewParams} = variables;
       if (optimistic) {
         // Update the specific view cache
@@ -67,9 +67,9 @@ export const useUpdateGroupSearchView = (
           }
         );
       }
-      options.onMutate?.(variables);
+      options.onMutate?.(variables, context);
     },
-    onSuccess: (data, parameters, context) => {
+    onSuccess: (data, parameters, onMutateResult, context) => {
       if (!parameters.optimistic) {
         // Update the specific view cache
         setApiQueryData(
@@ -94,11 +94,11 @@ export const useUpdateGroupSearchView = (
           }
         );
       }
-      options.onSuccess?.(data, parameters, context);
+      options.onSuccess?.(data, parameters, onMutateResult, context);
     },
-    onError: (error, variables, context) => {
+    onError: (error, variables, onMutateResult, context) => {
       addErrorMessage(t('Failed to update view'));
-      options.onError?.(error, variables, context);
+      options.onError?.(error, variables, onMutateResult, context);
     },
   });
 };
diff --git a/static/app/views/issueList/mutations/useUpdateGroupSearchViewStarred.tsx b/static/app/views/issueList/mutations/useUpdateGroupSearchViewStarred.tsx
index d5f2d083eeeeef..f4c1f39f0acd02 100644
--- a/static/app/views/issueList/mutations/useUpdateGroupSearchViewStarred.tsx
+++ b/static/app/views/issueList/mutations/useUpdateGroupSearchViewStarred.tsx
@@ -37,11 +37,11 @@ export const useUpdateGroupSearchViewStarred = (
           data: {starred},
         }
       ),
-    onError: (error, variables, context) => {
+    onError: (error, variables, onMutateResult, context) => {
       addErrorMessage(
         variables.starred ? t('Failed to star view') : t('Failed to unstar view')
       );
-      options.onError?.(error, variables, context);
+      options.onError?.(error, variables, onMutateResult, context);
     },
     onSettled: (...args) => {
       queryClient.invalidateQueries({
diff --git a/static/app/views/navigation/secondary/sections/issues/issueViews/useUpdateGroupSearchViewLastVisited.tsx b/static/app/views/navigation/secondary/sections/issues/issueViews/useUpdateGroupSearchViewLastVisited.tsx
index 904d18700dc8ea..ae2547c8ac8fb0 100644
--- a/static/app/views/navigation/secondary/sections/issues/issueViews/useUpdateGroupSearchViewLastVisited.tsx
+++ b/static/app/views/navigation/secondary/sections/issues/issueViews/useUpdateGroupSearchViewLastVisited.tsx
@@ -26,8 +26,5 @@ export function useUpdateGroupSearchViewLastVisited(
         }
       );
     },
-    onError: (error, variables, context) => {
-      options.onError?.(error, variables, context);
-    },
   });
 }
diff --git a/static/app/views/organizationStats/index.spec.tsx b/static/app/views/organizationStats/index.spec.tsx
index 266dbc50296c5f..ba682e4ef746c1 100644
--- a/static/app/views/organizationStats/index.spec.tsx
+++ b/static/app/views/organizationStats/index.spec.tsx
@@ -97,7 +97,7 @@ describe('OrganizationStats', () => {
     // Render the cards
     expect(screen.getAllByText('Total')[0]).toBeInTheDocument();
     // Total from cards and project table should match
-    expect(screen.getAllByText('67')).toHaveLength(2);
+    await waitFor(() => expect(screen.getAllByText('67')).toHaveLength(2));
 
     expect(screen.getAllByText('Accepted')[0]).toBeInTheDocument();
     // Total from cards and project table should match
@@ -336,7 +336,7 @@ describe('OrganizationStats', () => {
     });
 
     expect(await screen.findByTestId('usage-stats-chart')).toBeInTheDocument();
-    await userEvent.click(screen.getByTestId('proj-1'));
+    await userEvent.click(await screen.findByTestId('proj-1'));
     expect(screen.queryByText('My Projects')).not.toBeInTheDocument();
     expect(screen.getAllByText('proj-1')).toHaveLength(2);
   });
diff --git a/static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx b/static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx
index 014d37a81dba02..dd4118934cdb59 100644
--- a/static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx
+++ b/static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx
@@ -182,16 +182,13 @@ export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults
   // used to query a demo transaction event from the backend.
   const mode = decodeScalar(normalizedParams.demo) ? 'demo' : undefined;
 
-  const query = useQuery<
-    {
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
+  const query = useQuery({
+    queryKey: ['traceData', replayTraces.map(trace => trace.traceSlug)],
+    queryFn: async (): Promise<{
       apiErrors: Error[];
       meta: TraceMeta | EAPTraceMeta;
-    },
-    Error
-  >({
-    // eslint-disable-next-line @tanstack/query/exhaustive-deps
-    queryKey: ['traceData', replayTraces.map(trace => trace.traceSlug)],
-    queryFn: async () => {
+    }> => {
       const result = await fetchTraceMetaInBatches(
         isEAP ? 'eap' : 'non-eap',
         api,
diff --git a/static/app/views/preprod/buildDetails/buildDetails.spec.tsx b/static/app/views/preprod/buildDetails/buildDetails.spec.tsx
index bdcda024fdf4bb..0db7b7129e0b6a 100644
--- a/static/app/views/preprod/buildDetails/buildDetails.spec.tsx
+++ b/static/app/views/preprod/buildDetails/buildDetails.spec.tsx
@@ -348,7 +348,7 @@ describe('BuildDetails', () => {
     await waitFor(() => expect(buildDetailsMock).toHaveBeenCalledTimes(2));
 
     // Second call returns PROCESSING state - shows processing message
-    expect(screen.getByText('Running size analysis')).toBeInTheDocument();
+    expect(await screen.findByText('Running size analysis')).toBeInTheDocument();
 
     // Size analysis should not be refetched since we're still processing
     expect(appSizeMock).toHaveBeenCalledTimes(1);
diff --git a/static/app/views/projectDetail/projectDetail.spec.tsx b/static/app/views/projectDetail/projectDetail.spec.tsx
index 919d9111519cd5..6734ec757aa227 100644
--- a/static/app/views/projectDetail/projectDetail.spec.tsx
+++ b/static/app/views/projectDetail/projectDetail.spec.tsx
@@ -71,6 +71,7 @@ describe('ProjectDetail', () => {
 
   it('Render an error if project not found', async () => {
     ProjectsStore.loadInitialData([{...project, slug: 'different-slug'}]);
+    setupMockResponses();
 
     render(, {
       organization,
diff --git a/static/app/views/releases/drawer/releasesDrawerDetails.spec.tsx b/static/app/views/releases/drawer/releasesDrawerDetails.spec.tsx
index 493343dfc4655e..748ae238d2402a 100644
--- a/static/app/views/releases/drawer/releasesDrawerDetails.spec.tsx
+++ b/static/app/views/releases/drawer/releasesDrawerDetails.spec.tsx
@@ -178,7 +178,7 @@ describe('ReleasesDrawerDetails', () => {
     expect(screen.getByText('File Changes')).toBeInTheDocument();
 
     // assert that the  component goes to the correct url
-    const link = screen.getByTestId('select-project-1');
+    const link = await screen.findByTestId('select-project-1');
     expect(link).toHaveAttribute(
       'href',
       expect.stringContaining('/?rdReleaseProjectId=1')
diff --git a/static/app/views/releases/list/index.spec.tsx b/static/app/views/releases/list/index.spec.tsx
index 42d9e7204f5c66..621f38cf59c2ff 100644
--- a/static/app/views/releases/list/index.spec.tsx
+++ b/static/app/views/releases/list/index.spec.tsx
@@ -141,7 +141,6 @@ describe('ReleasesList', () => {
     });
     PageFiltersStore.updateProjects([Number(projectWithoutReleases.id)], null);
     render(, {organization});
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(await screen.findByText('Set up Releases')).toBeInTheDocument();
     expect(screen.queryByTestId('release-panel')).not.toBeInTheDocument();
   });
@@ -158,7 +157,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
       await screen.findByText("There are no releases that match: 'abc'.")
     ).toBeInTheDocument();
@@ -179,7 +177,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
       await screen.findByText('There are no releases with data in the last 7 days.')
     ).toBeInTheDocument();
@@ -200,7 +197,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
       await screen.findByText(
         'There are no releases with active user data (users in the last 24 hours).'
@@ -220,7 +216,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
       await screen.findByText(
         'There are no releases with active session data (sessions in the last 24 hours).'
@@ -237,7 +232,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     expect(
       await screen.findByText('There are no releases with semantic versioning.')
     ).toBeInTheDocument();
@@ -282,8 +276,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
     const input = await screen.findByDisplayValue('derp');
     expect(input).toBeInTheDocument();
 
@@ -310,18 +302,18 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    expect(endpointMock).toHaveBeenCalledWith(
-      `/organizations/${organization.slug}/releases/`,
-      expect.objectContaining({
-        query: expect.objectContaining({
-          sort: ReleasesSortOption.SESSIONS,
-        }),
-      })
-    );
+    await waitFor(() => {
+      expect(endpointMock).toHaveBeenCalledWith(
+        `/organizations/${organization.slug}/releases/`,
+        expect.objectContaining({
+          query: expect.objectContaining({
+            sort: ReleasesSortOption.SESSIONS,
+          }),
+        })
+      );
+    });
 
-    await userEvent.click(screen.getByText('Sort By'));
+    await userEvent.click(await screen.findByText('Sort By'));
 
     const dateCreatedOption = screen.getByText('Date Created');
     expect(dateCreatedOption).toBeInTheDocument();
@@ -378,14 +370,6 @@ describe('ReleasesList', () => {
         },
       },
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-    expect(endpointMock).toHaveBeenCalledWith(
-      `/organizations/${organization.slug}/releases/`,
-      expect.objectContaining({
-        query: expect.objectContaining({status: ReleasesStatusOption.ARCHIVED}),
-      })
-    );
-
     expect(
       await screen.findByText('These releases have been archived.')
     ).toBeInTheDocument();
diff --git a/static/app/views/settings/account/accountEmails.spec.tsx b/static/app/views/settings/account/accountEmails.spec.tsx
index 7367244c3d975f..2eb2e385bdb52f 100644
--- a/static/app/views/settings/account/accountEmails.spec.tsx
+++ b/static/app/views/settings/account/accountEmails.spec.tsx
@@ -132,6 +132,7 @@ describe('AccountEmails', () => {
     });
 
     const textbox = await screen.findByRole('textbox');
+    await screen.findAllByLabelText('Remove email');
     expect(screen.getAllByLabelText('Remove email')).toHaveLength(
       AccountEmailsFixture().filter(email => !email.isPrimary).length
     );
diff --git a/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx b/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx
index 61bf97cf419356..859f5564dc087b 100644
--- a/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx
+++ b/static/app/views/settings/account/accountSecurity/accountSecurityWrapper.tsx
@@ -20,10 +20,9 @@ export default function AccountSecurityWrapper() {
   const api = useApi();
   const {authId} = useParams<{authId?: string}>();
 
-  const orgRequest = useQuery({
-    // eslint-disable-next-line @tanstack/query/exhaustive-deps
+  const orgRequest = useQuery({
     queryKey: ['organizations'],
-    queryFn: () => fetchOrganizations(api),
+    queryFn: (): Promise => fetchOrganizations(api),
     staleTime: 0,
   });
   const {refetch: refetchOrganizations} = orgRequest;
diff --git a/static/app/views/settings/account/apiTokenDetails.spec.tsx b/static/app/views/settings/account/apiTokenDetails.spec.tsx
index 9500a6e383993e..731e027416de1a 100644
--- a/static/app/views/settings/account/apiTokenDetails.spec.tsx
+++ b/static/app/views/settings/account/apiTokenDetails.spec.tsx
@@ -34,7 +34,7 @@ describe('ApiNewToken', () => {
       url: `/api-tokens/1/`,
     });
 
-    await userEvent.type(screen.getByRole('textbox', {name: /name/i}), ' new');
+    await userEvent.type(await screen.findByRole('textbox', {name: /name/i}), ' new');
 
     await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
 
@@ -79,7 +79,7 @@ describe('ApiNewToken', () => {
       url: `/api-tokens/1/`,
     });
 
-    await userEvent.clear(screen.getByRole('textbox', {name: /name/i}));
+    await userEvent.clear(await screen.findByRole('textbox', {name: /name/i}));
 
     await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
 
@@ -126,7 +126,7 @@ describe('ApiNewToken', () => {
     });
 
     await userEvent.type(
-      screen.getByRole('textbox', {name: /name/i}),
+      await screen.findByRole('textbox', {name: /name/i}),
       'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in'
     );
 
diff --git a/static/app/views/settings/dynamicSampling/utils/useSamplingProjectRates.tsx b/static/app/views/settings/dynamicSampling/utils/useSamplingProjectRates.tsx
index a7550c9fb9abf9..a6411a0ddfc352 100644
--- a/static/app/views/settings/dynamicSampling/utils/useSamplingProjectRates.tsx
+++ b/static/app/views/settings/dynamicSampling/utils/useSamplingProjectRates.tsx
@@ -63,10 +63,10 @@ const fetchAllSamplingRates = async (
 export function useGetSamplingProjectRates() {
   const api = useApi();
   const organization = useOrganization();
-  return useQuery({
-    // eslint-disable-next-line @tanstack/query/exhaustive-deps
+  return useQuery({
     queryKey: getQueryKey(organization),
-    queryFn: () => fetchAllSamplingRates(api, organization),
+    queryFn: (): Promise =>
+      fetchAllSamplingRates(api, organization),
     staleTime: 0,
   });
 }
diff --git a/static/app/views/settings/dynamicSampling/utils/useUpdateOrganization.tsx b/static/app/views/settings/dynamicSampling/utils/useUpdateOrganization.tsx
index 4f79450a0d5b59..b853a973efe938 100644
--- a/static/app/views/settings/dynamicSampling/utils/useUpdateOrganization.tsx
+++ b/static/app/views/settings/dynamicSampling/utils/useUpdateOrganization.tsx
@@ -25,8 +25,8 @@ export function useUpdateOrganization(
         data: variables,
       });
     },
-    onSuccess: (newOrg, variables, context) => {
-      options?.onSuccess?.(newOrg, variables, context);
+    onSuccess: (newOrg, variables, onMutateResult, context) => {
+      options?.onSuccess?.(newOrg, variables, onMutateResult, context);
       OrganizationStore.onUpdate(newOrg);
     },
   });
diff --git a/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx
index cabb34a1041a0d..c0d075ed17ed9a 100644
--- a/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx
+++ b/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx
@@ -30,9 +30,9 @@ describe('PermissionSelection', () => {
     );
   }
 
-  it('renders a row for each resource', () => {
+  it('renders a row for each resource', async () => {
     renderForm();
-    expect(screen.getByRole('textbox', {name: 'Project'})).toBeInTheDocument();
+    expect(await screen.findByRole('textbox', {name: 'Project'})).toBeInTheDocument();
     expect(screen.getByRole('textbox', {name: 'Team'})).toBeInTheDocument();
     expect(screen.getByRole('textbox', {name: 'Release'})).toBeInTheDocument();
     expect(screen.getByRole('textbox', {name: 'Issue & Event'})).toBeInTheDocument();
@@ -42,6 +42,7 @@ describe('PermissionSelection', () => {
 
   it('lists human readable permissions', async () => {
     renderForm();
+    await screen.findByRole('textbox', {name: 'Project'});
     const expectOptions = async (name: string, options: string[]) => {
       for (const option of options) {
         await selectEvent.select(screen.getByRole('textbox', {name}), option);
@@ -58,6 +59,7 @@ describe('PermissionSelection', () => {
 
   it('stores the permissions the User has selected', async () => {
     renderForm();
+    await screen.findByRole('textbox', {name: 'Project'});
     const selectByValue = (name: string, value: string) =>
       selectEvent.select(screen.getByRole('textbox', {name}), value);
 
diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx
index 30f2a81cd56679..ea5fcfe19ada79 100644
--- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx
+++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx
@@ -186,7 +186,6 @@ describe('Sentry Application Dashboard', () => {
           route: '/settings/:orgId/developer-settings/:appSlug/dashboard/',
         },
       });
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
       // The mock response has 1 request
       expect(await screen.findByTestId('request-item')).toBeInTheDocument();
       const requestLog = within(screen.getByTestId('request-item'));
diff --git a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx
index 5d2bb21dac9383..0db3df891310b1 100644
--- a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.tsx
@@ -81,9 +81,9 @@ describe('IntegrationCodeMappings', () => {
 
   it('shows the paths', async () => {
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
 
-    for (const repo of repos) {
+    expect(await screen.findByText(repos[0]!.name)).toBeInTheDocument();
+    for (const repo of repos.slice(1)) {
       expect(screen.getByText(repo.name)).toBeInTheDocument();
     }
   });
@@ -106,10 +106,9 @@ describe('IntegrationCodeMappings', () => {
       }),
     });
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     const {waitForModalToHide} = renderGlobalModal();
 
-    await userEvent.click(screen.getByRole('button', {name: 'Add Code Mapping'}));
+    await userEvent.click(await screen.findByRole('button', {name: 'Add Code Mapping'}));
     expect(screen.getByRole('dialog')).toBeInTheDocument();
 
     await selectEvent.select(
@@ -165,10 +164,9 @@ describe('IntegrationCodeMappings', () => {
       }),
     });
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     const {waitForModalToHide} = renderGlobalModal();
 
-    await userEvent.click(screen.getAllByRole('button', {name: 'edit'})[0]!);
+    await userEvent.click((await screen.findAllByRole('button', {name: 'edit'}))[0]!);
     await userEvent.clear(screen.getByRole('textbox', {name: 'Stack Trace Root'}));
     await userEvent.type(
       screen.getByRole('textbox', {name: 'Stack Trace Root'}),
@@ -206,10 +204,9 @@ describe('IntegrationCodeMappings', () => {
       },
     });
     render();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
     renderGlobalModal();
 
-    await userEvent.click(screen.getByRole('button', {name: 'Add Code Mapping'}));
+    await userEvent.click(await screen.findByRole('button', {name: 'Add Code Mapping'}));
     expect(screen.getByRole('textbox', {name: 'Branch'})).toHaveValue('main');
 
     await selectEvent.select(screen.getByText('Choose repo'), repos[1]!.name);
@@ -227,10 +224,9 @@ describe('IntegrationCodeMappings', () => {
 
     render();
     renderGlobalModal();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
 
     // Should show both path configs initially
-    expect(screen.getByText(pathConfig1.repoName)).toBeInTheDocument();
+    expect(await screen.findByText(pathConfig1.repoName)).toBeInTheDocument();
     expect(screen.getByText(pathConfig2.repoName)).toBeInTheDocument();
 
     // Override mock before refetch happens after delete
diff --git a/static/app/views/settings/organizationIntegrations/integrationDetailedView.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationDetailedView.spec.tsx
index 31f96756da04e5..6a55e6cc390f94 100644
--- a/static/app/views/settings/organizationIntegrations/integrationDetailedView.spec.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationDetailedView.spec.tsx
@@ -118,8 +118,7 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('bitbucket'),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-    expect(screen.getByText('Bitbucket')).toBeInTheDocument();
+    expect(await screen.findByText('Bitbucket')).toBeInTheDocument();
     expect(screen.getByText('Installed')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Add integration'})).toBeEnabled();
   });
@@ -129,9 +128,7 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('bitbucket', {tab: 'configurations'}),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    expect(screen.getByTestId('integration-name')).toHaveTextContent(
+    expect(await screen.findByTestId('integration-name')).toHaveTextContent(
       '{fb715533-bbd7-4666-aa57-01dc93dd9cc0}'
     );
     expect(screen.getByRole('button', {name: 'Configure'})).toBeEnabled();
@@ -149,9 +146,7 @@ describe('IntegrationDetailedView', () => {
       },
       organization: lowerAccessOrg,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    expect(screen.getByRole('button', {name: 'Configure'})).toHaveAttribute(
+    expect(await screen.findByRole('button', {name: 'Configure'})).toHaveAttribute(
       'aria-disabled',
       'true'
     );
@@ -209,9 +204,7 @@ describe('IntegrationDetailedView', () => {
       },
       organization: lowerAccessOrganization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    expect(screen.getByRole('button', {name: 'Configure'})).toBeEnabled();
+    expect(await screen.findByRole('button', {name: 'Configure'})).toBeEnabled();
   });
 
   it('shows features tab for github only', async () => {
@@ -219,8 +212,7 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('github'),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-    expect(screen.getByText('features')).toBeInTheDocument();
+    expect(await screen.findByText('features')).toBeInTheDocument();
   });
 
   it('cannot enable PR bot without GitHub integration', async () => {
@@ -233,9 +225,7 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('github'),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    await userEvent.click(screen.getByText('features'));
+    await userEvent.click(await screen.findByText('features'));
 
     expect(
       screen.getByRole('checkbox', {name: /Enable Comments on Suspect Pull Requests/})
@@ -247,9 +237,7 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('github'),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-    await userEvent.click(screen.getByText('features'));
+    await userEvent.click(await screen.findByText('features'));
 
     const mock = MockApiClient.addMockResponse({
       url: ENDPOINT,
@@ -288,9 +276,8 @@ describe('IntegrationDetailedView', () => {
       initialRouterConfig: createRouterConfig('gitlab'),
       organization,
     });
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
 
-    await userEvent.click(screen.getByText('features'));
+    await userEvent.click(await screen.findByText('features'));
 
     const mock = MockApiClient.addMockResponse({
       url: ENDPOINT,
diff --git a/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx
index 85c7e50407796c..741bb5b6b9882c 100644
--- a/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx
@@ -53,8 +53,6 @@ describe('IntegrationListDirectory', () => {
       render(, {
         organization,
       });
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
       expect(await screen.findByRole('textbox', {name: 'Filter'})).toBeInTheDocument();
 
       [
@@ -72,8 +70,6 @@ describe('IntegrationListDirectory', () => {
       render(, {
         organization,
       });
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
       expect(await screen.findByRole('textbox', {name: 'Filter'})).toBeInTheDocument();
       expect(screen.queryByText('GitHub (Legacy)')).not.toBeInTheDocument();
     });
@@ -82,16 +78,12 @@ describe('IntegrationListDirectory', () => {
       render(, {
         organization,
       });
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
       expect(await screen.findByText('PagerDuty (Legacy)')).toBeInTheDocument();
     });
 
     it('shows integrations that match the search query', async () => {
       render(, {organization});
-      expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
-
-      expect(screen.getByText('PagerDuty (Legacy)')).toBeInTheDocument();
+      expect(await screen.findByText('PagerDuty (Legacy)')).toBeInTheDocument();
 
       await userEvent.type(screen.getByRole('textbox', {name: 'Filter'}), 'it');
       await userEvent.keyboard('{enter}');
diff --git a/static/app/views/settings/organizationIntegrations/integrationReposAddRepository.tsx b/static/app/views/settings/organizationIntegrations/integrationReposAddRepository.tsx
index 27c165e10383e5..4bfbebfee74d2f 100644
--- a/static/app/views/settings/organizationIntegrations/integrationReposAddRepository.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationReposAddRepository.tsx
@@ -44,6 +44,7 @@ export function IntegrationReposAddRepository({
   const [search, setSearch] = useState();
   const debouncedSearch = useDebouncedValue(search, 200);
 
+  // eslint-disable-next-line @tanstack/query/exhaustive-deps
   const query = useQuery({
     queryKey: [
       getApiUrl(
diff --git a/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx b/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx
index 73260e6b2fb24d..3b9f209795e661 100644
--- a/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx
+++ b/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx
@@ -24,11 +24,7 @@ describe('SentryAppDetailedView', () => {
     jest.clearAllMocks();
   });
 
-  async function renderSentryAppDetailedView({
-    integrationSlug,
-  }: {
-    integrationSlug: string;
-  }) {
+  function renderSentryAppDetailedView({integrationSlug}: {integrationSlug: string}) {
     render(, {
       initialRouterConfig: {
         route: '/settings/:orgId/integrations/:integrationSlug/',
@@ -39,7 +35,6 @@ describe('SentryAppDetailedView', () => {
       organization,
     });
     renderGlobalModal();
-    expect(await screen.findByTestId('loading-indicator')).not.toBeInTheDocument();
   }
 
   describe('Published Sentry App', () => {
@@ -113,7 +108,7 @@ describe('SentryAppDetailedView', () => {
     });
 
     it('renders a published sentry app', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'clickup'});
+      renderSentryAppDetailedView({integrationSlug: 'clickup'});
 
       expect(sentryAppInteractionRequest).toHaveBeenCalledWith(
         `/sentry-apps/clickup/interaction/`,
@@ -126,7 +121,7 @@ describe('SentryAppDetailedView', () => {
       );
 
       // Shows the Integration name and install status
-      expect(screen.getByText('ClickUp')).toBeInTheDocument();
+      expect(await screen.findByText('ClickUp')).toBeInTheDocument();
       expect(screen.getByText('Not Installed')).toBeInTheDocument();
 
       // Shows the Accept & Install button
@@ -134,9 +129,11 @@ describe('SentryAppDetailedView', () => {
     });
 
     it('installs and uninstalls', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'clickup'});
+      renderSentryAppDetailedView({integrationSlug: 'clickup'});
 
-      await userEvent.click(screen.getByRole('button', {name: 'Accept & Install'}));
+      await userEvent.click(
+        await screen.findByRole('button', {name: 'Accept & Install'})
+      );
       expect(createRequest).toHaveBeenCalledTimes(1);
 
       expect(await screen.findByRole('button', {name: 'Uninstall'})).toBeInTheDocument();
@@ -209,11 +206,13 @@ describe('SentryAppDetailedView', () => {
     });
 
     it('should get redirected to Developer Settings', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'my-headband-washer-289499'});
+      renderSentryAppDetailedView({integrationSlug: 'my-headband-washer-289499'});
 
-      expect(mockNavigate).toHaveBeenLastCalledWith(
-        `/settings/${organization.slug}/developer-settings/my-headband-washer-289499/`
-      );
+      await waitFor(() => {
+        expect(mockNavigate).toHaveBeenLastCalledWith(
+          `/settings/${organization.slug}/developer-settings/my-headband-washer-289499/`
+        );
+      });
     });
   });
 
@@ -285,14 +284,16 @@ describe('SentryAppDetailedView', () => {
       });
     });
     it('shows the Integration name and install status', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'la-croix-monitor'});
-      expect(screen.getByText('La Croix Monitor')).toBeInTheDocument();
+      renderSentryAppDetailedView({integrationSlug: 'la-croix-monitor'});
+      expect(await screen.findByText('La Croix Monitor')).toBeInTheDocument();
       expect(screen.getByText('Not Installed')).toBeInTheDocument();
     });
 
     it('installs and uninstalls', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'la-croix-monitor'});
-      await userEvent.click(screen.getByRole('button', {name: 'Accept & Install'}));
+      renderSentryAppDetailedView({integrationSlug: 'la-croix-monitor'});
+      await userEvent.click(
+        await screen.findByRole('button', {name: 'Accept & Install'})
+      );
       expect(createRequest).toHaveBeenCalledTimes(1);
     });
   });
@@ -357,17 +358,19 @@ describe('SentryAppDetailedView', () => {
       });
     });
     it('shows the Integration name and install status', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'go-to-google'});
-      expect(screen.getByText('Go to Google')).toBeInTheDocument();
+      renderSentryAppDetailedView({integrationSlug: 'go-to-google'});
+      expect(await screen.findByText('Go to Google')).toBeInTheDocument();
       expect(screen.getByText('Not Installed')).toBeInTheDocument();
 
       // Shows the Accept & Install button
       expect(screen.getByRole('button', {name: 'Accept & Install'})).toBeEnabled();
     });
     it('onClick: redirects url', async () => {
-      await renderSentryAppDetailedView({integrationSlug: 'go-to-google'});
+      renderSentryAppDetailedView({integrationSlug: 'go-to-google'});
 
-      await userEvent.click(screen.getByRole('button', {name: 'Accept & Install'}));
+      await userEvent.click(
+        await screen.findByRole('button', {name: 'Accept & Install'})
+      );
 
       expect(createRequest).toHaveBeenCalled();
       await waitFor(() => {
diff --git a/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx b/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx
index 31d2b95987eebb..6fcb77c0553a9b 100644
--- a/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx
+++ b/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx
@@ -682,8 +682,7 @@ describe('OrganizationMembersList', () => {
       });
       renderGlobalModal();
 
-      expect(await screen.findByText('Members')).toBeInTheDocument();
-      expect(screen.getByText(member.name)).toBeInTheDocument();
+      expect(await screen.findByText(member.name)).toBeInTheDocument();
     });
 
     it('renders only current user in demo mode', async () => {
@@ -695,7 +694,7 @@ describe('OrganizationMembersList', () => {
       renderGlobalModal();
 
       expect(await screen.findByText('Members')).toBeInTheDocument();
-      expect(screen.getByText(currentUser.name)).toBeInTheDocument();
+      expect(await screen.findByText(currentUser.name)).toBeInTheDocument();
       expect(screen.queryByText(member.name)).not.toBeInTheDocument();
 
       (isDemoModeActive as jest.Mock).mockReset();
@@ -729,7 +728,7 @@ describe('OrganizationMembersList', () => {
       expect(await screen.findByText('Members')).toBeInTheDocument();
       expect(searchQuery).toHaveBeenCalled();
       expect(ownerQuery).toHaveBeenCalled();
-      const leaveButton = screen.getByRole('button', {name: 'Leave'});
+      const leaveButton = await screen.findByRole('button', {name: 'Leave'});
       expect(leaveButton).toBeEnabled();
     });
   });
@@ -778,7 +777,7 @@ describe('OrganizationMembersList', () => {
 
       // Current user should be able to leave since the deleted owner doesn't count
       // (they have null user, so filtered out of ownership check)
-      const leaveButton = screen.getByRole('button', {name: 'Leave'});
+      const leaveButton = await screen.findByRole('button', {name: 'Leave'});
       expect(leaveButton).toBeDisabled(); // Disabled because they're the only valid owner
     });
   });
diff --git a/static/app/views/settings/organizationTeams/teamMembers.spec.tsx b/static/app/views/settings/organizationTeams/teamMembers.spec.tsx
index a00d90b0dda178..096b22103a6b54 100644
--- a/static/app/views/settings/organizationTeams/teamMembers.spec.tsx
+++ b/static/app/views/settings/organizationTeams/teamMembers.spec.tsx
@@ -236,7 +236,7 @@ describe('TeamMembers', () => {
       organization,
     });
 
-    await screen.findAllByRole('button', {name: 'Add Member'});
+    await screen.findAllByRole('button', {name: 'Remove'});
 
     expect(deleteMock).not.toHaveBeenCalled();
     await userEvent.click(screen.getAllByRole('button', {name: 'Remove'})[0]!);
@@ -267,7 +267,7 @@ describe('TeamMembers', () => {
       organization: organizationMember,
     });
 
-    await screen.findAllByRole('button', {name: 'Add Member'});
+    await screen.findAllByTestId('letter_avatar-avatar');
 
     expect(deleteMock).not.toHaveBeenCalled();
 
diff --git a/static/app/views/settings/project/projectKeys/list/index.tsx b/static/app/views/settings/project/projectKeys/list/index.tsx
index 31a7676535c893..23056d8a035b9f 100644
--- a/static/app/views/settings/project/projectKeys/list/index.tsx
+++ b/static/app/views/settings/project/projectKeys/list/index.tsx
@@ -101,7 +101,7 @@ export default function ProjectKeys() {
         }
       );
     },
-    onMutate: ({data}: {data: ProjectKey}) => {
+    onMutate: ({data}) => {
       addLoadingMessage(t('Saving changes\u2026'));
       setKeyListState(
         keyList.map(key => {
diff --git a/static/app/views/settings/projectGeneralSettings/index.tsx b/static/app/views/settings/projectGeneralSettings/index.tsx
index 764d20228d5a9f..be9af290f0a17b 100644
--- a/static/app/views/settings/projectGeneralSettings/index.tsx
+++ b/static/app/views/settings/projectGeneralSettings/index.tsx
@@ -365,6 +365,7 @@ export function ProjectGeneralSettings({project, onChangeSlug}: Props) {
           fields={[
             fields.allowedDomains,
             fields.scrapeJavaScript,
+            fields.scmSourceContextEnabled,
             fields.securityToken,
             fields.securityTokenHeader,
             fields.verifySSL,
diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx
index c298dcbf911e98..102a5e2901f725 100644
--- a/static/app/views/settings/projectSeer/index.spec.tsx
+++ b/static/app/views/settings/projectSeer/index.spec.tsx
@@ -128,6 +128,28 @@ describe('ProjectSeer', () => {
       await within(modal).findByRole('button', {name: /getsentry\/seer/})
     );
 
+    // Override GET mock to return updated data before mutation triggers refetch
+    MockApiClient.addMockResponse({
+      url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
+      method: 'GET',
+      body: {
+        code_mapping_repos: [
+          {
+            provider: 'github',
+            owner: 'getsentry',
+            name: 'sentry',
+            external_id: '101',
+          },
+          {
+            provider: 'github',
+            owner: 'getsentry',
+            name: 'seer',
+            external_id: '102',
+          },
+        ],
+      },
+    });
+
     // Save changes in the modal
     await userEvent.click(within(modal).getByRole('button', {name: 'Add 1 Repository'}));
 
@@ -244,6 +266,16 @@ describe('ProjectSeer', () => {
 
     // Open the row and click remove
     await userEvent.click(repoItem);
+
+    // Override GET mock to return updated data before mutation triggers refetch
+    MockApiClient.addMockResponse({
+      url: `/projects/${organization.slug}/${project.slug}/seer/preferences/`,
+      method: 'GET',
+      body: {
+        code_mapping_repos: [],
+      },
+    });
+
     await userEvent.click(screen.getByRole('button', {name: 'Remove Repository'}));
 
     await userEvent.click(await screen.findByRole('button', {name: 'Confirm'}));
diff --git a/static/gsAdmin/components/addToStartupProgramAction.spec.tsx b/static/gsAdmin/components/addToStartupProgramAction.spec.tsx
index df0908a33b6096..04b9017d9b2571 100644
--- a/static/gsAdmin/components/addToStartupProgramAction.spec.tsx
+++ b/static/gsAdmin/components/addToStartupProgramAction.spec.tsx
@@ -209,7 +209,7 @@ describe('AddToStartupProgramAction', () => {
       url: `/_admin/customers/${organization.slug}/balance-changes/`,
       method: 'POST',
       body: {},
-      asyncDelay: 100,
+      asyncDelay: 500,
     });
 
     triggerAddToStartupProgramModal(modalProps);
diff --git a/static/gsAdmin/views/userDetails.spec.tsx b/static/gsAdmin/views/userDetails.spec.tsx
index e4d4131c983e4d..959eb6cc3cf038 100644
--- a/static/gsAdmin/views/userDetails.spec.tsx
+++ b/static/gsAdmin/views/userDetails.spec.tsx
@@ -1,3 +1,4 @@
+import {notifyManager} from '@tanstack/react-query';
 import {UserFixture} from 'sentry-fixture/user';
 
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
@@ -12,6 +13,9 @@ describe('User Details', () => {
   });
 
   beforeEach(() => {
+    // Use synchronous scheduling to avoid React 19 act() timing issues
+    // with TanStack Query's default setTimeout-based batching
+    notifyManager.setScheduler(cb => cb());
     MockApiClient.clearMockResponses();
     MockApiClient.addMockResponse({
       url: `/users/${mockUser.id}/`,
@@ -40,6 +44,11 @@ describe('User Details', () => {
     });
   });
 
+  afterEach(() => {
+    // Restore default scheduler
+    notifyManager.setScheduler(setTimeout);
+  });
+
   describe('page rendering', () => {
     it('renders correct sections', async () => {
       render(, {
diff --git a/static/gsApp/views/amCheckout/components/cart.spec.tsx b/static/gsApp/views/amCheckout/components/cart.spec.tsx
index 39b4be37ed41d4..e211dd783ef9fa 100644
--- a/static/gsApp/views/amCheckout/components/cart.spec.tsx
+++ b/static/gsApp/views/amCheckout/components/cart.spec.tsx
@@ -114,7 +114,7 @@ describe('Cart', () => {
     const cart = await screen.findByTestId('cart');
     expect(cart).toHaveTextContent('Business Plan');
     expect(cart).toHaveTextContent('Pay-as-you-go spend limitup to $300/mo');
-    expect(cart).toHaveTextContent('Plan Total$89/mo');
+    await waitFor(() => expect(cart).toHaveTextContent('Plan Total$89/mo'));
     expect(cart).toHaveTextContent('Default Amount');
   });
 
diff --git a/static/gsApp/views/amCheckout/index.spec.tsx b/static/gsApp/views/amCheckout/index.spec.tsx
index bc5aea584a5d0d..5011aa363d60ec 100644
--- a/static/gsApp/views/amCheckout/index.spec.tsx
+++ b/static/gsApp/views/amCheckout/index.spec.tsx
@@ -13,7 +13,7 @@ import AMCheckout from 'getsentry/views/amCheckout';
 import {getCheckoutAPIData} from 'getsentry/views/amCheckout/utils';
 import {hasOnDemandBudgetsFeature} from 'getsentry/views/spendLimits/utils';
 
-function assertCheckoutSteps({
+async function assertCheckoutSteps({
   tier,
   hasBillingCycleStep = true,
   hasBillingInfoStep = true,
@@ -22,7 +22,7 @@ function assertCheckoutSteps({
   hasBillingCycleStep?: boolean;
   hasBillingInfoStep?: boolean;
 }) {
-  expect(screen.getByTestId('checkout-steps')).toBeInTheDocument();
+  expect(await screen.findByTestId('checkout-steps')).toBeInTheDocument();
   [
     'Select a plan',
     [PlanTier.AM1, PlanTier.AM2].includes(tier)
@@ -102,7 +102,7 @@ describe('Legacy Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM2});
+    await assertCheckoutSteps({tier: PlanTier.AM2});
   });
 
   it('renders for AM1', async () => {
@@ -126,7 +126,7 @@ describe('Legacy Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM1});
+    await assertCheckoutSteps({tier: PlanTier.AM1});
   });
 
   it('renders standard checkout for business bundle', async () => {
@@ -166,7 +166,7 @@ describe('Legacy Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM2});
+    await assertCheckoutSteps({tier: PlanTier.AM2});
 
     // Verify that Business is preselected
     expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
@@ -231,7 +231,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM3});
+    await assertCheckoutSteps({tier: PlanTier.AM3});
   });
 
   it('renders for new customers (default free plan)', async () => {
@@ -261,7 +261,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM3});
+    await assertCheckoutSteps({tier: PlanTier.AM3});
   });
 
   it('renders for customers migrating from partner billing', async () => {
@@ -306,7 +306,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM3});
+    await assertCheckoutSteps({tier: PlanTier.AM3});
 
     expect(
       screen.getByText(
@@ -359,7 +359,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM3, hasBillingInfoStep: false});
+    await assertCheckoutSteps({tier: PlanTier.AM3, hasBillingInfoStep: false});
     expect(
       screen.queryByText(
         'Your promotional plan with BAR ends on ' + contractPeriodEnd.format('ll') + '.'
@@ -408,7 +408,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({tier: PlanTier.AM3, hasBillingInfoStep: false});
+    await assertCheckoutSteps({tier: PlanTier.AM3, hasBillingInfoStep: false});
     expect(
       screen.getByText('Billing handled externally through BAR')
     ).toBeInTheDocument();
@@ -456,7 +456,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    assertCheckoutSteps({
+    await assertCheckoutSteps({
       tier: PlanTier.AM3,
       hasBillingInfoStep: false,
       hasBillingCycleStep: false,
@@ -491,7 +491,7 @@ describe('Default Tier Checkout', () => {
       );
     });
     expect(hasOnDemandBudgetsFeature(organization, sub)).toBe(false);
-    assertCheckoutSteps({tier: PlanTier.AM3});
+    await assertCheckoutSteps({tier: PlanTier.AM3});
     expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
   });
 
@@ -547,7 +547,9 @@ describe('Default Tier Checkout', () => {
     });
 
     expect(
-      screen.getByRole('textbox', {name: 'Custom shared spending limit (in dollars)'})
+      await screen.findByRole('textbox', {
+        name: 'Custom shared spending limit (in dollars)',
+      })
     ).toHaveValue('20');
     expect(screen.getByTestId('errors-volume-item')).toHaveTextContent('100K');
     expect(screen.getByTestId('attachments-volume-item')).toHaveTextContent('25 GB');
@@ -604,7 +606,9 @@ describe('Default Tier Checkout', () => {
     });
 
     expect(
-      screen.getByRole('textbox', {name: 'Custom shared spending limit (in dollars)'})
+      await screen.findByRole('textbox', {
+        name: 'Custom shared spending limit (in dollars)',
+      })
     ).toHaveValue('20');
     expect(screen.getByTestId('errors-volume-item')).toHaveTextContent('100K');
     expect(screen.getByTestId('attachments-volume-item')).toHaveTextContent('25 GB');
@@ -680,7 +684,7 @@ describe('Default Tier Checkout', () => {
     });
 
     await userEvent.click(
-      screen.getByRole('button', {name: 'Show reserved volume sliders'})
+      await screen.findByRole('button', {name: 'Show reserved volume sliders'})
     );
     // Check that missing 'Errors' category defaults to 50,000 errors
     expect(screen.getByTestId('errors-volume-item')).toHaveTextContent('50K');
@@ -747,7 +751,7 @@ describe('Default Tier Checkout', () => {
       );
     });
 
-    expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked();
+    expect(await screen.findByRole('radio', {name: 'Business'})).toBeChecked();
 
     await userEvent.click(screen.getByRole('radio', {name: 'Team'}));
     expect(screen.getByRole('radio', {name: 'Team'})).toBeChecked();
@@ -827,7 +831,7 @@ describe('Default Tier Checkout', () => {
 
     // not open by default because it's a trial subscription
     await userEvent.click(
-      screen.getByRole('button', {name: 'Show reserved volume sliders'})
+      await screen.findByRole('button', {name: 'Show reserved volume sliders'})
     );
 
     // Verify that sliders show reasonable values, NOT the high trial volumes
diff --git a/static/gsApp/views/amCheckout/steps/addBillingInfo.spec.tsx b/static/gsApp/views/amCheckout/steps/addBillingInfo.spec.tsx
index bd35e2d6f638df..71a6a89564856b 100644
--- a/static/gsApp/views/amCheckout/steps/addBillingInfo.spec.tsx
+++ b/static/gsApp/views/amCheckout/steps/addBillingInfo.spec.tsx
@@ -86,7 +86,7 @@ describe('AddBillingInformation', () => {
     );
 
     expect(await screen.findByText('Edit billing information')).toBeInTheDocument();
-    expect(screen.getByText('Business address')).toBeInTheDocument();
+    expect(await screen.findByText('Business address')).toBeInTheDocument();
     expect(screen.getByText('Payment method')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Confirm'})).toBeEnabled();
     expect(
@@ -109,7 +109,7 @@ describe('AddBillingInformation', () => {
     );
 
     expect(await screen.findByText('Edit billing information')).toBeInTheDocument();
-    expect(screen.getByText('Business address')).toBeInTheDocument();
+    expect(await screen.findByText('Business address')).toBeInTheDocument();
     expect(screen.getByText('Payment method')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Confirm'})).toBeDisabled();
     expect(
@@ -139,7 +139,7 @@ describe('AddBillingInformation', () => {
 
     expect(await screen.findByText('Add billing information')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Confirm'})).toBeDisabled(); // cannot checkout without billing info
-    expect(screen.getByTestId('credit-card-panel')).toBeInTheDocument();
+    expect(await screen.findByTestId('credit-card-panel')).toBeInTheDocument();
     expect(screen.getByTestId('billing-details-panel')).toBeInTheDocument();
     const inCardPanel = within(screen.getByTestId('credit-card-panel'));
     const inBillingDetailsPanel = within(screen.getByTestId('billing-details-panel'));
diff --git a/static/gsApp/views/invoiceDetails/index.spec.tsx b/static/gsApp/views/invoiceDetails/index.spec.tsx
index 7412c6c320b69d..efeb9d1b18e67f 100644
--- a/static/gsApp/views/invoiceDetails/index.spec.tsx
+++ b/static/gsApp/views/invoiceDetails/index.spec.tsx
@@ -127,7 +127,7 @@ describe('InvoiceDetails', () => {
     await waitFor(() => expect(mockapi).toHaveBeenCalled());
 
     expect(
-      screen.getByText(
+      await screen.findByText(
         /Your subscription will automatically renew on or about the same day each year and your credit card on file will be charged the recurring subscription fees set forth above. In addition to recurring subscription fees, you may also be charged for monthly pay-as-you-go fees. You may cancel your subscription at any time /
       )
     ).toBeInTheDocument();
@@ -221,8 +221,8 @@ describe('InvoiceDetails', () => {
 
     await waitFor(() => expect(mockapiInvoice).toHaveBeenCalled());
 
-    expect(screen.getByText(/Receipt Details/)).toBeInTheDocument();
-    expect(screen.getByText(/AWAITING PAYMENT/)).toBeInTheDocument();
+    expect(await screen.findByText(/Receipt Details/)).toBeInTheDocument();
+    expect(await screen.findByText(/AWAITING PAYMENT/)).toBeInTheDocument();
     expect(screen.queryByText(/Pay Now/)).not.toBeInTheDocument();
   });
 
@@ -309,8 +309,8 @@ describe('InvoiceDetails', () => {
     await waitFor(() => expect(mockapiInvoice).toHaveBeenCalled());
     await waitFor(() => expect(mockapiPayments).toHaveBeenCalled());
 
-    expect(screen.getByText(/Receipt Details/)).toBeInTheDocument();
-    expect(screen.getAllByText(/Pay Now/)).toHaveLength(2);
+    expect(await screen.findByText(/Receipt Details/)).toBeInTheDocument();
+    await waitFor(() => expect(screen.getAllByText(/Pay Now/)).toHaveLength(2));
     expect(screen.getByText(/Pay Bill/)).toBeInTheDocument();
     expect(screen.getByTestId('modal-backdrop')).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
diff --git a/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx b/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx
index dfc42f6dd06732..20bb0dff19b959 100644
--- a/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx
+++ b/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx
@@ -62,7 +62,7 @@ describe('InvoiceDetails > Payment Form', () => {
 
     await waitFor(() => expect(mockget).toHaveBeenCalled());
     expect(screen.getByText('Pay Bill')).toBeInTheDocument();
-    expect(screen.getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
+    expect(await screen.findByRole('button', {name: 'Cancel'})).toBeInTheDocument();
     expect(screen.getByRole('button', {name: 'Pay Now'})).toBeInTheDocument();
     expect(
       screen.queryByText(
@@ -124,7 +124,7 @@ describe('InvoiceDetails > Payment Form', () => {
 
     expect(screen.getByText('Pay Bill')).toBeInTheDocument();
 
-    const button = screen.getByRole('button', {name: 'Pay Now'});
+    const button = await screen.findByRole('button', {name: 'Pay Now'});
     await userEvent.click(button);
     await waitFor(() => expect(reloadInvoice).toHaveBeenCalled());
     expect(reloadInvoice).toHaveBeenCalled();
diff --git a/static/gsApp/views/subscriptionPage/billingInformation.spec.tsx b/static/gsApp/views/subscriptionPage/billingInformation.spec.tsx
index fad40af0bc9de6..6fe633fa87f30d 100644
--- a/static/gsApp/views/subscriptionPage/billingInformation.spec.tsx
+++ b/static/gsApp/views/subscriptionPage/billingInformation.spec.tsx
@@ -218,7 +218,7 @@ describe('Subscription > BillingInformation', () => {
     render(, {organization});
 
     await screen.findByText('Billing Information');
-    expect(screen.getByText('Account balance: $100')).toBeInTheDocument();
+    expect(await screen.findByText('Account balance: $100')).toBeInTheDocument();
   });
 
   it('renders with credit if account balance < 0', async () => {
@@ -233,7 +233,7 @@ describe('Subscription > BillingInformation', () => {
     render(, {organization});
 
     await screen.findByText('Billing Information');
-    expect(screen.getByText('Account balance: $100 credit')).toBeInTheDocument();
+    expect(await screen.findByText('Account balance: $100 credit')).toBeInTheDocument();
   });
 
   it('hides account balance when it is 0', async () => {
diff --git a/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx b/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx
index 40c6c30bef1154..67279ef253936d 100644
--- a/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx
+++ b/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx
@@ -55,7 +55,7 @@ describe('Subscription > PaymentHistory', () => {
     render(, {organization});
 
     await screen.findByText('Receipts');
-    expect(screen.getByTestId('payment-list')).toBeInTheDocument();
+    expect(await screen.findByTestId('payment-list')).toBeInTheDocument();
   });
 
   it('renders no receipts found', async () => {
diff --git a/static/gsApp/views/subscriptionPage/usageLog.spec.tsx b/static/gsApp/views/subscriptionPage/usageLog.spec.tsx
index 4751950e62b7ae..97bb9253bc7de9 100644
--- a/static/gsApp/views/subscriptionPage/usageLog.spec.tsx
+++ b/static/gsApp/views/subscriptionPage/usageLog.spec.tsx
@@ -80,7 +80,7 @@ describe('Subscription Usage Log', () => {
 
     await screen.findByText(/Select Action/i);
     expect(screen.getByRole('heading', {name: /Activity Logs/i})).toBeInTheDocument();
-    expect(screen.getByText(/cancelled plan/i)).toBeInTheDocument();
+    expect(await screen.findByText(/cancelled plan/i)).toBeInTheDocument();
     expect(screen.getByText(/Sentry Staff/i)).toBeInTheDocument();
     expect(screen.getByText(/Jun/i)).toBeInTheDocument();
     await userEvent.click(screen.getByText(/Select Action/i));
@@ -97,7 +97,7 @@ describe('Subscription Usage Log', () => {
     render(, {organization});
 
     await screen.findByText(/Select Action/i);
-    expect(screen.getByText(/No entries available/i)).toBeInTheDocument();
+    expect(await screen.findByText(/No entries available/i)).toBeInTheDocument();
   });
 
   it('keeps hyphens in on-demand and PAYG', async () => {
@@ -116,7 +116,7 @@ describe('Subscription Usage Log', () => {
     render(, {organization});
 
     await screen.findByText(/Select Action/i);
-    expect(screen.getByText('On-demand Edit')).toBeInTheDocument();
+    expect(await screen.findByText('On-demand Edit')).toBeInTheDocument();
     expect(screen.getByText('Pay-as-you-go Edit')).toBeInTheDocument();
   });
 });
diff --git a/tests/sentry/models/test_dynamicsampling.py b/tests/sentry/models/test_dynamicsampling.py
deleted file mode 100644
index 52bbd5737c4a8f..00000000000000
--- a/tests/sentry/models/test_dynamicsampling.py
+++ /dev/null
@@ -1,363 +0,0 @@
-from datetime import datetime, timedelta
-
-import pytest
-from django.utils import timezone
-
-from sentry.models.dynamicsampling import (
-    MAX_CUSTOM_RULES_PER_PROJECT,
-    CustomDynamicSamplingRule,
-    TooManyRules,
-)
-from sentry.models.organization import Organization
-from sentry.models.project import Project
-from sentry.testutils.cases import TestCase
-from sentry.testutils.helpers.datetime import freeze_time
-
-
-def _create_rule_for_env(
-    env_idx: int, projects: list[Project], organization: Organization
-) -> CustomDynamicSamplingRule:
-    condition = {"op": "equals", "name": "environment", "value": f"prod{env_idx}"}
-    return CustomDynamicSamplingRule.update_or_create(
-        condition=condition,
-        start=timezone.now(),
-        end=timezone.now() + timedelta(hours=1),
-        project_ids=[project.id for project in projects],
-        organization_id=organization.id,
-        num_samples=100,
-        sample_rate=0.5,
-        query=f"environment:prod{env_idx}",
-    )
-
-
-@freeze_time("2023-09-18")
-class TestCustomDynamicSamplingRuleProject(TestCase):
-    def setUp(self) -> None:
-        super().setUp()
-        self.second_project = self.create_project()
-        self.second_organization = self.create_organization(owner=self.user)
-        self.third_project = self.create_project(organization=self.second_organization)
-
-    def test_update_or_create(self) -> None:
-        condition = {"op": "equals", "name": "environment", "value": "prod"}
-
-        end1 = timezone.now() + timedelta(hours=1)
-
-        rule = CustomDynamicSamplingRule.update_or_create(
-            condition=condition,
-            start=timezone.now(),
-            end=end1,
-            project_ids=[self.project.id],
-            organization_id=self.organization.id,
-            num_samples=100,
-            sample_rate=0.5,
-            query="environment:prod",
-        )
-
-        end2 = timezone.now() + timedelta(hours=1)
-        updated_rule = CustomDynamicSamplingRule.update_or_create(
-            condition=condition,
-            start=timezone.now() + timedelta(minutes=1),
-            end=end2,
-            project_ids=[self.project.id],
-            organization_id=self.organization.id,
-            num_samples=100,
-            sample_rate=0.5,
-            query="environment:prod",
-        )
-
-        assert rule.id == updated_rule.id
-        projects = updated_rule.projects.all()
-
-        assert len(projects) == 1
-        assert self.project in projects
-
-        assert updated_rule.end_date >= end1
-        assert updated_rule.end_date >= end2
-
-    def test_assign_rule_id(self) -> None:
-        rule_ids = set()
-        rules = []
-        for idx in range(3):
-            rule = _create_rule_for_env(idx, [self.project], self.organization)
-            rule_ids.add(rule.rule_id)
-            rules.append(rule)
-
-        # all 3 rules have different rule ids
-        assert len(rule_ids) == 3
-
-        # make a rule obsolete and check that the rule id is reused
-        rules[1].is_active = False
-        rules[1].save()
-
-        new_rule = _create_rule_for_env(4, [self.project], self.organization)
-        assert new_rule.rule_id == rules[1].rule_id
-
-        # a new rule will take another slot (now that there is no free slot)
-        new_rule_2 = _create_rule_for_env(5, [self.project], self.organization)
-        assert new_rule_2.rule_id not in rule_ids
-
-        # make again an empty slot ( this time by having the rule expire)
-        rules[2].start_date = timezone.now() - timedelta(hours=2)
-        rules[2].end_date = timezone.now() - timedelta(hours=1)
-        rules[2].save()
-
-        # the new rule should take the empty slot
-        new_rule_3 = _create_rule_for_env(6, [self.project], self.organization)
-        assert new_rule_3.rule_id == rules[2].rule_id
-
-    def test_deactivate_old_rules(self) -> None:
-        idx = 1
-
-        old_rules = []
-        new_rules = []
-
-        def create_rule(is_old: bool, idx: int) -> CustomDynamicSamplingRule:
-            condition = {"op": "equals", "name": "environment", "value": f"prod{idx}"}
-            if is_old:
-                end_delta = -timedelta(hours=1)
-            else:
-                end_delta = timedelta(hours=1)
-            return CustomDynamicSamplingRule.update_or_create(
-                condition=condition,
-                start=timezone.now() - timedelta(hours=2),
-                end=timezone.now() + end_delta,
-                project_ids=[self.project.id],
-                organization_id=self.organization.id,
-                num_samples=100,
-                sample_rate=0.5,
-                query=f"environment:prod{idx}",
-            )
-
-        for i in range(10):
-            for is_old in [True, False]:
-                idx += 1
-                rule = create_rule(is_old, idx)
-                if is_old:
-                    old_rules.append(rule)
-                else:
-                    new_rules.append(rule)
-
-        CustomDynamicSamplingRule.deactivate_old_rules()
-
-        # check that all old rules are inactive and all new rules are active
-        inactive_rules = list(CustomDynamicSamplingRule.objects.filter(is_active=False))
-        assert len(inactive_rules) == 10
-        for rule in old_rules:
-            assert rule in inactive_rules
-
-        active_rules = list(CustomDynamicSamplingRule.objects.filter(is_active=True))
-        assert len(active_rules) == 10
-        for rule in new_rules:
-            assert rule in active_rules
-
-    def test_get_rule_for_org(self) -> None:
-        """
-        Test the get_rule_for_org method
-        """
-        condition = {"op": "equals", "name": "environment", "value": "prod"}
-
-        # check empty result
-        rule = CustomDynamicSamplingRule.get_rule_for_org(
-            condition, self.organization.id, [self.project.id]
-        )
-        assert rule is None
-
-        new_rule = CustomDynamicSamplingRule.update_or_create(
-            condition=condition,
-            start=timezone.now() - timedelta(hours=2),
-            end=timezone.now() + timedelta(hours=1),
-            project_ids=[self.project.id],
-            organization_id=self.organization.id,
-            num_samples=100,
-            sample_rate=0.5,
-            query="environment:prod",
-        )
-
-        rule = CustomDynamicSamplingRule.get_rule_for_org(
-            condition, self.organization.id, [self.project.id]
-        )
-        assert rule == new_rule
-
-    def test_get_project_rules(self) -> None:
-        """
-        Tests that all valid rules (i.e. active and within the date range) that apply to a project
-        (i.e. that are either organization rules or apply to the project) are returned.
-        """
-
-        idx = [1]
-
-        def create_rule(
-            project_ids: list[int],
-            org_id: int | None = None,
-            old: bool = False,
-            new: bool = False,
-        ) -> CustomDynamicSamplingRule:
-            idx[0] += 1
-            condition = {"op": "equals", "name": "environment", "value": f"prod{idx[0]}"}
-            if old:
-                end_delta = -timedelta(hours=2)
-            else:
-                end_delta = timedelta(hours=2)
-
-            if new:
-                start_delta = timedelta(hours=1)
-            else:
-                start_delta = -timedelta(hours=1)
-
-            if org_id is None:
-                org_id = self.organization.id
-
-            return CustomDynamicSamplingRule.update_or_create(
-                condition=condition,
-                start=timezone.now() + start_delta,
-                end=timezone.now() + end_delta,
-                project_ids=project_ids,
-                organization_id=org_id,
-                num_samples=100,
-                sample_rate=0.5,
-                query=f"environment:prod{idx[0]}",
-            )
-
-        valid_project_rule = create_rule([self.project.id, self.second_project.id])
-        valid_org_rule = create_rule([])
-        # rule for another project
-        create_rule([self.second_project.id])
-        # rule for another org
-        create_rule([self.third_project.id], org_id=self.second_organization.id)
-        # old project rule ( already expired)
-        create_rule([self.project.id], old=True)
-        # new project rule ( not yet active)
-        create_rule([self.project.id], new=True)
-        # old org rule
-        create_rule([], old=True)
-        # new org rule
-        create_rule([], new=True)
-
-        # we should only get valid_project_rule and valid_org_rule
-        rules = list(CustomDynamicSamplingRule.get_project_rules(self.project))
-        assert len(rules) == 2
-        assert valid_project_rule in rules
-        assert valid_org_rule in rules
-
-    def test_separate_projects_create_different_rules(self) -> None:
-        """
-        Tests that same condition for different projects create different rules
-        """
-        condition = {"op": "equals", "name": "environment", "value": "prod"}
-
-        end1 = timezone.now() + timedelta(hours=1)
-
-        rule = CustomDynamicSamplingRule.update_or_create(
-            condition=condition,
-            start=timezone.now(),
-            end=end1,
-            project_ids=[self.project.id],
-            organization_id=self.organization.id,
-            num_samples=100,
-            sample_rate=0.5,
-            query="environment:prod",
-        )
-
-        end2 = timezone.now() + timedelta(hours=1)
-        second_rule = CustomDynamicSamplingRule.update_or_create(
-            condition=condition,
-            start=timezone.now() + timedelta(minutes=1),
-            end=end2,
-            project_ids=[self.second_project.id],
-            organization_id=self.organization.id,
-            num_samples=100,
-            sample_rate=0.5,
-            query="environment:prod",
-        )
-
-        assert rule.id != second_rule.id
-
-        first_projects = rule.projects.all()
-        assert len(first_projects) == 1
-        assert self.project == first_projects[0]
-
-        second_projects = second_rule.projects.all()
-        assert len(second_projects) == 1
-        assert self.second_project == second_projects[0]
-
-    def test_deactivate_expired_rules(self) -> None:
-        """
-        Tests that expired, and only expired, rules are deactivated
-        """
-
-        def create_rule(
-            env_idx: int, end: datetime, project_ids: list[int]
-        ) -> CustomDynamicSamplingRule:
-            condition = {"op": "equals", "name": "environment", "value": f"prod{env_idx}"}
-            return CustomDynamicSamplingRule.update_or_create(
-                condition=condition,
-                start=timezone.now() - timedelta(hours=5),
-                end=end,
-                project_ids=project_ids,
-                organization_id=self.organization.id,
-                num_samples=100,
-                sample_rate=0.5,
-                query=f"environment:prod{env_idx}",
-            )
-
-        env_idx = 1
-        expired_rules: set[int] = set()
-        active_rules: set[int] = set()
-
-        for projects in [
-            [self.project],
-            [self.second_project],
-            [self.third_project],
-            [self.project, self.second_project, self.third_project],
-            [],
-        ]:
-            # create some expired rules
-            project_ids = [p.id for p in projects]
-            rule = create_rule(env_idx, timezone.now() - timedelta(minutes=5), project_ids)
-            expired_rules.add(rule.id)
-            env_idx += 1
-
-            # create some active rules
-            rule = create_rule(env_idx, timezone.now() + timedelta(minutes=5), project_ids)
-            active_rules.add(rule.id)
-            env_idx += 1
-
-        # check that all rules are active before deactivation
-        for rule in CustomDynamicSamplingRule.objects.all():
-            assert rule.is_active
-
-        CustomDynamicSamplingRule.deactivate_expired_rules()
-
-        # check that all expired rules are inactive and all active rules are still active
-        for rule in CustomDynamicSamplingRule.objects.all():
-            if rule.id in expired_rules:
-                assert not rule.is_active
-            else:
-                assert rule.is_active
-                assert rule.id in active_rules
-
-    def test_per_project_limit(self) -> None:
-        """
-        Tests that it is not possible to create more than MAX_CUSTOM_RULES_PER_PROJECT
-        for a project
-        """
-
-        # a few org rules
-        num_org_rules = 10
-        for idx in range(num_org_rules):
-            _create_rule_for_env(idx, [], self.organization)
-
-        # now add project rules (up to MAX_CUSTOM_RULES_PER_PROJECT)
-        for idx in range(num_org_rules, MAX_CUSTOM_RULES_PER_PROJECT):
-            _create_rule_for_env(idx, [self.project], self.organization)
-            _create_rule_for_env(idx, [self.second_project], self.organization)
-
-        # we've reached the limit for both project and second_project next one should raise TooManyRules()
-        with pytest.raises(TooManyRules):
-            _create_rule_for_env(MAX_CUSTOM_RULES_PER_PROJECT, [self.project], self.organization)
-
-        with pytest.raises(TooManyRules):
-            _create_rule_for_env(
-                MAX_CUSTOM_RULES_PER_PROJECT, [self.second_project], self.organization
-            )
diff --git a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap
index 84c6bfda4e2c21..bb542cfeee47e3 100644
--- a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap
+++ b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap
@@ -96,7 +96,7 @@ steps:
   - -U
   - postgres
   - -c
-  - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_dataconditiongroup,workflow_engine_datasource,workflow_engine_datacondition,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_customdynamicsamplingrule,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,workflow_engine_workflow,workflow_engine_detector,workflow_engine_datasourcedetector,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_dashboardpermissions,sentry_dashboardfavoriteuser,sentry_customdynamicsamplingruleproject,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,workflow_engine_workflowdataconditiongroup,workflow_engine_detectorworkflow,workflow_engine_alertruleworkflow,workflow_engine_alertruledetector,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_dashboardpermissionsteam,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,workflow_engine_alertruletriggerdatacondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsnapshot,sentry_incidentactivity
+  - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_dataconditiongroup,workflow_engine_datasource,workflow_engine_datacondition,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,workflow_engine_workflow,workflow_engine_detector,workflow_engine_datasourcedetector,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_dashboardpermissions,sentry_dashboardfavoriteuser,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,workflow_engine_workflowdataconditiongroup,workflow_engine_detectorworkflow,workflow_engine_alertruleworkflow,workflow_engine_alertruledetector,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_dashboardpermissionsteam,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,workflow_engine_alertruletriggerdatacondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsnapshot,sentry_incidentactivity
     RESTART IDENTITY CASCADE;
   id: clear-database
   name: gcr.io/cloud-builders/docker