Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(unmerge): Add unmerge PUT endpoint #79122

Merged
merged 4 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"/api/0/{var}/{issue_id}/events/{event_id}/": {"GET"},
"/api/0/{var}/{issue_id}/{var}/": {"GET", "POST"},
"/api/0/{var}/{issue_id}/{var}/{note_id}/": {"DELETE", "PUT"},
"/api/0/{var}/{issue_id}/hashes/": {"GET", "DELETE"},
"/api/0/{var}/{issue_id}/hashes/": {"GET", "DELETE", "PUT"},
"/api/0/{var}/{issue_id}/reprocessing/": {"POST"},
"/api/0/{var}/{issue_id}/stats/": {"GET"},
"/api/0/{var}/{issue_id}/tags/": {"GET"},
Expand Down Expand Up @@ -82,6 +82,7 @@
"/api/0/organizations/{organization_id_or_slug}/{var}/{issue_id}/hashes/": {
"GET",
"DELETE",
"PUT",
},
"/api/0/organizations/{organization_id_or_slug}/{var}/{issue_id}/reprocessing/": {"POST"},
"/api/0/organizations/{organization_id_or_slug}/{var}/{issue_id}/stats/": {"GET"},
Expand Down
36 changes: 36 additions & 0 deletions src/sentry/issues/endpoints/group_hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@region_silo_endpoint
class GroupHashesEndpoint(GroupEndpoint):
publish_status = {
"PUT": ApiPublishStatus.PRIVATE,
"DELETE": ApiPublishStatus.PRIVATE,
"GET": ApiPublishStatus.PRIVATE,
}
Expand Down Expand Up @@ -91,6 +92,41 @@ def delete(self, request: Request, group) -> Response:

return Response(status=202)

def put(self, request: Request, group) -> Response:
"""
Perform an unmerge by reassigning events with hash values corresponding to the given
grouphash ids from being part of the given group to being part of a new group.

Note that if multiple grouphash ids are given, all their corresponding events will end up in
a single new group together, rather than each hash's events ending in their own new group.
"""
grouphash_ids = request.GET.getlist("id")
if not grouphash_ids:
return Response()

grouphashes = list(
GroupHash.objects.filter(
project_id=group.project_id, group=group.id, hash__in=grouphash_ids
)
.exclude(state=GroupHash.State.LOCKED_IN_MIGRATION)
.values_list("hash", flat=True)
)
if not grouphashes:
return Response({"detail": "Already being unmerged"}, status=409)

metrics.incr(
"grouping.unmerge_issues",
sample_rate=1.0,
# We assume that if someone's merged groups, they were all from the same platform
tags={"platform": group.platform or "unknown", "sdk": group.sdk or "unknown"},
)

unmerge.delay(
group.project_id, group.id, None, grouphashes, request.user.id if request.user else None
)

return Response(status=202)

def __handle_results(self, project_id, group_id, user, results):
return [self.__handle_result(user, project_id, group_id, result) for result in results]

Expand Down
57 changes: 57 additions & 0 deletions tests/sentry/issues/endpoints/test_group_hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,63 @@ def test_unmerge(self):
tags={"platform": "javascript", "sdk": "sentry.javascript.nextjs"},
)

def test_unmerge_delete_member(self):
member = self.create_user(is_superuser=False)
self.login_as(user=member)

group = self.create_group(
platform="javascript",
metadata={"sdk": {"name_normalized": "sentry.javascript.nextjs"}},
)

hashes = [
GroupHash.objects.create(project=group.project, group=group, hash=hash)
for hash in ["a" * 32, "b" * 32]
]

url = "?".join(
[
f"/api/0/issues/{group.id}/hashes/",
urlencode({"id": [h.hash for h in hashes]}, True),
]
)

response = self.client.delete(url, format="json")

assert response.status_code == 403, response.content

def test_unmerge_put_member(self):
member_user = self.create_user(is_superuser=False)
member = self.create_member(organization=self.organization, user=member_user, role="member")
self.login_as(user=member)

group = self.create_group(
platform="javascript",
metadata={"sdk": {"name_normalized": "sentry.javascript.nextjs"}},
)

hashes = [
GroupHash.objects.create(project=group.project, group=group, hash=hash)
for hash in ["a" * 32, "b" * 32]
]

url = "?".join(
[
f"/api/0/issues/{group.id}/hashes/",
urlencode({"id": [h.hash for h in hashes]}, True),
]
)

with patch("sentry.issues.endpoints.group_hashes.metrics.incr") as mock_metrics_incr:
response = self.client.put(url, format="json")

assert response.status_code == 202, response.content
mock_metrics_incr.assert_any_call(
"grouping.unmerge_issues",
sample_rate=1.0,
tags={"platform": "javascript", "sdk": "sentry.javascript.nextjs"},
)

def test_unmerge_conflict(self):
self.login_as(user=self.user)

Expand Down
Loading