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 image --- .../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")] + }