Skip to content

Commit 480efb7

Browse files
authored
feat(insights): add endpoint to create/delete starred transactions (#89330)
Follow up to #89203 In insights overview pages, we want individual users to be able to star their own transactions. This PR creates an endpoint to allow users to do so
1 parent a88583c commit 480efb7

File tree

5 files changed

+167
-0
lines changed

5 files changed

+167
-0
lines changed

src/sentry/api/urls.py

+7
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
TeamAlertsTriggeredIndexEndpoint,
123123
TeamAlertsTriggeredTotalsEndpoint,
124124
)
125+
from sentry.insights.endpoints.starred_segments import InsightsStarredSegmentsEndpoint
125126
from sentry.integrations.api.endpoints.doc_integration_avatar import DocIntegrationAvatarEndpoint
126127
from sentry.integrations.api.endpoints.doc_integration_details import DocIntegrationDetailsEndpoint
127128
from sentry.integrations.api.endpoints.doc_integrations_index import DocIntegrationsEndpoint
@@ -1325,6 +1326,12 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
13251326
ProjectTransactionThresholdOverrideEndpoint.as_view(),
13261327
name="sentry-api-0-organization-project-transaction-threshold-override",
13271328
),
1329+
# Insights
1330+
re_path(
1331+
r"^(?P<organization_id_or_slug>[^\/]+)/insights/starred-segments/$",
1332+
InsightsStarredSegmentsEndpoint.as_view(),
1333+
name="sentry-api-0-insights-starred-segments",
1334+
),
13281335
# Explore
13291336
re_path(
13301337
r"^(?P<organization_id_or_slug>[^\/]+)/explore/saved/$",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from django.db import router
2+
from rest_framework import serializers, status
3+
from rest_framework.request import Request
4+
from rest_framework.response import Response
5+
6+
from sentry import features
7+
from sentry.api.api_owners import ApiOwner
8+
from sentry.api.api_publish_status import ApiPublishStatus
9+
from sentry.api.base import region_silo_endpoint
10+
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
11+
from sentry.insights.models import InsightsStarredSegment
12+
from sentry.models.organization import Organization
13+
from sentry.utils.db import atomic_transaction
14+
15+
16+
class StarSegmentSerializer(serializers.Serializer):
17+
segment_name = serializers.CharField(required=True)
18+
project_id = serializers.IntegerField(required=True)
19+
20+
21+
class MemberPermission(OrganizationPermission):
22+
scope_map = {
23+
"POST": ["member:read", "member:write"],
24+
"DELETE": ["member:read", "member:write"],
25+
}
26+
27+
28+
@region_silo_endpoint
29+
class InsightsStarredSegmentsEndpoint(OrganizationEndpoint):
30+
publish_status = {
31+
"POST": ApiPublishStatus.EXPERIMENTAL,
32+
"DELETE": ApiPublishStatus.EXPERIMENTAL,
33+
}
34+
owner = ApiOwner.PERFORMANCE
35+
permission_classes = (MemberPermission,)
36+
37+
def has_feature(self, organization, request):
38+
return features.has(
39+
"organizations:insights-modules-use-eap", organization, actor=request.user
40+
)
41+
42+
def post(self, request: Request, organization: Organization) -> Response:
43+
"""
44+
Star a segment for the current organization member.
45+
"""
46+
if not self.has_feature(organization, request):
47+
return self.respond(status=404)
48+
49+
serializer = StarSegmentSerializer(data=request.data)
50+
if not serializer.is_valid():
51+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
52+
53+
segment_name = serializer.validated_data["segment_name"]
54+
project_id = serializer.validated_data["project_id"]
55+
with atomic_transaction(using=router.db_for_write(InsightsStarredSegment)):
56+
_, created = InsightsStarredSegment.objects.get_or_create(
57+
organization=organization,
58+
project_id=project_id,
59+
user_id=request.user.id,
60+
segment_name=segment_name,
61+
)
62+
63+
if not created:
64+
return Response(status=status.HTTP_403_FORBIDDEN)
65+
66+
return Response(status=status.HTTP_200_OK)
67+
68+
def delete(self, request: Request, organization: Organization) -> Response:
69+
"""
70+
Delete a starred segment for the current organization member.
71+
"""
72+
if not self.has_feature(organization, request):
73+
return self.respond(status=404)
74+
75+
serializer = StarSegmentSerializer(data=request.data)
76+
if not serializer.is_valid():
77+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
78+
79+
segment_name = serializer.validated_data["segment_name"]
80+
project_id = serializer.validated_data["project_id"]
81+
82+
InsightsStarredSegment.objects.filter(
83+
organization=organization,
84+
user_id=request.user.id,
85+
project_id=project_id,
86+
segment_name=segment_name,
87+
).delete()
88+
89+
return Response(status=status.HTTP_200_OK)

tests/sentry/insights/__init__.py

Whitespace-only changes.

tests/sentry/insights/endpoints/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from django.urls import reverse
2+
3+
from sentry.insights.models import InsightsStarredSegment
4+
from sentry.testutils.cases import APITestCase, SnubaTestCase
5+
6+
7+
class InsightsStarredSegmentTest(APITestCase, SnubaTestCase):
8+
feature_name = "organizations:insights-modules-use-eap"
9+
10+
def setUp(self):
11+
super().setUp()
12+
self.login_as(user=self.user)
13+
self.org = self.create_organization(owner=self.user)
14+
self.project_ids = [
15+
self.create_project(organization=self.org).id,
16+
self.create_project(organization=self.org).id,
17+
]
18+
19+
self.url = reverse(
20+
"sentry-api-0-insights-starred-segments",
21+
kwargs={"organization_id_or_slug": self.org.slug},
22+
)
23+
24+
def test_post_and_delete(self):
25+
with self.feature(self.feature_name):
26+
segment_name = "my_segment"
27+
28+
assert not InsightsStarredSegment.objects.filter(
29+
segment_name=segment_name,
30+
).exists()
31+
32+
response = self.client.post(
33+
self.url, data={"segment_name": segment_name, "project_id": self.project_ids[0]}
34+
)
35+
assert response.status_code == 200, response.content
36+
37+
assert InsightsStarredSegment.objects.filter(
38+
segment_name=segment_name,
39+
).exists()
40+
41+
response = self.client.delete(
42+
self.url, data={"segment_name": segment_name, "project_id": self.project_ids[0]}
43+
)
44+
assert response.status_code == 200, response.content
45+
46+
assert not InsightsStarredSegment.objects.filter(
47+
segment_name=segment_name,
48+
).exists()
49+
50+
def test_no_error_deleting_non_existent_segment(self):
51+
with self.feature(self.feature_name):
52+
response = self.client.delete(
53+
self.url,
54+
data={"segment_name": "non_existent_segment", "project_id": self.project_ids[0]},
55+
)
56+
assert response.status_code == 200, response.content
57+
58+
def test_error_creating_duplicate_segment(self):
59+
with self.feature(self.feature_name):
60+
segment_name = "my_segment"
61+
InsightsStarredSegment.objects.create(
62+
segment_name=segment_name,
63+
project_id=self.project_ids[0],
64+
organization=self.org,
65+
user_id=self.user.id,
66+
)
67+
68+
response = self.client.post(
69+
self.url, data={"segment_name": segment_name, "project_id": self.project_ids[0]}
70+
)
71+
assert response.status_code == 403

0 commit comments

Comments
 (0)