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

chore(integrations): SourceCodeSearchEndpoint metrics #80956

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
55 changes: 32 additions & 23 deletions src/sentry/integrations/bitbucket/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
from sentry.integrations.bitbucket.integration import BitbucketIntegration
from sentry.integrations.models.integration import Integration
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
from sentry.integrations.source_code_management.metrics import (
SCMIntegrationInteractionType,
SourceCodeSearchEndpointHaltReason,
)
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
from sentry.shared_integrations.exceptions import ApiError

Expand Down Expand Up @@ -37,32 +41,37 @@ def installation_class(self):
return BitbucketIntegration

def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
assert repo
with self.record_event(
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
).capture() as lifecycle:
assert repo

full_query = f'title~"{query}"'
try:
response = installation.search_issues(query=full_query, repo=repo)
except ApiError as e:
if "no issue tracker" in str(e):
logger.info(
"bitbucket.issue-search-no-issue-tracker",
extra={"installation_id": installation.model.id, "repo": repo},
)
return Response(
{"detail": "Bitbucket Repository has no issue tracker."}, status=400
)
raise
full_query = f'title~"{query}"'
try:
response = installation.search_issues(query=full_query, repo=repo)
except ApiError as e:
if "no issue tracker" in str(e):
lifecycle.record_halt(str(SourceCodeSearchEndpointHaltReason.NO_ISSUE_TRACKER))
logger.info(
"bitbucket.issue-search-no-issue-tracker",
extra={"installation_id": installation.model.id, "repo": repo},
)
return Response(
{"detail": "Bitbucket Repository has no issue tracker."}, status=400
)
raise

assert isinstance(response, dict)
return Response(
[
{"label": "#{} {}".format(i["id"], i["title"]), "value": i["id"]}
for i in response.get("values", [])
]
)
assert isinstance(response, dict)
return Response(
[
{"label": "#{} {}".format(i["id"], i["title"]), "value": i["id"]}
for i in response.get("values", [])
]
)

def handle_search_repositories(
self, integration: Integration, installation: T, query: str
) -> Response:
result = installation.get_repositories(query)
return Response([{"label": i["name"], "value": i["name"]} for i in result])
with self.record_event(SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES).capture():
result = installation.get_repositories(query)
return Response([{"label": i["name"], "value": i["name"]} for i in result])
79 changes: 47 additions & 32 deletions src/sentry/integrations/github/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegration
from sentry.integrations.models.integration import Integration
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
from sentry.integrations.source_code_management.metrics import (
SCMIntegrationInteractionType,
SourceCodeSearchEndpointHaltReason,
)
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
from sentry.shared_integrations.exceptions import ApiError

Expand All @@ -30,42 +34,53 @@ def installation_class(self):
return (GitHubIntegration, GitHubEnterpriseIntegration)

def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
assert repo
with self.record_event(
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
).capture() as lifecycle:
assert repo

try:
response = installation.search_issues(query=f"repo:{repo} {query}")
except ApiError as err:
if err.code == 403:
return Response({"detail": "Rate limit exceeded"}, status=429)
raise
try:
response = installation.search_issues(query=f"repo:{repo} {query}")
except ApiError as err:
if err.code == 403:
lifecycle.record_halt(str(SourceCodeSearchEndpointHaltReason.RATE_LIMITED))
return Response({"detail": "Rate limit exceeded"}, status=429)
raise

assert isinstance(response, dict)
return Response(
[
{"label": "#{} {}".format(i["number"], i["title"]), "value": i["number"]}
for i in response.get("items", [])
]
)
assert isinstance(response, dict)
return Response(
[
{"label": "#{} {}".format(i["number"], i["title"]), "value": i["number"]}
for i in response.get("items", [])
]
)

def handle_search_repositories(
self, integration: Integration, installation: T, query: str
) -> Response:
assert isinstance(installation, self.installation_class)
with self.record_event(
SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES
).capture() as lifecyle:
assert isinstance(installation, self.installation_class)

full_query = build_repository_query(integration.metadata, integration.name, query)
try:
response = installation.get_client().search_repositories(full_query)
except ApiError as err:
if err.code == 403:
return Response({"detail": "Rate limit exceeded"}, status=429)
if err.code == 422:
return Response(
{
"detail": "Repositories could not be searched because they do not exist, or you do not have access to them."
},
status=404,
)
raise
return Response(
[{"label": i["name"], "value": i["full_name"]} for i in response.get("items", [])]
)
full_query = build_repository_query(integration.metadata, integration.name, query)
try:
response = installation.get_client().search_repositories(full_query)
except ApiError as err:
if err.code == 403:
lifecyle.record_halt(str(SourceCodeSearchEndpointHaltReason.RATE_LIMITED))
return Response({"detail": "Rate limit exceeded"}, status=429)
if err.code == 422:
lifecyle.record_halt(
str(SourceCodeSearchEndpointHaltReason.MISSING_REPOSITORY_OR_NO_ACCESS)
)
return Response(
{
"detail": "Repositories could not be searched because they do not exist, or you do not have access to them."
},
status=404,
)
raise
return Response(
[{"label": i["name"], "value": i["full_name"]} for i in response.get("items", [])]
)
73 changes: 41 additions & 32 deletions src/sentry/integrations/gitlab/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sentry.integrations.gitlab.integration import GitlabIntegration
from sentry.integrations.models.integration import Integration
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
from sentry.integrations.source_code_management.metrics import SCMIntegrationInteractionType
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
from sentry.shared_integrations.exceptions import ApiError

