Skip to content

Commit

Permalink
Stack trace linking for Bitbucket (#52952)
Browse files Browse the repository at this point in the history
This PR enables Stack trace linking for Bitbucket.

Depends on #53584
  • Loading branch information
jianyuan authored Sep 28, 2023
1 parent 2b05e4e commit a13a813
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 7 deletions.
13 changes: 13 additions & 0 deletions src/sentry/integrations/bitbucket/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from requests import PreparedRequest

from sentry.integrations.utils import get_query_hash
from sentry.models import Repository
from sentry.services.hybrid_cloud.integration.model import RpcIntegration
from sentry.services.hybrid_cloud.util import control_silo_function
from sentry.shared_integrations.client.base import BaseApiResponseX
from sentry.shared_integrations.client.proxy import IntegrationProxyClient, infer_org_integration
from sentry.utils import jwt
from sentry.utils.http import absolute_uri
Expand Down Expand Up @@ -42,6 +44,8 @@ class BitbucketAPIPath:
repository_hook = "/2.0/repositories/{repo}/hooks/{uid}"
repository_hooks = "/2.0/repositories/{repo}/hooks"

source = "/2.0/repositories/{repo}/src/{sha}/{path}"


class BitbucketApiClient(IntegrationProxyClient):
"""
Expand Down Expand Up @@ -172,3 +176,12 @@ def compare_commits(self, repo, start_sha, end_sha):
break

return self.zip_commit_data(repo, commits)

def check_file(self, repo: Repository, path: str, version: str) -> BaseApiResponseX:
return self.head_cached(
path=BitbucketAPIPath.source.format(
repo=repo.name,
sha=version,
path=path,
),
)
35 changes: 33 additions & 2 deletions src/sentry/integrations/bitbucket/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)
from sentry.integrations.mixins import RepositoryMixin
from sentry.integrations.utils import AtlassianConnectValidationError, get_integration_from_request
from sentry.models import Integration
from sentry.models import Integration, Repository
from sentry.pipeline import NestedPipelineView, PipelineView
from sentry.services.hybrid_cloud.organization import RpcOrganizationSummary
from sentry.services.hybrid_cloud.repository import RpcRepository, repository_service
Expand Down Expand Up @@ -60,6 +60,13 @@
""",
IntegrationFeatures.ISSUE_BASIC,
),
FeatureDescription(
"""
Link your Sentry stack traces back to your Bitbucket source code with stack
trace linking.
""",
IntegrationFeatures.STACKTRACE_LINK,
),
]

metadata = IntegrationMetadata(
Expand Down Expand Up @@ -136,14 +143,38 @@ def get_unmigratable_repositories(self) -> List[RpcRepository]:
def reinstall(self):
self.reinstall_repositories()

def source_url_matches(self, url: str) -> bool:
return url.startswith(f'https://{self.model.metadata["domain_name"]}') or url.startswith(
"https://bitbucket.org",
)

def format_source_url(self, repo: Repository, filepath: str, branch: str) -> str:
return f"https://bitbucket.org/{repo.name}/src/{branch}/{filepath}"

def extract_branch_from_source_url(self, repo: Repository, url: str) -> str:
url = url.replace(f"{repo.url}/src/", "")
branch, _, _ = url.partition("/")
return branch

def extract_source_path_from_source_url(self, repo: Repository, url: str) -> str:
url = url.replace(f"{repo.url}/src/", "")
_, _, source_path = url.partition("/")
return source_path


class BitbucketIntegrationProvider(IntegrationProvider):
key = "bitbucket"
name = "Bitbucket"
metadata = metadata
scopes = scopes
integration_cls = BitbucketIntegration
features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])
features = frozenset(
[
IntegrationFeatures.ISSUE_BASIC,
IntegrationFeatures.COMMITS,
IntegrationFeatures.STACKTRACE_LINK,
]
)

def get_pipeline_views(self):
identity_pipeline_config = {"redirect_url": absolute_uri("/extensions/bitbucket/setup/")}
Expand Down
65 changes: 63 additions & 2 deletions tests/sentry/integrations/bitbucket/test_client.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import re

import jwt
import pytest
import responses
from django.test import override_settings
from requests import Request

from sentry.integrations.bitbucket.client import BitbucketApiClient, BitbucketAPIPath
from sentry.integrations.bitbucket.integration import BitbucketIntegration
from sentry.integrations.utils.atlassian_connect import get_query_hash
from sentry.models import Repository
from sentry.shared_integrations.exceptions import ApiError
from sentry.shared_integrations.response.base import BaseApiResponse
from sentry.silo.base import SiloMode
from sentry.silo.util import PROXY_BASE_PATH, PROXY_OI_HEADER, PROXY_SIGNATURE_HEADER
from sentry.testutils.cases import BaseTestCase, TestCase
from sentry.testutils.helpers.datetime import freeze_time
from sentry.testutils.silo import control_silo_test
from sentry.testutils.silo import assume_test_silo_mode, control_silo_test

control_address = "http://controlserver"
secret = "hush-hush-im-invisible"
Expand Down Expand Up @@ -40,9 +45,20 @@ def setUp(self):
"type": "team",
},
)
self.install = self.integration.get_installation(self.organization.id)
install = self.integration.get_installation(self.organization.id)
assert isinstance(install, BitbucketIntegration)
self.install = install
self.bitbucket_client: BitbucketApiClient = self.install.get_client()

