From ae2ccdf30c1745630305fd568a5c02635bef7e5c Mon Sep 17 00:00:00 2001
From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com>
Date: Tue, 9 Jan 2024 11:32:26 -0500
Subject: [PATCH] feat(browser-starfish): Add project settings endpoint for
images (#62025)
Work for #60482
This PR adds a new endpoint to enable/disable images for the resource
module as a project settings.
Complements #62131 , this PR shall be merged and deployed first.
TODO:
- [x] Add some more error handling
- [x] Add tests
---
.../project_performance_general_settings.py | 75 +++++++++++++++++++
src/sentry/api/urls.py | 8 ++
src/sentry/projectoptions/defaults.py | 10 +++
...st_project_performance_general_settings.py | 74 ++++++++++++++++++
4 files changed, 167 insertions(+)
create mode 100644 src/sentry/api/endpoints/project_performance_general_settings.py
create mode 100644 tests/sentry/api/endpoints/test_project_performance_general_settings.py
diff --git a/src/sentry/api/endpoints/project_performance_general_settings.py b/src/sentry/api/endpoints/project_performance_general_settings.py
new file mode 100644
index 00000000000000..dee0fd27b81fec
--- /dev/null
+++ b/src/sentry/api/endpoints/project_performance_general_settings.py
@@ -0,0 +1,75 @@
+from rest_framework import serializers, status
+from rest_framework.request import Request
+from rest_framework.response import Response
+
+from sentry import features
+from sentry.api.api_owners import ApiOwner
+from sentry.api.api_publish_status import ApiPublishStatus
+from sentry.api.base import region_silo_endpoint
+from sentry.api.bases.project import ProjectEndpoint, ProjectSettingPermission
+from sentry.api.permissions import SuperuserPermission
+from sentry.models.project import Project
+from sentry.projectoptions.defaults import DEFAULT_PROJECT_PERFORMANCE_GENERAL_SETTINGS
+
+SETTINGS_PROJECT_OPTION_KEY = "sentry:performance_general_settings"
+
+
+class ProjectPerformanceGeneralSettingsSerializer(serializers.Serializer):
+ enable_images = serializers.BooleanField(required=False)
+
+
+class ProjectOwnerOrSuperUserPermissions(ProjectSettingPermission):
+ def has_object_permission(self, request: Request, view, project):
+ return super().has_object_permission(
+ request, view, project
+ ) or SuperuserPermission().has_permission(request, view)
+
+
+@region_silo_endpoint
+class ProjectPerformanceGeneralSettingsEndpoint(ProjectEndpoint):
+ owner = ApiOwner.PERFORMANCE
+ publish_status = {
+ "DELETE": ApiPublishStatus.PRIVATE,
+ "GET": ApiPublishStatus.PRIVATE,
+ "POST": ApiPublishStatus.PRIVATE,
+ }
+ permission_classes = (ProjectOwnerOrSuperUserPermissions,)
+
+ def get(self, request: Request, project) -> Response:
+ if not self.has_feature(project, request):
+ return self.respond(status=status.HTTP_404_NOT_FOUND)
+
+ if not project:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+
+ project_option_settings = self.get_current_settings(project)
+ return Response(project_option_settings)
+
+ def post(self, request: Request, project: Project) -> Response:
+ if not self.has_feature(project, request):
+ return self.respond(status=status.HTTP_404_NOT_FOUND)
+
+ if not project:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+
+ serializer = ProjectPerformanceGeneralSettingsSerializer(data=request.data)
+
+ if not serializer.is_valid():
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ self.update_settings(project, request.data)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+ def has_feature(self, project, request) -> bool:
+ return features.has(
+ "organizations:performance-view", project.organization, actor=request.user
+ )
+
+ def get_current_settings(self, project: Project):
+ return project.get_option(
+ SETTINGS_PROJECT_OPTION_KEY, DEFAULT_PROJECT_PERFORMANCE_GENERAL_SETTINGS
+ )
+
+ def update_settings(self, project: Project, new_settings: dict):
+ current_settings = self.get_current_settings(project)
+ project.update_option(SETTINGS_PROJECT_OPTION_KEY, {**current_settings, **new_settings})
diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py
index f7c3d0496b4fe4..3fe2f219c524b8 100644
--- a/src/sentry/api/urls.py
+++ b/src/sentry/api/urls.py
@@ -480,6 +480,9 @@
from .endpoints.project_keys import ProjectKeysEndpoint
from .endpoints.project_member_index import ProjectMemberIndexEndpoint
from .endpoints.project_ownership import ProjectOwnershipEndpoint
+from .endpoints.project_performance_general_settings import (
+ ProjectPerformanceGeneralSettingsEndpoint,
+)
from .endpoints.project_performance_issue_settings import ProjectPerformanceIssueSettingsEndpoint
from .endpoints.project_platforms import ProjectPlatformsEndpoint
from .endpoints.project_plugin_details import ProjectPluginDetailsEndpoint
@@ -2484,6 +2487,11 @@
ProjectPerformanceIssueSettingsEndpoint.as_view(),
name="sentry-api-0-project-performance-issue-settings",
),
+ re_path(
+ r"^(?P[^\/]+)/(?P[^\/]+)/performance/configure/$",
+ ProjectPerformanceGeneralSettingsEndpoint.as_view(),
+ name="sentry-api-0-project-performance-general-settings",
+ ),
# Load plugin project urls
re_path(
r"^(?P[^\/]+)/(?P[^\/]+)/plugins/$",
diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py
index 8bfa5d33effe8d..2b84f2bd4c029a 100644
--- a/src/sentry/projectoptions/defaults.py
+++ b/src/sentry/projectoptions/defaults.py
@@ -120,12 +120,22 @@
"transaction_duration_regression_detection_enabled": True,
}
+DEFAULT_PROJECT_PERFORMANCE_GENERAL_SETTINGS = {
+ "enable_images": False,
+}
+
# A dict containing all the specific detection thresholds and rates.
register(
key="sentry:performance_issue_settings",
default=DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS,
)
+register(
+ key="sentry:performance_general_settings",
+ default=DEFAULT_PROJECT_PERFORMANCE_GENERAL_SETTINGS,
+)
+
+
# Replacement rules for transaction names discovered by the transaction clusterer.
# Contains a mapping from rule to last seen timestamp,
# for example `{"/organizations/*/**": 1334318402}`
diff --git a/tests/sentry/api/endpoints/test_project_performance_general_settings.py b/tests/sentry/api/endpoints/test_project_performance_general_settings.py
new file mode 100644
index 00000000000000..d5524d3ea466b2
--- /dev/null
+++ b/tests/sentry/api/endpoints/test_project_performance_general_settings.py
@@ -0,0 +1,74 @@
+from django.urls import reverse
+from rest_framework.exceptions import ErrorDetail
+
+from sentry.testutils.cases import APITestCase
+from sentry.testutils.silo import region_silo_test
+
+PERFORMANCE_SETTINGS_FEATURES = {
+ "organizations:performance-view": True,
+}
+
+
+@region_silo_test
+class ProjectPerformanceGeneralSettingsTest(APITestCase):
+ endpoint = "sentry-api-0-project-performance-general-settings"
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.login_as(user=self.user, superuser=True)
+ self.project = self.create_project()
+
+ self.url = reverse(
+ self.endpoint,
+ kwargs={
+ "organization_slug": self.project.organization.slug,
+ "project_slug": self.project.slug,
+ },
+ )
+
+ def test_get_project_general_settings_defaults(self):
+ with self.feature(PERFORMANCE_SETTINGS_FEATURES):
+ response = self.client.get(self.url, format="json")
+
+ assert response.status_code == 200, response.content
+
+ assert response.data["enable_images"] is False
+
+ def test_get_returns_error_without_feature_enabled(self):
+ with self.feature({}):
+ response = self.client.get(self.url, format="json")
+ assert response.status_code == 404
+
+ def test_updates_to_new_value(self):
+ with self.feature(PERFORMANCE_SETTINGS_FEATURES):
+ response = self.client.post(
+ self.url,
+ data={
+ "enable_images": True,
+ },
+ )
+ response = self.client.get(self.url, format="json")
+ assert response.data["enable_images"] is True
+
+ response = self.client.post(
+ self.url,
+ data={
+ "enable_images": False,
+ },
+ )
+ response = self.client.get(self.url, format="json")
+ assert response.data["enable_images"] is False
+
+ def test_update_project_setting_check_validation(self):
+ with self.feature(PERFORMANCE_SETTINGS_FEATURES):
+ response = self.client.post(
+ self.url,
+ data={
+ "enable_images": -1,
+ },
+ )
+
+ assert response.status_code == 400, response.content
+ assert response.data == {
+ "enable_images": [ErrorDetail(string="Must be a valid boolean.", code="invalid")]
+ }