Просмотр исходного кода

feat(codecov): Fetch code coverage for stacktrace (#43116)

Fixes WOR-2550

Co-authored-by: Snigdha Sharma <snigdha.sharma@sentry.io>
Co-authored-by: @armenzg <armenzg@sentry.io>
Jodi Jang 2 лет назад
Родитель
Сommit
3d2231dc40

+ 1 - 0
mypy.ini

@@ -83,6 +83,7 @@ files = fixtures/mypy-stubs,
         src/sentry/integrations/message_builder.py,
         src/sentry/integrations/utils/atlassian_connect.py,
         src/sentry/integrations/utils/code_mapping.py,
+        src/sentry/integrations/utils/codecov.py,
         src/sentry/integrations/vsts/,
         src/sentry/issues/,
         src/sentry/killswitches.py,

+ 33 - 1
src/sentry/api/endpoints/project_stacktrace_link.py

@@ -1,15 +1,17 @@
 import logging
 from typing import Dict, List, Mapping, Optional
 
+import requests
 from rest_framework.request import Request
 from rest_framework.response import Response
 from sentry_sdk import Scope, configure_scope
 
-from sentry import analytics
+from sentry import analytics, features
 from sentry.api.base import region_silo_endpoint
 from sentry.api.bases.project import ProjectEndpoint
 from sentry.api.serializers import serialize
 from sentry.integrations import IntegrationFeatures
+from sentry.integrations.utils.codecov import CODECOV_TOKEN, get_codecov_line_coverage
 from sentry.models import Integration, Project, RepositoryProjectPathConfig
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.utils.event_frames import munged_filename_and_frames
@@ -47,6 +49,7 @@ def get_link(
         result["attemptedUrl"] = install.format_source_url(
             config.repository, formatted_path, config.default_branch
         )
+    result["sourcePath"] = formatted_path
 
     return result
 
@@ -258,6 +261,35 @@ class ProjectStacktraceLinkEndpoint(ProjectEndpoint):  # type: ignore
                     # When no code mapping have been matched we have not attempted a URL
                     if current_config["outcome"].get("attemptedUrl"):
                         result["attemptedUrl"] = current_config["outcome"]["attemptedUrl"]
+
+                should_get_codecov_line_coverage = (
+                    features.has(
+                        "organizations:codecov-stacktrace-integration",
+                        project.organization,
+                        actor=request.user,
+                    )
+                    and project.organization.flags.codecov_access
+                    and CODECOV_TOKEN
+                )
+                if should_get_codecov_line_coverage:
+                    try:
+                        result["lineCoverage"] = get_codecov_line_coverage(
+                            repo=current_config["config"]["repoName"],
+                            service=current_config["config"]["provider"]["key"],
+                            branch=current_config["config"]["defaultBranch"],
+                            path=current_config["outcome"]["sourcePath"],
+                        )
+                        if result["lineCoverage"]:
+                            result["codecovStatusCode"] = 200
+                    except requests.exceptions.HTTPError as error:
+                        result["codecovStatusCode"] = error.response.status_code
+                        if error.response.status_code != 404:
+                            logger.exception(
+                                "Failed to get expected coverage data from Codecov, pending investigation. Continuing execution."
+                            )
+                    except Exception:
+                        logger.exception("Something unexpected happen. Continuing execution.")
+
             try:
                 set_tags(scope, result)
             except Exception:

+ 22 - 0
src/sentry/integrations/utils/codecov.py

@@ -0,0 +1,22 @@
+from typing import Any
+
+import requests
+
+from sentry import options
+
+CODECOV_URL = "https://api.codecov.io/api/v2/{service}/{owner_username}/repos/{repo_name}/report"
+CODECOV_TOKEN = options.get("codecov.client-secret")
+
+
+def get_codecov_line_coverage(repo: str, service: str, branch: str, path: str) -> Any:
+    owner_username, repo_name = repo.split("/")
+    if service == "github":
+        service = "gh"
+    url = CODECOV_URL.format(service=service, owner_username=owner_username, repo_name=repo_name)
+    params = {"branch": branch, "path": path}
+    response = requests.get(
+        url, params=params, headers={"Authorization": f"tokenAuth {CODECOV_TOKEN}"}
+    )
+    response.raise_for_status()
+
+    return response.json()["files"][0]["line_coverage"]

+ 3 - 0
src/sentry/options/defaults.py

@@ -199,6 +199,9 @@ register("slack.client-secret", flags=FLAG_CREDENTIAL | FLAG_PRIORITIZE_DISK)
 register("slack.verification-token", flags=FLAG_CREDENTIAL | FLAG_PRIORITIZE_DISK)
 register("slack.signing-secret", flags=FLAG_CREDENTIAL | FLAG_PRIORITIZE_DISK)
 
+# Codecov Integration
+register("codecov.client-secret", flags=FLAG_CREDENTIAL | FLAG_PRIORITIZE_DISK)
+
 # GitHub Integration
 register("github-app.id", default=0)
 register("github-app.name", default="")

+ 87 - 1
tests/sentry/api/endpoints/test_project_stacktrace_link.py

@@ -1,10 +1,15 @@
+import logging
 from typing import Any, Mapping
 from unittest import mock
+from unittest.mock import MagicMock
+
+import pytest
 
 from sentry.api.endpoints.project_stacktrace_link import ProjectStacktraceLinkEndpoint
 from sentry.integrations.example.integration import ExampleIntegration
 from sentry.models import Integration, OrganizationIntegration
 from sentry.testutils import APITestCase
+from sentry.testutils.helpers.features import with_feature
 from sentry.testutils.silo import region_silo_test
 
 example_base_url = "https://example.com/getsentry/sentry/blob/master"
@@ -299,6 +304,87 @@ class ProjectStacktraceLinkTestMobile(BaseProjectStacktraceLink):
         assert response.data["sourceUrl"] == f"{example_base_url}/{file_path}"
 
 
+class ProjectStracktraceLinkTestCodecov(BaseProjectStacktraceLink):
+    def setUp(self):
+        BaseProjectStacktraceLink.setUp(self)
+        self.code_mapping1 = self.create_code_mapping(
+            organization_integration=self.oi,
+            project=self.project,
+            repo=self.repo,
+            stack_root="",
+            source_root="",
+        )
+        self.filepath = "src/path/to/file.py"
+
+    @pytest.fixture(autouse=True)
+    def inject_fixtures(self, caplog):
+        self._caplog = caplog
+
+    @with_feature("organizations:codecov-stacktrace-integration")
+    @mock.patch("sentry.api.endpoints.project_stacktrace_link.get_codecov_line_coverage")
+    @mock.patch("sentry.api.endpoints.project_stacktrace_link.CODECOV_TOKEN")
+    @mock.patch.object(ExampleIntegration, "get_stacktrace_link")
+    def test_codecov_line_coverage_success(
+        self, mock_integration, mock_token, mock_get_codecov_line_coverage
+    ):
+        self.organization.flags.codecov_access = True
+        self.organization.save()
+
+        expected_line_coverage = [[1, 0], [3, 1], [4, 0]]
+        expected_status_code = 200
+        mock_integration.return_value = "https://github.com/repo/blob/master/src/path/to/file.py"
+        mock_get_codecov_line_coverage.return_value = expected_line_coverage
+        mock_token.return_value = "value"
+        response = self.get_success_response(
+            self.organization.slug,
+            self.project.slug,
+            qs_params={
+                "file": self.filepath,
+                "absPath": "abs_path",
+                "module": "module",
+                "package": "package",
+            },
+        )
+        assert response.data["lineCoverage"] == expected_line_coverage
+        assert response.data["codecovStatusCode"] == expected_status_code
+
+    @with_feature("organizations:codecov-stacktrace-integration")
+    @mock.patch("sentry.api.endpoints.project_stacktrace_link.CODECOV_TOKEN", return_value="value")
+    @mock.patch("sentry.api.endpoints.project_stacktrace_link.get_codecov_line_coverage")
+    # @mock.patch("sentry.integrations.utils.codecov.get_codecov_line_coverage")
+    @mock.patch.object(ExampleIntegration, "get_stacktrace_link")
+    def test_codecov_line_coverage_exception(
+        self, mock_integration, mock_get_codecov_line_coverage, mock_token
+    ):
+        self._caplog.set_level(logging.ERROR, logger="sentry")
+        self.organization.flags.codecov_access = True
+        self.organization.save()
+
+        mock_integration.return_value = "https://github.com/repo/blob/master/src/path/to/file.py"
+        mock_exception = MagicMock(side_effect=Exception)
+        mock_exception.status = 400
+        mock_get_codecov_line_coverage.side_effect = mock_exception
+
+        self.get_success_response(
+            self.organization.slug,
+            self.project.slug,
+            qs_params={
+                "file": self.filepath,
+                "absPath": "abs_path",
+                "module": "module",
+                "package": "package",
+            },
+        )
+
+        assert self._caplog.record_tuples == [
+            (
+                "sentry.api.endpoints.project_stacktrace_link",
+                logging.ERROR,
+                "Something unexpected happen. Continuing execution.",
+            )
+        ]
+
+
 class ProjectStacktraceLinkTestMultipleMatches(BaseProjectStacktraceLink):
     def setUp(self):
         BaseProjectStacktraceLink.setUp(self)
@@ -357,7 +443,7 @@ class ProjectStacktraceLinkTestMultipleMatches(BaseProjectStacktraceLink):
 
         self.filepath = "usr/src/getsentry/src/sentry/src/sentry/utils/safe.py"
 
-    def test_test_multiple_code_mapping_matches_order(self):
+    def test_multiple_code_mapping_matches_order(self):
         project_stacktrace_link_endpoint = ProjectStacktraceLinkEndpoint()
 
         configs = self.code_mappings