Skip to content

Commit 85cac7a

Browse files
committed
Merge remote-tracking branch 'origin/master' into kafka-no-zookeeper
2 parents 15d166f + dcac7bb commit 85cac7a

File tree

103 files changed

+1934
-967
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+1934
-967
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ repos:
5858
types: [python]
5959
exclude: ^src/sentry/metrics/minimetrics.py$
6060

61+
- repo: https://github.com/pre-commit/pygrep-hooks
62+
rev: v1.10.0
63+
hooks:
64+
- id: python-use-type-annotations
65+
6166
- repo: https://github.com/python-jsonschema/check-jsonschema
6267
rev: 0.24.1
6368
hooks:

pyproject.toml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,6 @@ module = [
809809
"tests.sentry.api.endpoints.test_organization_metrics",
810810
"tests.sentry.api.helpers.test_group_index",
811811
"tests.sentry.api.serializers.test_project",
812-
"tests.sentry.api.serializers.test_team",
813812
"tests.sentry.api.test_authentication",
814813
"tests.sentry.api.test_base",
815814
"tests.sentry.api.test_event_search",
@@ -819,11 +818,6 @@ module = [
819818
"tests.sentry.eventstore.test_base",
820819
"tests.sentry.grouping.test_result",
821820
"tests.sentry.identity.test_oauth2",
822-
"tests.sentry.incidents.endpoints.test_organization_alert_rule_details",
823-
"tests.sentry.incidents.endpoints.test_organization_alert_rule_index",
824-
"tests.sentry.incidents.endpoints.test_organization_incident_details",
825-
"tests.sentry.incidents.endpoints.test_organization_incident_subscription_index",
826-
"tests.sentry.incidents.endpoints.test_project_alert_rule_index",
827821
"tests.sentry.incidents.test_logic",
828822
"tests.sentry.ingest.test_slicing",
829823
"tests.sentry.integrations.github.test_client",
@@ -834,8 +828,6 @@ module = [
834828
"tests.sentry.models.test_organizationmember",
835829
"tests.sentry.models.test_project",
836830
"tests.sentry.release_health.release_monitor",
837-
"tests.sentry.release_health.test_tasks",
838-
"tests.sentry.replays.consumers.test_recording",
839831
"tests.sentry.replays.test_project_replay_recording_segment_details",
840832
"tests.sentry.replays.test_project_replay_recording_segment_index",
841833
"tests.sentry.replays.unit.test_dead_click_issue",
@@ -849,7 +841,6 @@ module = [
849841
"tests.sentry.snuba.metrics.test_snql",
850842
"tests.sentry.snuba.test_tasks",
851843
"tests.sentry.tagstore.test_types",
852-
"tests.sentry.tasks.deletion.test_scheduled",
853844
"tests.sentry.tasks.test_post_process",
854845
"tests.sentry.web.test_client_config",
855846
"tests.snuba.rules.conditions.test_event_frequency",

src/sentry/api/endpoints/codeowners/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ class Meta:
2727
fields = ["raw", "code_mapping_id", "organization_integration_id"]
2828

2929
def get_max_length(self) -> int:
30-
# typecast needed for typing, though these will always be ints
31-
return int(MAX_RAW_LENGTH)
30+
return MAX_RAW_LENGTH
3231

3332
def validate(self, attrs: Mapping[str, Any]) -> Mapping[str, Any]:
3433
# If it already exists, set default attrs with existing values

src/sentry/api/endpoints/notifications/notification_actions_index.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Dict
33

44
from django.db.models import Q
5+
from drf_spectacular.utils import extend_schema
56
from rest_framework import status
67
from rest_framework.exceptions import PermissionDenied
78
from rest_framework.request import Request
@@ -14,7 +15,11 @@
1415
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
1516
from sentry.api.paginator import OffsetPaginator
1617
from sentry.api.serializers.base import serialize
18+
from sentry.api.serializers.models.notification_action import OutgoingNotificationActionSerializer
1719
from sentry.api.serializers.rest_framework.notification_action import NotificationActionSerializer
20+
from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN
21+
from sentry.apidocs.examples import notification_examples
22+
from sentry.apidocs.parameters import GlobalParams, NotificationParams, OrganizationParams
1823
from sentry.models.notificationaction import NotificationAction
1924
from sentry.models.organization import Organization
2025

@@ -43,10 +48,11 @@ class NotificationActionsPermission(OrganizationPermission):
4348

4449

4550
@region_silo_endpoint
51+
@extend_schema(tags=["Notifications"])
4652
class NotificationActionsIndexEndpoint(OrganizationEndpoint):
4753
publish_status = {
48-
"GET": ApiPublishStatus.EXPERIMENTAL,
49-
"POST": ApiPublishStatus.EXPERIMENTAL,
54+
"GET": ApiPublishStatus.PUBLIC,
55+
"POST": ApiPublishStatus.PUBLIC,
5056
}
5157

5258
owner = ApiOwner.ENTERPRISE
@@ -59,7 +65,29 @@ class NotificationActionsIndexEndpoint(OrganizationEndpoint):
5965

6066
permission_classes = (NotificationActionsPermission,)
6167

68+
@extend_schema(
69+
operation_id="List Spike Protection Notifications",
70+
parameters=[
71+
GlobalParams.ORG_SLUG,
72+
OrganizationParams.PROJECT,
73+
OrganizationParams.PROJECT_SLUG,
74+
NotificationParams.TRIGGER_TYPE,
75+
],
76+
responses={
77+
201: OutgoingNotificationActionSerializer,
78+
400: RESPONSE_BAD_REQUEST,
79+
403: RESPONSE_FORBIDDEN,
80+
},
81+
examples=notification_examples.CREATE_NOTIFICATION_ACTION,
82+
)
6283
def get(self, request: Request, organization: Organization) -> Response:
84+
"""
85+
Returns all Spike Protection Notification Actions for an organization.
86+
87+
Notification Actions notify a set of members through a service such as Slack or Sentry when an action has been triggered. For example, you can email the organization owner when the spike protection threshold has been reached.
88+
89+
You can use either the `project` or `projectSlug` query parameter to filter for certain projects. Note that if both are present, `projectSlug` takes priority.
90+
"""
6391
queryset = NotificationAction.objects.filter(organization_id=organization.id)
6492
# If a project query is specified, filter out non-project-specific actions
6593
# otherwise, include them but still ensure project permissions are enforced
@@ -89,7 +117,25 @@ def get(self, request: Request, organization: Organization) -> Response:
89117
paginator_cls=OffsetPaginator,
90118
)
91119

120+
@extend_schema(
121+
operation_id="Create a Spike Protection Notification Action",
122+
parameters=[
123+
GlobalParams.ORG_SLUG,
124+
],
125+
request=NotificationActionSerializer,
126+
responses={
127+
201: OutgoingNotificationActionSerializer,
128+
400: RESPONSE_BAD_REQUEST,
129+
403: RESPONSE_FORBIDDEN,
130+
},
131+
examples=notification_examples.CREATE_NOTIFICATION_ACTION,
132+
)
92133
def post(self, request: Request, organization: Organization) -> Response:
134+
"""
135+
Creates a new Notification Action for Spike Protection.
136+
137+
Notification Actions notify a set of members through a service such as Slack or Sentry when an action has been triggered. For example, you can email the organization owner when the spike protection threshold has been reached.
138+
"""
93139
# team admins and regular org members don't have project:write on an org level
94140
if not request.access.has_scope("project:write"):
95141
# check if user has access to create notification actions for all requested projects

src/sentry/api/endpoints/organization_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sentry.api.utils import InvalidParams
1616
from sentry.apidocs import constants as api_constants
1717
from sentry.apidocs.examples.discover_performance_examples import DiscoverAndPerformanceExamples
18-
from sentry.apidocs.parameters import GlobalParams, VisibilityParams
18+
from sentry.apidocs.parameters import GlobalParams, OrganizationParams, VisibilityParams
1919
from sentry.apidocs.utils import inline_sentry_response_serializer
2020
from sentry.models.organization import Organization
2121
from sentry.ratelimits.config import RateLimitConfig
@@ -170,7 +170,7 @@ def get_features(self, organization: Organization, request: Request) -> Mapping[
170170
GlobalParams.END,
171171
GlobalParams.ENVIRONMENT,
172172
GlobalParams.ORG_SLUG,
173-
GlobalParams.PROJECT,
173+
OrganizationParams.PROJECT,
174174
GlobalParams.START,
175175
GlobalParams.STATS_PERIOD,
176176
VisibilityParams.FIELD,

src/sentry/api/endpoints/organization_profiling_functions.py

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from enum import Enum
55
from typing import Any, List
66

7-
import sentry_sdk
87
from rest_framework import serializers
98
from rest_framework.request import Request
109
from rest_framework.response import Response
@@ -16,7 +15,6 @@
1615
from sentry.api.paginator import GenericOffsetPaginator
1716
from sentry.exceptions import InvalidSearchQuery
1817
from sentry.models.organization import Organization
19-
from sentry.profiles.utils import get_from_profiling_service
2018
from sentry.search.events.builder import ProfileTopFunctionsTimeseriesQueryBuilder
2119
from sentry.search.events.types import QueryBuilderConfig
2220
from sentry.seer.utils import BreakpointData, detect_breakpoints
@@ -223,17 +221,6 @@ def get_trends_data(stats_data) -> List[BreakpointData]:
223221
reverse=data["trend"] is TrendType.REGRESSION,
224222
)
225223

226-
if data["trend"] is TrendType.REGRESSION:
227-
try:
228-
if features.has(
229-
"organizations:profile-function-regression-exp-ingest",
230-
organization,
231-
actor=request.user,
232-
):
233-
forward_regression_occurrences(organization, trending_functions, stats_data)
234-
except Exception as e:
235-
sentry_sdk.capture_exception(e)
236-
237224
def paginate_trending_events(offset, limit):
238225
return {"data": trending_functions[offset : limit + offset]}
239226

@@ -322,48 +309,3 @@ def get_interval_from_range(date_range: timedelta) -> str:
322309
return "2h"
323310

324311
return "1h"
325-
326-
327-
def forward_regression_occurrences(
328-
organization: Organization,
329-
regressions: List[BreakpointData],
330-
stats_data: Any,
331-
):
332-
payloads = []
333-
334-
for entry in regressions:
335-
project_id = int(entry["project"])
336-
fingerprint = int(entry["transaction"])
337-
338-
profile_id = None
339-
examples = (
340-
stats_data.get(f"{project_id},{fingerprint}", {}).get("worst()", {}).get("data", [])
341-
)
342-
for row in reversed(examples):
343-
example = row[1][0]["count"]
344-
if isinstance(example, str):
345-
profile_id = example
346-
break
347-
348-
if profile_id is None:
349-
continue
350-
351-
payloads.append(
352-
{
353-
"organization_id": organization.id,
354-
"project_id": project_id,
355-
"profile_id": profile_id,
356-
"fingerprint": fingerprint,
357-
"absolute_percentage_change": entry["absolute_percentage_change"],
358-
"aggregate_range_1": entry["aggregate_range_1"],
359-
"aggregate_range_2": entry["aggregate_range_2"],
360-
"breakpoint": int(entry["breakpoint"]),
361-
"trend_difference": entry["trend_difference"],
362-
"trend_percentage": entry["trend_percentage"],
363-
"unweighted_p_value": entry["unweighted_p_value"],
364-
"unweighted_t_value": entry["unweighted_t_value"],
365-
}
366-
)
367-
368-
if payloads:
369-
get_from_profiling_service(method="POST", path="/regressed", json_data=payloads)

src/sentry/api/endpoints/project_stacktrace_link.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import logging
24
from typing import Dict, List, Mapping, Optional
35

@@ -157,7 +159,7 @@ def get_code_mapping_configs(project: Project) -> List[RepositoryProjectPathConf
157159
project=project, organization_integration_id__isnull=False
158160
)
159161

160-
sorted_configs = [] # type: List[RepositoryProjectPathConfig]
162+
sorted_configs: list[RepositoryProjectPathConfig] = []
161163

162164
try:
163165
for config in configs:

src/sentry/api/serializers/models/notification_action.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, Sequence
1+
from typing import Any, Dict, Sequence
22

33
from django.contrib.auth.models import AnonymousUser
44

@@ -34,7 +34,7 @@ def get_attrs(self, item_list: Sequence[NotificationAction], user):
3434
for item in item_list
3535
}
3636

37-
def serialize(self, obj: NotificationAction, attrs, user, **kwargs):
37+
def serialize(self, obj: NotificationAction, attrs, user, **kwargs) -> Dict[str, Any]:
3838
return {
3939
"id": obj.id,
4040
"organizationId": obj.organization_id,

src/sentry/api/serializers/rest_framework/dashboard.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
from datetime import datetime, timedelta
3+
from enum import Enum
34

45
from django.db.models import Max
56
from rest_framework import serializers
@@ -236,6 +237,11 @@ def _get_attr(self, data, attr, empty_value=None):
236237
return empty_value
237238

238239

240+
class ThresholdMaxKeys(Enum):
241+
MAX_1 = "max1"
242+
MAX_2 = "max2"
243+
244+
239245
class DashboardWidgetSerializer(CamelSnakeSerializer):
240246
# Is a string because output serializers also make it a string.
241247
id = serializers.CharField(required=False)
@@ -301,6 +307,49 @@ def validate(self, data):
301307
raise serializers.ValidationError(
302308
{"displayType": "displayType is required during creation."}
303309
)
310+
311+
# Validate widget thresholds
312+
thresholds = data.get("thresholds")
313+
if thresholds:
314+
max_values = thresholds.get("max_values")
315+
allowed_max_keys = [key.value for key in ThresholdMaxKeys]
316+
if max_values:
317+
for i in range(len(max_values)):
318+
max_key = f"max{i+1}"
319+
320+
if max_key not in allowed_max_keys:
321+
raise serializers.ValidationError(
322+
{"thresholds": f"Invalid maximum key {max_key}"}
323+
)
324+
325+
if max_values.get(max_key):
326+
if max_values.get(max_key) < 0:
327+
raise serializers.ValidationError(
328+
{"thresholds": {max_key: "Maximum values can not be negative"}}
329+
)
330+
elif i > 0:
331+
prev_max_key = f"max{i}"
332+
if max_values.get(prev_max_key) and max_values.get(
333+
prev_max_key
334+
) >= max_values.get(max_key):
335+
raise serializers.ValidationError(
336+
{
337+
"thresholds": {
338+
max_key: "Maximum value must be greater than minimum."
339+
}
340+
}
341+
)
342+
343+
if len(max_values) < len(ThresholdMaxKeys):
344+
for key in allowed_max_keys:
345+
if max_values.get(key) is None:
346+
raise serializers.ValidationError(
347+
{
348+
"thresholds": {
349+
key: "Must set all threshold maximums or none at all."
350+
}
351+
}
352+
)
304353
return data
305354

306355

0 commit comments

Comments
 (0)