diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 9565e3f257b7..69bf36856d33 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -4434,3 +4434,10 @@ default=False, flags=FLAG_AUTOMATOR_MODIFIABLE, ) + +register( + "preprod.snapshots.objectstore.snapshots-usecase.enabled", + type=Bool, + default=False, + flags=FLAG_AUTOMATOR_MODIFIABLE, +) diff --git a/src/sentry/preprod/api/endpoints/project_preprod_upload_options.py b/src/sentry/preprod/api/endpoints/project_preprod_upload_options.py index 2999f75a246d..2fc7174c79ee 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_upload_options.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_upload_options.py @@ -16,6 +16,7 @@ from sentry.models.project import Project from sentry.objectstore import UsecaseId, get_session from sentry.objectstore.types import ObjectstoreUploadOptions +from sentry.preprod.snapshots.storage import get_snapshot_usecase from sentry.utils.http import absolute_uri @@ -28,9 +29,13 @@ class ProjectPreprodUploadOptionsEndpoint(ProjectEndpoint): permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project: Project) -> Response: - if request.GET.get("usecase") not in (None, "auto"): + requested = request.GET.get("usecase") + if requested is None: + usecase = UsecaseId.PREPROD + elif requested == "auto": + usecase = get_snapshot_usecase() + else: return Response({"detail": "Invalid usecase"}, status=400) - usecase = UsecaseId.PREPROD organization = project.organization session = get_session(usecase, project) diff --git a/src/sentry/preprod/snapshots/storage.py b/src/sentry/preprod/snapshots/storage.py index 1a2473bb4cbf..1b8f1c48b01e 100644 --- a/src/sentry/preprod/snapshots/storage.py +++ b/src/sentry/preprod/snapshots/storage.py @@ -6,11 +6,18 @@ from objectstore_client.multipart import MultipartUpload from urllib3.exceptions import HTTPError +from sentry import options from sentry.models.project import Project from sentry.objectstore import UsecaseId, get_session from sentry.utils import metrics +def get_snapshot_usecase() -> UsecaseId: + if options.get("preprod.snapshots.objectstore.snapshots-usecase.enabled"): + return UsecaseId.PREPROD_SNAPSHOTS + return UsecaseId.PREPROD + + # TODO: On January 1, 2027, remove the preprod fallback and use preprod_snapshots exclusively. class SnapshotStorage: def __init__(self, primary: Session, fallback: Session) -> None: @@ -62,7 +69,13 @@ def delete(self, key: str) -> None: def get_snapshot_storage(project: Project | int, *, org: int | None = None) -> SnapshotStorage: + primary_usecase = get_snapshot_usecase() + if primary_usecase == UsecaseId.PREPROD: + fallback_usecase = UsecaseId.PREPROD_SNAPSHOTS + else: + fallback_usecase = UsecaseId.PREPROD + return SnapshotStorage( - primary=get_session(UsecaseId.PREPROD, project, org=org), - fallback=get_session(UsecaseId.PREPROD_SNAPSHOTS, project, org=org), + primary=get_session(primary_usecase, project, org=org), + fallback=get_session(fallback_usecase, project, org=org), ) diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_upload_options.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_upload_options.py index f09e31e46495..ef26d2f6c4fc 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_upload_options.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_upload_options.py @@ -7,6 +7,7 @@ from sentry.objectstore import UsecaseId from sentry.testutils.cases import APITestCase +from sentry.testutils.helpers.options import override_options class ProjectPreprodUploadOptionsTest(APITestCase): @@ -55,6 +56,32 @@ def test_auto_returns_preprod(self, mock_get_session) -> None: assert response.data["objectstore"]["usecase"] == "preprod" mock_get_session.assert_called_once_with(UsecaseId.PREPROD, self.project) + @patch("sentry.preprod.api.endpoints.project_preprod_upload_options.get_session") + def test_auto_follows_option(self, mock_get_session) -> None: + mock_session = MagicMock() + mock_session.mint_token.return_value = "fake-token" + mock_get_session.return_value = mock_session + + with override_options({"preprod.snapshots.objectstore.snapshots-usecase.enabled": True}): + response = self.client.get(self.url, {"usecase": "auto"}) + + assert response.status_code == 200 + assert response.data["objectstore"]["usecase"] == "preprod_snapshots" + mock_get_session.assert_called_once_with(UsecaseId.PREPROD_SNAPSHOTS, self.project) + + @patch("sentry.preprod.api.endpoints.project_preprod_upload_options.get_session") + def test_omitted_param_stays_preprod_after_flip(self, mock_get_session) -> None: + mock_session = MagicMock() + mock_session.mint_token.return_value = "fake-token" + mock_get_session.return_value = mock_session + + with override_options({"preprod.snapshots.objectstore.snapshots-usecase.enabled": True}): + response = self.client.get(self.url) + + assert response.status_code == 200 + assert response.data["objectstore"]["usecase"] == "preprod" + mock_get_session.assert_called_once_with(UsecaseId.PREPROD, self.project) + def test_rejects_explicit_usecase(self) -> None: response = self.client.get(self.url, {"usecase": "preprod_snapshots"}) diff --git a/tests/sentry/preprod/snapshots/test_storage.py b/tests/sentry/preprod/snapshots/test_storage.py index e730e64b9670..8bb17c56260b 100644 --- a/tests/sentry/preprod/snapshots/test_storage.py +++ b/tests/sentry/preprod/snapshots/test_storage.py @@ -6,6 +6,8 @@ from sentry.objectstore import UsecaseId from sentry.preprod.snapshots.storage import SnapshotStorage, get_snapshot_storage +from sentry.testutils.helpers.options import override_options +from sentry.testutils.pytest.fixtures import django_db_all @pytest.fixture @@ -92,10 +94,22 @@ def test_fallback_records_metric(mock_metrics, sessions) -> None: ) +@django_db_all @patch("sentry.preprod.snapshots.storage.get_session") -def test_factory_builds_all_sessions(mock_get_session) -> None: +def test_factory_defaults_to_preprod_primary(mock_get_session) -> None: get_snapshot_storage(42, org=7) assert mock_get_session.call_args_list == [ call(UsecaseId.PREPROD, 42, org=7), call(UsecaseId.PREPROD_SNAPSHOTS, 42, org=7), ] + + +@django_db_all +@patch("sentry.preprod.snapshots.storage.get_session") +def test_factory_follows_option(mock_get_session) -> None: + with override_options({"preprod.snapshots.objectstore.snapshots-usecase.enabled": True}): + get_snapshot_storage(42, org=7) + assert mock_get_session.call_args_list == [ + call(UsecaseId.PREPROD_SNAPSHOTS, 42, org=7), + call(UsecaseId.PREPROD, 42, org=7), + ]