|
@@ -1,12 +1,11 @@
|
|
|
import math
|
|
|
import time
|
|
|
from datetime import timedelta
|
|
|
-from itertools import chain
|
|
|
from uuid import uuid4
|
|
|
|
|
|
from django.db import IntegrityError, router, transaction
|
|
|
from django.utils import timezone
|
|
|
-from drf_spectacular.utils import extend_schema, inline_serializer
|
|
|
+from drf_spectacular.utils import extend_schema, extend_schema_serializer
|
|
|
from rest_framework import serializers, status
|
|
|
from rest_framework.request import Request
|
|
|
from rest_framework.response import Response
|
|
@@ -66,24 +65,6 @@ from sentry.utils import json
|
|
|
#: Limit determined experimentally here: https://github.com/getsentry/relay/blob/3105d8544daca3a102c74cefcd77db980306de71/relay-general/src/pii/convert.rs#L289
|
|
|
MAX_SENSITIVE_FIELD_CHARS = 4000
|
|
|
|
|
|
-_options_description = """
|
|
|
-Configure various project filters:
|
|
|
-- `Hydration Errors` - Filter out react hydration errors that are often unactionable
|
|
|
-- `IP Addresses` - Filter events from these IP addresses separated with newlines.
|
|
|
-- `Releases` - Filter events from these releases separated with newlines. Allows [glob pattern matching](https://docs.sentry.io/product/data-management-settings/filtering/#glob-matching).
|
|
|
-- `Error Message` - Filter events by error messages separated with newlines. Allows [glob pattern matching](https://docs.sentry.io/product/data-management-settings/filtering/#glob-matching).
|
|
|
-```json
|
|
|
-{
|
|
|
- options: {
|
|
|
- filters:react-hydration-errors: true,
|
|
|
- filters:blacklisted_ips: "127.0.0.1\\n192.168. 0.1"
|
|
|
- filters:releases: "[!3]\\n4"
|
|
|
- filters:error_messages: "TypeError*\\n*ConnectionError*"
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-"""
|
|
|
-
|
|
|
|
|
|
def clean_newline_inputs(value, case_insensitive=True):
|
|
|
result = []
|
|
@@ -109,22 +90,63 @@ class DynamicSamplingBiasSerializer(serializers.Serializer):
|
|
|
|
|
|
|
|
|
class ProjectMemberSerializer(serializers.Serializer):
|
|
|
- isBookmarked = serializers.BooleanField()
|
|
|
- isSubscribed = serializers.BooleanField()
|
|
|
+ isBookmarked = serializers.BooleanField(
|
|
|
+ help_text="Enables starring the project within the projects tab. Can be updated with **`project:read`** permission.",
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
+ isSubscribed = serializers.BooleanField(
|
|
|
+ help_text="Subscribes the member for notifications related to the project. Can be updated with **`project:read`** permission.",
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
|
|
|
|
|
|
+@extend_schema_serializer(exclude_fields=["options"])
|
|
|
class ProjectAdminSerializer(ProjectMemberSerializer, PreventNumericSlugMixin):
|
|
|
- name = serializers.CharField(max_length=200)
|
|
|
+ name = serializers.CharField(
|
|
|
+ help_text="The name for the project",
|
|
|
+ max_length=200,
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
slug = serializers.RegexField(
|
|
|
DEFAULT_SLUG_PATTERN,
|
|
|
max_length=50,
|
|
|
error_messages={"invalid": DEFAULT_SLUG_ERROR_MESSAGE},
|
|
|
+ help_text="Uniquely identifies a project and is used for the interface.",
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
+ platform = serializers.CharField(
|
|
|
+ help_text="The platform for the project",
|
|
|
+ required=False,
|
|
|
+ allow_null=True,
|
|
|
+ allow_blank=True,
|
|
|
+ )
|
|
|
+
|
|
|
+ subjectPrefix = serializers.CharField(
|
|
|
+ help_text="Custom prefix for emails from this project.",
|
|
|
+ max_length=200,
|
|
|
+ allow_blank=True,
|
|
|
+ required=False,
|
|
|
)
|
|
|
+ subjectTemplate = serializers.CharField(
|
|
|
+ help_text="""The email subject to use (excluding the prefix) for individual alerts. Here are the list of variables you can use:
|
|
|
+- `$title`
|
|
|
+- `$shortID`
|
|
|
+- `$projectID`
|
|
|
+- `$orgID`
|
|
|
+- `${tag:key}` - such as `${tag:environment}` or `${tag:release}`.""",
|
|
|
+ max_length=200,
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
+ resolveAge = EmptyIntegerField(
|
|
|
+ required=False,
|
|
|
+ allow_null=True,
|
|
|
+ help_text="Automatically resolve an issue if it hasn't been seen for this many hours. Set to `0` to disable auto-resolve.",
|
|
|
+ )
|
|
|
+
|
|
|
+ # TODO: Add help_text to all the fields for public documentation
|
|
|
team = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50)
|
|
|
digestsMinDelay = serializers.IntegerField(min_value=60, max_value=3600)
|
|
|
digestsMaxDelay = serializers.IntegerField(min_value=60, max_value=3600)
|
|
|
- subjectPrefix = serializers.CharField(max_length=200, allow_blank=True)
|
|
|
- subjectTemplate = serializers.CharField(max_length=200)
|
|
|
securityToken = serializers.RegexField(
|
|
|
r"^[-a-zA-Z0-9+/=\s]+$", max_length=255, allow_blank=True
|
|
|
)
|
|
@@ -155,8 +177,7 @@ class ProjectAdminSerializer(ProjectMemberSerializer, PreventNumericSlugMixin):
|
|
|
groupingAutoUpdate = serializers.BooleanField(required=False)
|
|
|
scrapeJavaScript = serializers.BooleanField(required=False)
|
|
|
allowedDomains = EmptyListField(child=OriginField(allow_blank=True), required=False)
|
|
|
- resolveAge = EmptyIntegerField(required=False, allow_null=True)
|
|
|
- platform = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
|
|
+
|
|
|
copy_from_project = serializers.IntegerField(required=False)
|
|
|
dynamicSamplingBiases = DynamicSamplingBiasSerializer(required=False, many=True)
|
|
|
performanceIssueCreationRate = serializers.FloatField(required=False, min_value=0, max_value=1)
|
|
@@ -165,6 +186,13 @@ class ProjectAdminSerializer(ProjectMemberSerializer, PreventNumericSlugMixin):
|
|
|
recapServerUrl = serializers.URLField(required=False, allow_blank=True, allow_null=True)
|
|
|
recapServerToken = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
|
|
|
|
|
+ # DO NOT ADD MORE TO OPTIONS
|
|
|
+ # Each param should be a field in the serializer like above.
|
|
|
+ # Keeping options here for backward compatibility but removing it from documentation.
|
|
|
+ options = serializers.DictField(
|
|
|
+ required=False,
|
|
|
+ )
|
|
|
+
|
|
|
def validate(self, data):
|
|
|
max_delay = (
|
|
|
data["digestsMaxDelay"]
|
|
@@ -473,34 +501,7 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
|
|
|
GlobalParams.ORG_SLUG,
|
|
|
GlobalParams.PROJECT_SLUG,
|
|
|
],
|
|
|
- request=inline_serializer(
|
|
|
- "UpdateProject",
|
|
|
- fields={
|
|
|
- "slug": serializers.RegexField(
|
|
|
- DEFAULT_SLUG_PATTERN,
|
|
|
- help_text="Uniquely identifies a project and is used for the interface.",
|
|
|
- required=False,
|
|
|
- max_length=50,
|
|
|
- error_messages={"invalid": DEFAULT_SLUG_ERROR_MESSAGE},
|
|
|
- ),
|
|
|
- "name": serializers.CharField(
|
|
|
- help_text="The name for the project.",
|
|
|
- required=False,
|
|
|
- max_length=200,
|
|
|
- ),
|
|
|
- "platform": serializers.CharField(
|
|
|
- help_text="The platform for the project.",
|
|
|
- required=False,
|
|
|
- allow_null=True,
|
|
|
- allow_blank=True,
|
|
|
- ),
|
|
|
- "isBookmarked": serializers.BooleanField(
|
|
|
- help_text="Enables starring the project within the projects tab.",
|
|
|
- required=False,
|
|
|
- ),
|
|
|
- "options": serializers.DictField(help_text=_options_description, required=False),
|
|
|
- },
|
|
|
- ),
|
|
|
+ request=ProjectAdminSerializer,
|
|
|
responses={
|
|
|
200: DetailedProjectSerializer,
|
|
|
403: RESPONSE_FORBIDDEN,
|
|
@@ -513,7 +514,7 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
|
|
|
Update various attributes and configurable settings for the given project.
|
|
|
|
|
|
Note that solely having the **`project:read`** scope restricts updatable settings to
|
|
|
- `isBookmarked` only.
|
|
|
+ `isBookmarked` and `isSubscribed`.
|
|
|
"""
|
|
|
|
|
|
old_data = serialize(project, request.user, DetailedProjectSerializer())
|
|
@@ -546,14 +547,12 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
|
|
|
return Response(serializer.errors, status=400)
|
|
|
|
|
|
if not has_elevated_scopes:
|
|
|
- # options isn't part of the serializer, but should not be editable by members
|
|
|
- for key in chain(ProjectAdminSerializer().fields.keys(), ["options"]):
|
|
|
+ for key in ProjectAdminSerializer().fields.keys():
|
|
|
if request.data.get(key) and not result.get(key):
|
|
|
return Response(
|
|
|
{"detail": "You do not have permission to perform this action."},
|
|
|
status=403,
|
|
|
)
|
|
|
-
|
|
|
changed = False
|
|
|
changed_proj_settings = {}
|
|
|
|
|
@@ -726,9 +725,9 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
|
|
|
changed_proj_settings["sentry:dynamic_sampling_biases"] = result[
|
|
|
"dynamicSamplingBiases"
|
|
|
]
|
|
|
- # TODO(dcramer): rewrite options to use standard API config
|
|
|
+
|
|
|
if has_elevated_scopes:
|
|
|
- options = request.data.get("options", {})
|
|
|
+ options = result.get("options", {})
|
|
|
if "sentry:origins" in options:
|
|
|
project.update_option(
|
|
|
"sentry:origins", clean_newline_inputs(options["sentry:origins"])
|
|
@@ -806,7 +805,7 @@ class ProjectDetailsEndpoint(ProjectEndpoint):
|
|
|
if "filters:react-hydration-errors" in options:
|
|
|
project.update_option(
|
|
|
"filters:react-hydration-errors",
|
|
|
- bool(options["filters:react-hydration-errors"]),
|
|
|
+ "1" if bool(options["filters:react-hydration-errors"]) else "0",
|
|
|
)
|
|
|
if "filters:chunk-load-error" in options:
|
|
|
project.update_option(
|