Expand All @@ -27,43 +28,51 @@ def installation_class(self):
return GitlabIntegration

def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
assert repo
with self.record_event(
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
).capture() as lifecycle:
assert repo

full_query: str | None = query
full_query: str | None = query

try:
iids = [int(query)]
full_query = None
except ValueError:
iids = None
try:
iids = [int(query)]
full_query = None
except ValueError:
iids = None

try:
response = installation.search_issues(query=full_query, project_id=repo, iids=iids)
except ApiError as e:
return Response({"detail": str(e)}, status=400)
try:
response = installation.search_issues(query=full_query, project_id=repo, iids=iids)
except ApiError as e:
lifecycle.record_failure(e)
return Response({"detail": str(e)}, status=400)
Dismissed Show dismissed Hide dismissed

assert isinstance(response, list)
return Response(
[
{
"label": "(#{}) {}".format(i["iid"], i["title"]),
"value": "{}#{}".format(i["project_id"], i["iid"]),
}
for i in response
]
)
assert isinstance(response, list)
return Response(
[
{
"label": "(#{}) {}".format(i["iid"], i["title"]),
"value": "{}#{}".format(i["project_id"], i["iid"]),
}
for i in response
]
)

def handle_search_repositories(
self, integration: Integration, installation: T, query: str
) -> Response:
assert isinstance(installation, self.installation_class)
try:
response = installation.search_projects(query)
except ApiError as e:
return Response({"detail": str(e)}, status=400)
return Response(
[
{"label": project["name_with_namespace"], "value": project["id"]}
for project in response
]
)
with self.record_event(
SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES
).capture() as lifecyle:
assert isinstance(installation, self.installation_class)
try:
response = installation.search_projects(query)
except ApiError as e:
lifecyle.record_failure(e)
return Response({"detail": str(e)}, status=400)
Dismissed Show dismissed Hide dismissed
return Response(
[
{"label": project["name_with_namespace"], "value": project["id"]}
for project in response
]
)
25 changes: 23 additions & 2 deletions src/sentry/integrations/source_code_management/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class SCMIntegrationInteractionType(Enum):
# SourceCodeIssueIntegration (SCM only)
GET_REPOSITORY_CHOICES = "GET_REPOSITORY_CHOICES"

# SourceCodeSearchEndpoint
HANDLE_SEARCH_ISSUES = "HANDLE_SEARCH_ISSUES"
HANDLE_SEARCH_REPOSITORIES = "HANDLE_SEARCH_REPOSITORIES"
GET = "GET"

# CommitContextIntegration
CREATE_COMMENT = "CREATE_COMMENT"
UPDATE_COMMENT = "UPDATE_COMMENT"
Expand All @@ -39,7 +44,7 @@ def __str__(self) -> str:
@dataclass
class SCMIntegrationInteractionEvent(IntegrationEventLifecycleMetric):
"""
An instance to be recorded of a RepositoryIntegration feature call.
An instance to be recorded of an SCM integration feature call.
"""

interaction_type: SCMIntegrationInteractionType
Expand All @@ -66,9 +71,25 @@ def get_extras(self) -> Mapping[str, Any]:


class LinkAllReposHaltReason(StrEnum):
"""Common reasons why a link all repos task may halt without success/failure."""
"""
Common reasons why a link all repos task may halt without success/failure.
"""

MISSING_INTEGRATION = "missing_integration"
MISSING_ORGANIZATION = "missing_organization"
RATE_LIMITED = "rate_limited"
REPOSITORY_NOT_CREATED = "repository_not_created"


class SourceCodeSearchEndpointHaltReason(StrEnum):
"""
Reasons why a SourceCodeSearchEndpoint method (handle_search_issues,
handle_search_repositories, or get) may halt without success/failure.
"""

NO_ISSUE_TRACKER = "no_issue_tracker"
RATE_LIMITED = "rate_limited"
MISSING_REPOSITORY_OR_NO_ACCESS = "missing_repository_or_no_access"
MISSING_INTEGRATION = "missing_integration"
SERIALIZER_ERRORS = "serializer_errors"
MISSING_REPOSITORY_FIELD = "missing_repository_field"
Loading
Loading