with assume_test_silo_mode(SiloMode.REGION):
self.repo = Repository.objects.create(
provider="bitbucket",
name="sentryuser/newsdiffs",
organization_id=self.organization.id,
config={"name": "sentryuser/newsdiffs"},
integration_id=self.integration.id,
)

@freeze_time("2023-01-01 01:01:01")
def test_authorize_request(self):
method = "GET"
Expand All @@ -69,6 +85,51 @@ def test_authorize_request(self):
"sub": self.integration.external_id,
}

@responses.activate
def test_check_file(self):
path = "src/sentry/integrations/bitbucket/client.py"
version = "master"
url = f"https://api.bitbucket.org/2.0/repositories/{self.repo.name}/src/{version}/{path}"

responses.add(
method=responses.HEAD,
url=url,
json={"text": 200},
)

resp = self.bitbucket_client.check_file(self.repo, path, version)
assert isinstance(resp, BaseApiResponse)
assert resp.status_code == 200

@responses.activate
def test_check_no_file(self):
path = "src/santry/integrations/bitbucket/client.py"
version = "master"
url = f"https://api.bitbucket.org/2.0/repositories/{self.repo.name}/src/{version}/{path}"

responses.add(method=responses.HEAD, url=url, status=404)

with pytest.raises(ApiError):
self.bitbucket_client.check_file(self.repo, path, version)

@responses.activate
def test_get_stacktrace_link(self):
path = "/src/sentry/integrations/bitbucket/client.py"
version = "master"
url = f"https://api.bitbucket.org/2.0/repositories/{self.repo.name}/src/{version}/{path.lstrip('/')}"

responses.add(
method=responses.HEAD,
url=url,
json={"text": 200},
)

source_url = self.install.get_stacktrace_link(self.repo, path, "master", version)
assert (
source_url
== "https://bitbucket.org/sentryuser/newsdiffs/src/master/src/sentry/integrations/bitbucket/client.py"
)

@responses.activate
def test_integration_proxy_is_active(self):
class BitbucketApiProxyTestClient(BitbucketApiClient):
Expand Down
70 changes: 67 additions & 3 deletions tests/sentry/integrations/bitbucket/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
import responses
from django.urls import reverse

from sentry.models import Integration
from sentry.integrations.bitbucket import BitbucketIntegrationProvider
from sentry.models import Integration, Repository
from sentry.silo.base import SiloMode
from sentry.testutils.cases import APITestCase
from sentry.testutils.silo import control_silo_test
from sentry.testutils.silo import assume_test_silo_mode, control_silo_test


@control_silo_test(stable=True)
class BitbucketIntegrationTest(APITestCase):
provider = BitbucketIntegrationProvider

def setUp(self):
self.base_url = "https://api.bitbucket.org"
self.shared_secret = "234567890"
self.subject = "connect:1234567"
self.integration = Integration.objects.create(
provider="bitbucket",
provider=self.provider.key,
external_id=self.subject,
name="sentryuser",
metadata={
"base_url": self.base_url,
"domain_name": "bitbucket.org/Test-Organization",
"shared_secret": self.shared_secret,
"subject": self.subject,
},
Expand Down Expand Up @@ -138,3 +143,62 @@ def test_get_repositories_no_exact_match(self):
{"identifier": "sentryuser/stuff-2018", "name": "sentryuser/stuff-2018"},
{"identifier": "sentryuser/stuff-2019", "name": "sentryuser/stuff-2019"},
]

@responses.activate
def test_source_url_matches(self):
installation = self.integration.get_installation(self.organization.id)

test_cases = [
(
"https://bitbucket.org/Test-Organization/sentry/src/master/src/sentry/integrations/bitbucket/integration.py",
True,
),
(
"https://notbitbucket.org/Test-Organization/sentry/src/master/src/sentry/integrations/bitbucket/integration.py",
False,
),
("https://jianyuan.io", False),
]
for source_url, matches in test_cases:
assert installation.source_url_matches(source_url) == matches

@responses.activate
def test_extract_branch_from_source_url(self):
installation = self.integration.get_installation(self.organization.id)
integration = Integration.objects.get(provider=self.provider.key)

with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
organization_id=self.organization.id,
name="Test-Organization/repo",
url="https://bitbucket.org/Test-Organization/repo",
provider="integrations:bitbucket",
external_id=123,
config={"name": "Test-Organization/repo"},
integration_id=integration.id,
)
source_url = "https://bitbucket.org/Test-Organization/repo/src/master/src/sentry/integrations/bitbucket/integration.py"

assert installation.extract_branch_from_source_url(repo, source_url) == "master"

@responses.activate
def test_extract_source_path_from_source_url(self):
installation = self.integration.get_installation(self.organization.id)
integration = Integration.objects.get(provider=self.provider.key)

with assume_test_silo_mode(SiloMode.REGION):
repo = Repository.objects.create(
organization_id=self.organization.id,
name="Test-Organization/repo",
url="https://bitbucket.org/Test-Organization/repo",
provider="integrations:bitbucket",
external_id=123,
config={"name": "Test-Organization/repo"},
integration_id=integration.id,
)
source_url = "https://bitbucket.org/Test-Organization/repo/src/master/src/sentry/integrations/bitbucket/integration.py"

assert (
installation.extract_source_path_from_source_url(repo, source_url)
== "src/sentry/integrations/bitbucket/integration.py"
)

0 comments on commit a13a813

Please sign in to comment.