Skip to content

Commit

Permalink
Merge branch 'master' into feat/releases-on-dashboard-charts
Browse files Browse the repository at this point in the history
  • Loading branch information
nikkikapadia committed Sep 23, 2024
2 parents 519df72 + 07396b7 commit 77bff75
Show file tree
Hide file tree
Showing 212 changed files with 3,609 additions and 2,043 deletions.
23 changes: 21 additions & 2 deletions fixtures/vsts.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ def _stub_vsts(self):
},
)

responses.add(
responses.POST,
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
json={
"access_token": self.access_token,
"token_type": "grant",
"expires_in": 300, # seconds (5 min)
"refresh_token": self.refresh_token,
},
)

responses.add(
responses.GET,
"https://app.vssps.visualstudio.com/_apis/accounts?memberId=%s&api-version=4.1"
Expand Down Expand Up @@ -195,19 +206,27 @@ def assert_vsts_oauth_redirect(self, redirect):
assert redirect.netloc == "app.vssps.visualstudio.com"
assert redirect.path == "/oauth2/authorize"

def assert_vsts_new_oauth_redirect(self, redirect):
assert redirect.scheme == "https"
assert redirect.netloc == "login.microsoftonline.com"
assert redirect.path == "/common/oauth2/v2.0/authorize"

def assert_account_selection(self, response, account_id=None):
account_id = account_id or self.vsts_account_id
assert response.status_code == 200
assert f'<option value="{account_id}"'.encode() in response.content

@assume_test_silo_mode(SiloMode.CONTROL)
def assert_installation(self):
def assert_installation(self, new=False):
# Initial request to the installation URL for VSTS
resp = self.make_init_request()
redirect = urlparse(resp["Location"])

assert resp.status_code == 302
self.assert_vsts_oauth_redirect(redirect)
if new:
self.assert_vsts_new_oauth_redirect(redirect)
else:
self.assert_vsts_oauth_redirect(redirect)

query = parse_qs(redirect.query)

Expand Down
2 changes: 1 addition & 1 deletion migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ hybridcloud: 0016_add_control_cacheversion
nodestore: 0002_nodestore_no_dictfield
remote_subscriptions: 0003_drop_remote_subscription
replays: 0004_index_together
sentry: 0763_add_created_by_to_broadcasts
sentry: 0764_migrate_bad_status_substatus_rows
social_auth: 0002_default_auto_field
uptime: 0013_uptime_subscription_new_unique
workflow_engine: 0005_data_source_detector
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,7 @@ module = [
"sentry.db.models.manager.*",
"sentry.db.models.paranoia",
"sentry.db.models.utils",
"sentry.deletions",
"sentry.deletions.tasks.groups",
"sentry.deletions.*",
"sentry.digests.notifications",
"sentry.eventstore.reprocessing.redis",
"sentry.eventtypes.error",
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ s3transfer==0.10.0
selenium==4.16.0
sentry-arroyo==2.16.5
sentry-cli==2.16.0
sentry-devenv==1.10.0
sentry-devenv==1.10.2
sentry-forked-django-stubs==5.0.4.post2
sentry-forked-djangorestframework-stubs==3.15.1.post1
sentry-kafka-schemas==0.1.109
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
--index-url https://pypi.devinfra.sentry.io/simple

sentry-devenv>=1.10.0
sentry-devenv>=1.10.2

covdefaults>=2.3.0
docker>=6
Expand Down
8 changes: 0 additions & 8 deletions src/sentry/api/endpoints/organization_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,6 @@ class EventsApiResponse(TypedDict):
meta: EventsMeta


# When calling make build-spectacular-docs we hit this issue
# https://github.com/tfranzel/drf-spectacular/issues/1041
# This is a work around
EventsMeta.__annotations__["datasetReason"] = str
EventsMeta.__annotations__["isMetricsData"] = bool
EventsMeta.__annotations__["isMetricsExtractedData"] = bool


def rate_limit_events(
request: Request, organization_id_or_slug: str | None = None, *args, **kwargs
) -> dict[str, dict[RateLimitCategory, RateLimit]]:
Expand Down
98 changes: 98 additions & 0 deletions src/sentry/api/endpoints/organization_events_anomalies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from drf_spectacular.utils import extend_schema
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.organization_events import OrganizationEventsV2EndpointBase
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.paginator import OffsetPaginator
from sentry.api.serializers.base import serialize
from sentry.apidocs.constants import (
RESPONSE_BAD_REQUEST,
RESPONSE_FORBIDDEN,
RESPONSE_NOT_FOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.examples.organization_examples import OrganizationExamples
from sentry.apidocs.parameters import GlobalParams
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models.organization import Organization
from sentry.seer.anomaly_detection.get_historical_anomalies import (
get_historical_anomaly_data_from_seer_preview,
)
from sentry.seer.anomaly_detection.types import DetectAnomaliesResponse, TimeSeriesPoint


@region_silo_endpoint
class OrganizationEventsAnomaliesEndpoint(OrganizationEventsV2EndpointBase):
owner = ApiOwner.ALERTS_NOTIFICATIONS
publish_status = {
"POST": ApiPublishStatus.EXPERIMENTAL,
}

@extend_schema(
operation_id="Identify anomalies in historical data",
parameters=[GlobalParams.ORG_ID_OR_SLUG],
responses={
200: inline_sentry_response_serializer(
"ListAlertRuleAnomalies", DetectAnomaliesResponse
),
400: RESPONSE_BAD_REQUEST,
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOT_FOUND,
},
examples=OrganizationExamples.GET_HISTORICAL_ANOMALIES,
)
def _format_historical_data(self, data) -> list[TimeSeriesPoint] | None:
"""
Format EventsStatsData into the format that the Seer API expects.
EventsStatsData is a list of lists with this format:
[epoch timestamp, {'count': count}]
Convert the data to this format:
list[TimeSeriesPoint]
"""
if data is None:
return data

formatted_data: list[TimeSeriesPoint] = []
for datum in data:
ts_point = TimeSeriesPoint(timestamp=datum[0], value=datum[1].get("count", 0))
formatted_data.append(ts_point)
return formatted_data

def post(self, request: Request, organization: Organization) -> Response:
"""
Return a list of anomalies for a time series of historical event data.
"""
if not features.has("organizations:anomaly-detection-alerts", organization):
raise ResourceDoesNotExist("Your organization does not have access to this feature.")

historical_data = self._format_historical_data(request.data.get("historical_data"))
current_data = self._format_historical_data(request.data.get("current_data"))

config = request.data.get("config")
project_id = request.data.get("project_id")

if project_id is None or not config or not historical_data or not current_data:
return Response(
"Unable to get historical anomaly data: missing required argument(s) project, start, and/or end",
status=400,
)

anomalies = get_historical_anomaly_data_from_seer_preview(
current_data, historical_data, project_id, config
)
# NOTE: returns None if there's a problem with the Seer response
if anomalies is None:
return Response("Unable to get historical anomaly data", status=400)
# NOTE: returns empty list if there is not enough event data
return self.paginate(
request=request,
queryset=anomalies,
paginator_cls=OffsetPaginator,
on_results=lambda x: serialize(x, request.user),
)
3 changes: 2 additions & 1 deletion src/sentry/api/endpoints/organization_member/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def _get_member(
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOT_FOUND,
},
examples=OrganizationExamples.UPDATE_ORG_MEMBER,
)
def get(
self,
Expand All @@ -136,7 +137,7 @@ def get(
"""
Retrieve an organization member's details.
Will return a pending invite as long as it's already approved.
Response will be a pending invite if it has been approved by organization owners or managers but is waiting to be accepted by the invitee.
"""
allowed_roles = get_allowed_org_roles(request, organization, member)
return Response(
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/organization_member/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def get(self, request: Request, organization) -> Response:
"""
List all organization members.
Response includes pending invites that are approved by organization admins but waiting to be accepted by the invitee.
Response includes pending invites that are approved by organization owners or managers but waiting to be accepted by the invitee.
"""
queryset = OrganizationMember.objects.filter(
Q(user_is_active=True, user_id__isnull=False) | Q(user_id__isnull=True),
Expand Down
9 changes: 7 additions & 2 deletions src/sentry/api/endpoints/organization_release_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from rest_framework.response import Response
from rest_framework.serializers import ListField

from sentry import release_health
from sentry import options, release_health
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import ReleaseAnalyticsMixin, region_silo_endpoint
Expand Down Expand Up @@ -526,7 +526,12 @@ def put(self, request: Request, organization, version) -> Response:
datetime=release.date_released,
)

return Response(serialize(release, request.user))
no_snuba_for_release_creation = options.get("releases.no_snuba_for_release_creation")
return Response(
serialize(
release, request.user, no_snuba_for_release_creation=no_snuba_for_release_creation
)
)

@extend_schema(
operation_id="Delete an Organization's Release",
Expand Down
4 changes: 3 additions & 1 deletion src/sentry/api/endpoints/organization_releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ def post(self, request: Request, organization) -> Response:
update_org_auth_token_last_used(request.auth, [project.id for project in projects])

scope.set_tag("success_status", status)
return Response(serialize(release, request.user), status=status)
return Response(
serialize(release, request.user, no_snuba_for_release_creation=True), status=status
)
scope.set_tag("failure_reason", "serializer_error")
return Response(serializer.errors, status=400)

Expand Down
9 changes: 7 additions & 2 deletions src/sentry/api/endpoints/project_release_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import options
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import ReleaseAnalyticsMixin, region_silo_endpoint
from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission
Expand Down Expand Up @@ -144,8 +145,12 @@ def put(self, request: Request, project, version) -> Response:
data={"version": release.version},
datetime=release.date_released,
)

return Response(serialize(release, request.user))
no_snuba_for_release_creation = options.get("releases.no_snuba_for_release_creation")
return Response(
serialize(
release, request.user, no_snuba_for_release_creation=no_snuba_for_release_creation
)
)

def delete(self, request: Request, project, version) -> Response:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_servicehook_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.api.serializers import serialize
from sentry.api.validators import ServiceHookValidator
from sentry.constants import ObjectStatus
from sentry.models.servicehook import ServiceHook
from sentry.sentry_apps.models.servicehook import ServiceHook


@region_silo_endpoint
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_servicehook_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sentry.api.base import StatsMixin, region_silo_endpoint
from sentry.api.bases.project import ProjectEndpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.models.servicehook import ServiceHook
from sentry.sentry_apps.models.servicehook import ServiceHook
from sentry.tsdb.base import TSDBModel


Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_servicehooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.api.serializers import serialize
from sentry.api.validators import ServiceHookValidator
from sentry.constants import ObjectStatus
from sentry.models.servicehook import ServiceHook
from sentry.sentry_apps.models.servicehook import ServiceHook
from sentry.sentry_apps.services.hook import hook_service


Expand Down
10 changes: 9 additions & 1 deletion src/sentry/api/event_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,15 @@
key = ~r"[a-zA-Z0-9_.-]+"
quoted_key = '"' ~r"[a-zA-Z0-9_.:-]+" '"'
explicit_tag_key = "tags" open_bracket search_key closed_bracket
explicit_typed_tag_key = "tags" open_bracket search_key spaces comma spaces search_type closed_bracket
aggregate_key = key open_paren spaces function_args? spaces closed_paren
function_args = aggregate_param (spaces comma spaces !comma aggregate_param?)*
aggregate_param = quoted_aggregate_param / raw_aggregate_param
raw_aggregate_param = ~r"[^()\t\n, \"]+"
quoted_aggregate_param = '"' ('\\"' / ~r'[^\t\n\"]')* '"'
search_key = key / quoted_key
text_key = explicit_tag_key / search_key
search_type = "number" / "string"
text_key = explicit_tag_key / explicit_typed_tag_key / search_key
value = ~r"[^()\t\n ]*"
quoted_value = '"' ('\\"' / ~r'[^"]')* '"'
in_value = (&in_value_termination in_value_char)+
Expand Down Expand Up @@ -1046,6 +1048,12 @@ def visit_quoted_key(self, node, children):
def visit_explicit_tag_key(self, node, children):
return SearchKey(f"tags[{children[2].name}]")

def visit_explicit_typed_tag_key(self, node, children):
return SearchKey(f"tags[{children[2].name},{children[6]}]")

def visit_search_type(self, node, children):
return node.text

def visit_aggregate_key(self, node, children):
children = remove_optional_nodes(children)
children = remove_space(children)
Expand Down
8 changes: 5 additions & 3 deletions src/sentry/api/serializers/models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ def __init__(self, environment_func=None):
def get_attrs(self, item_list, user, **kwargs):
# TODO(dcramer); assert on relations
user_ids = [i.user_id for i in item_list if i.user_id]
user_list = user_service.serialize_many(
filter={"user_ids": user_ids}, as_user=serialize_generic_user(user)
)
user_list = []
if user_ids:
user_list = user_service.serialize_many(
filter={"user_ids": user_ids}, as_user=serialize_generic_user(user)
)
users = {u["id"]: u for u in user_list}

commit_ids = {
Expand Down
9 changes: 6 additions & 3 deletions src/sentry/api/serializers/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,12 @@ def _serialize_assignees(self, item_list: Sequence[Group]) -> Mapping[int, Team
for team in Team.objects.filter(id__in=all_team_ids.keys()):
for group_id in all_team_ids[team.id]:
result[group_id] = team
for user in user_service.get_many_by_id(ids=list(all_user_ids.keys())):
for group_id in all_user_ids[user.id]:
result[group_id] = user

user_ids = list(all_user_ids.keys())
if user_ids:
for user in user_service.get_many_by_id(ids=user_ids):
for group_id in all_user_ids[user.id]:
result[group_id] = user

return result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class OrganizationMemberSCIMSerializerResponse(OrganizationMemberSCIMSerializerO

class _TeamRole(TypedDict):
teamSlug: str
role: str
role: str | None


@extend_schema_serializer(exclude_fields=["role", "roleName"])
Expand Down
Loading

0 comments on commit 77bff75

Please sign in to comment.