-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
226 additions
and
9 deletions.
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
src/sentry/api/endpoints/organization_pull_request_file_issues.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from __future__ import annotations | ||
|
||
from rest_framework import serializers, status | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
|
||
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 import OrganizationEndpoint | ||
from sentry.models.group import Group | ||
from sentry.models.organization import Organization | ||
from sentry.snuba.referrer import Referrer | ||
from sentry.tasks.integrations.github.language_parsers import PATCH_PARSERS | ||
from sentry.tasks.integrations.github.open_pr_comment import ( | ||
get_projects_and_filenames_from_source_file, | ||
get_top_5_issues_by_count_for_file, | ||
) | ||
|
||
|
||
class PullRequestFileSerializer(serializers.Serializer): | ||
filename = serializers.CharField(required=True) | ||
repo = serializers.CharField(required=True) | ||
patch = serializers.CharField(required=True) | ||
|
||
def validate_filename(self, value): | ||
if not value: | ||
raise serializers.ValidationError("Filename is required") | ||
|
||
file_extension = value.split(".")[-1] | ||
language_parser = PATCH_PARSERS.get(file_extension, None) | ||
if not language_parser: | ||
raise serializers.ValidationError("Unsupported file type") | ||
|
||
return value | ||
|
||
|
||
@region_silo_endpoint | ||
class OrganizationPullRequestFilesIssuesEndpoint(OrganizationEndpoint): | ||
owner = ApiOwner.ECOSYSTEM | ||
publish_status = { | ||
"GET": ApiPublishStatus.EXPERIMENTAL, | ||
} | ||
|
||
def post(self, request: Request, organization: Organization) -> Response: | ||
serializer = PullRequestFileSerializer(data=request.data) | ||
if not serializer.is_valid(): | ||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||
|
||
filename = serializer.validated_data["filename"] | ||
repo_name = serializer.validated_data["repo"] | ||
patch = serializer.validated_data["patch"] | ||
|
||
projects, sentry_filenames = get_projects_and_filenames_from_source_file( | ||
org_id=organization.id, repo_name=repo_name, pr_filename=filename | ||
) | ||
|
||
if not len(projects) or not len(sentry_filenames): | ||
return Response([]) | ||
|
||
file_extension = filename.split(".")[-1] | ||
language_parser = PATCH_PARSERS[file_extension] | ||
|
||
function_names = language_parser.extract_functions_from_patch(patch) | ||
|
||
if not len(function_names): | ||
return Response([]) | ||
|
||
top_issues = get_top_5_issues_by_count_for_file( | ||
list(projects), list(sentry_filenames), list(function_names) | ||
) | ||
|
||
group_id_to_info = {} | ||
for issue in top_issues: | ||
group_id = issue["group_id"] | ||
group_id_to_info[group_id] = dict(filter(lambda k: k[0] != "group_id", issue.items())) | ||
|
||
issues = Group.objects.filter(id__in=list(group_id_to_info.keys())).all() | ||
|
||
pr_file_issues = [ | ||
{ | ||
"title": issue.title, | ||
"culprit": issue.culprit, | ||
"url": issue.get_absolute_url(), | ||
"affected_users": issue.count_users_seen( | ||
referrer=Referrer.TAGSTORE_GET_GROUPS_USER_COUNTS_OPEN_PR_COMMENT.value | ||
), | ||
"event_count": group_id_to_info[issue.id]["event_count"], | ||
"function_name": group_id_to_info[issue.id]["function_name"], | ||
} | ||
for issue in issues | ||
] | ||
pr_file_issues.sort(key=lambda k: k.get("event_count", 0), reverse=True) | ||
|
||
return Response(pr_file_issues) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
tests/sentry/api/endpoints/test_organization_pull_request_file_issues.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
from unittest.mock import patch | ||
|
||
from sentry.models.group import Group | ||
from sentry.testutils.cases import APITestCase | ||
from tests.sentry.tasks.integrations.github.test_open_pr_comment import CreateEventTestCase | ||
|
||
|
||
@patch( | ||
"sentry.api.endpoints.organization_pull_request_file_issues.get_projects_and_filenames_from_source_file" | ||
) | ||
class OrganizationPullRequestFileIssuesTest(APITestCase, CreateEventTestCase): | ||
endpoint = "sentry-api-0-organization-pr-file-issues" | ||
method = "post" | ||
|
||
def setUp(self): | ||
super().setUp() | ||
self.user_id = "user_1" | ||
self.app_id = "app_1" | ||
|
||
self.group_id_1 = [ | ||
self._create_event( | ||
culprit="issue1", | ||
user_id=str(i), | ||
filenames=["bar.py", "foo.py"], | ||
function_names=["planet", "blue"], | ||
) | ||
for i in range(5) | ||
][0].group.id | ||
self.group_id_2 = [ | ||
self._create_event( | ||
culprit="issue2", | ||
filenames=["foo.py", "bar.py"], | ||
function_names=["blue", "planet"], | ||
user_id=str(i), | ||
) | ||
for i in range(6) | ||
][0].group.id | ||
|
||
self.gh_repo = self.create_repo( | ||
name="getsentry/sentry", | ||
provider="integrations:github", | ||
integration_id=self.integration.id, | ||
project=self.project, | ||
url="https://github.com/getsentry/sentry", | ||
) | ||
self.groups = [ | ||
{ | ||
"group_id": g.id, | ||
"event_count": 1000 * (i + 1), | ||
"function_name": "function_" + str(i), | ||
} | ||
for i, g in enumerate(Group.objects.all()) | ||
] | ||
self.groups.reverse() | ||
|
||
self.login_as(self.user) | ||
|
||
def test_simple(self, mock_reverse_codemappings): | ||
mock_reverse_codemappings.return_value = ([self.project], ["foo.py"]) | ||
|
||
patch = """@@ -36,6 +36,7 @@\n def blue(self):""" | ||
response = self.get_success_response( | ||
self.organization.slug, | ||
**{"filename": "foo.py", "repo": self.gh_repo.name, "patch": patch}, | ||
) | ||
group_urls = [g["url"] for g in response.data] | ||
assert group_urls == [ | ||
f"http://testserver/organizations/{self.organization.slug}/issues/{self.group_id_2}/", | ||
f"http://testserver/organizations/{self.organization.slug}/issues/{self.group_id_1}/", | ||
] | ||
|
||
def test_no_codemappings(self, mock_reverse_codemappings): | ||
mock_reverse_codemappings.return_value = ([], []) | ||
|
||
patch = """@@ -36,6 +36,7 @@\n def blue(self):""" | ||
response = self.get_success_response( | ||
self.organization.slug, | ||
**{"filename": "foo.py", "repo": self.gh_repo.name, "patch": patch}, | ||
) | ||
assert response.data == [] | ||
|
||
def test_no_functions(self, mock_reverse_codemappings): | ||
mock_reverse_codemappings.return_value = ([self.project], ["foo.py"]) | ||
|
||
patch = """@@ -36,6 +36,7 @@\n import pytest""" | ||
response = self.get_success_response( | ||
self.organization.slug, | ||
**{"filename": "foo.py", "repo": self.gh_repo.name, "patch": patch}, | ||
) | ||
assert response.data == [] | ||
|
||
def test_no_issues(self, mock_reverse_codemappings): | ||
mock_reverse_codemappings.return_value = ([self.project], ["foo.py"]) | ||
|
||
patch = """@@ -36,6 +36,7 @@\n def purple(self):""" | ||
response = self.get_success_response( | ||
self.organization.slug, | ||
**{"filename": "bar.py", "repo": self.gh_repo.name, "patch": patch}, | ||
) | ||
assert response.data == [] |