Skip to content

Commit 5b70513

Browse files
authored
chore(integrations): SourceCodeSearchEndpoint metrics (#80956)
Add metrics to `handle_search_issues`, `handle_search_repositories`, and `get`.
1 parent 46e2146 commit 5b70513

File tree

10 files changed

+410
-158
lines changed

10 files changed

+410
-158
lines changed

src/sentry/integrations/bitbucket/search.py

+32-23
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from sentry.integrations.bitbucket.integration import BitbucketIntegration
1010
from sentry.integrations.models.integration import Integration
1111
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
12+
from sentry.integrations.source_code_management.metrics import (
13+
SCMIntegrationInteractionType,
14+
SourceCodeSearchEndpointHaltReason,
15+
)
1216
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
1317
from sentry.shared_integrations.exceptions import ApiError
1418

@@ -37,32 +41,37 @@ def installation_class(self):
3741
return BitbucketIntegration
3842

3943
def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
40-
assert repo
44+
with self.record_event(
45+
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
46+
).capture() as lifecycle:
47+
assert repo
4148

42-
full_query = f'title~"{query}"'
43-
try:
44-
response = installation.search_issues(query=full_query, repo=repo)
45-
except ApiError as e:
46-
if "no issue tracker" in str(e):
47-
logger.info(
48-
"bitbucket.issue-search-no-issue-tracker",
49-
extra={"installation_id": installation.model.id, "repo": repo},
50-
)
51-
return Response(
52-
{"detail": "Bitbucket Repository has no issue tracker."}, status=400
53-
)
54-
raise
49+
full_query = f'title~"{query}"'
50+
try:
51+
response = installation.search_issues(query=full_query, repo=repo)
52+
except ApiError as e:
53+
if "no issue tracker" in str(e):
54+
lifecycle.record_halt(str(SourceCodeSearchEndpointHaltReason.NO_ISSUE_TRACKER))
55+
logger.info(
56+
"bitbucket.issue-search-no-issue-tracker",
57+
extra={"installation_id": installation.model.id, "repo": repo},
58+
)
59+
return Response(
60+
{"detail": "Bitbucket Repository has no issue tracker."}, status=400
61+
)
62+
raise
5563

56-
assert isinstance(response, dict)
57-
return Response(
58-
[
59-
{"label": "#{} {}".format(i["id"], i["title"]), "value": i["id"]}
60-
for i in response.get("values", [])
61-
]
62-
)
64+
assert isinstance(response, dict)
65+
return Response(
66+
[
67+
{"label": "#{} {}".format(i["id"], i["title"]), "value": i["id"]}
68+
for i in response.get("values", [])
69+
]
70+
)
6371

6472
def handle_search_repositories(
6573
self, integration: Integration, installation: T, query: str
6674
) -> Response:
67-
result = installation.get_repositories(query)
68-
return Response([{"label": i["name"], "value": i["name"]} for i in result])
75+
with self.record_event(SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES).capture():
76+
result = installation.get_repositories(query)
77+
return Response([{"label": i["name"], "value": i["name"]} for i in result])

src/sentry/integrations/github/search.py

+47-32
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegration
88
from sentry.integrations.models.integration import Integration
99
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
10+
from sentry.integrations.source_code_management.metrics import (
11+
SCMIntegrationInteractionType,
12+
SourceCodeSearchEndpointHaltReason,
13+
)
1014
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
1115
from sentry.shared_integrations.exceptions import ApiError
1216

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

3236
def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
33-
assert repo
37+
with self.record_event(
38+
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
39+
).capture() as lifecycle:
40+
assert repo
3441

35-
try:
36-
response = installation.search_issues(query=f"repo:{repo} {query}")
37-
except ApiError as err:
38-
if err.code == 403:
39-
return Response({"detail": "Rate limit exceeded"}, status=429)
40-
raise
42+
try:
43+
response = installation.search_issues(query=f"repo:{repo} {query}")
44+
except ApiError as err:
45+
if err.code == 403:
46+
lifecycle.record_halt(str(SourceCodeSearchEndpointHaltReason.RATE_LIMITED))
47+
return Response({"detail": "Rate limit exceeded"}, status=429)
48+
raise
4149

42-
assert isinstance(response, dict)
43-
return Response(
44-
[
45-
{"label": "#{} {}".format(i["number"], i["title"]), "value": i["number"]}
46-
for i in response.get("items", [])
47-
]
48-
)
50+
assert isinstance(response, dict)
51+
return Response(
52+
[
53+
{"label": "#{} {}".format(i["number"], i["title"]), "value": i["number"]}
54+
for i in response.get("items", [])
55+
]
56+
)
4957

5058
def handle_search_repositories(
5159
self, integration: Integration, installation: T, query: str
5260
) -> Response:
53-
assert isinstance(installation, self.installation_class)
61+
with self.record_event(
62+
SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES
63+
).capture() as lifecyle:
64+
assert isinstance(installation, self.installation_class)
5465

55-
full_query = build_repository_query(integration.metadata, integration.name, query)
56-
try:
57-
response = installation.get_client().search_repositories(full_query)
58-
except ApiError as err:
59-
if err.code == 403:
60-
return Response({"detail": "Rate limit exceeded"}, status=429)
61-
if err.code == 422:
62-
return Response(
63-
{
64-
"detail": "Repositories could not be searched because they do not exist, or you do not have access to them."
65-
},
66-
status=404,
67-
)
68-
raise
69-
return Response(
70-
[{"label": i["name"], "value": i["full_name"]} for i in response.get("items", [])]
71-
)
66+
full_query = build_repository_query(integration.metadata, integration.name, query)
67+
try:
68+
response = installation.get_client().search_repositories(full_query)
69+
except ApiError as err:
70+
if err.code == 403:
71+
lifecyle.record_halt(str(SourceCodeSearchEndpointHaltReason.RATE_LIMITED))
72+
return Response({"detail": "Rate limit exceeded"}, status=429)
73+
if err.code == 422:
74+
lifecyle.record_halt(
75+
str(SourceCodeSearchEndpointHaltReason.MISSING_REPOSITORY_OR_NO_ACCESS)
76+
)
77+
return Response(
78+
{
79+
"detail": "Repositories could not be searched because they do not exist, or you do not have access to them."
80+
},
81+
status=404,
82+
)
83+
raise
84+
return Response(
85+
[{"label": i["name"], "value": i["full_name"]} for i in response.get("items", [])]
86+
)

src/sentry/integrations/gitlab/search.py

+41-32
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sentry.integrations.gitlab.integration import GitlabIntegration
77
from sentry.integrations.models.integration import Integration
88
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
9+
from sentry.integrations.source_code_management.metrics import SCMIntegrationInteractionType
910
from sentry.integrations.source_code_management.search import SourceCodeSearchEndpoint
1011
from sentry.shared_integrations.exceptions import ApiError
1112

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

2930
def handle_search_issues(self, installation: T, query: str, repo: str | None) -> Response:
30-
assert repo
31+
with self.record_event(
32+
SCMIntegrationInteractionType.HANDLE_SEARCH_ISSUES
33+
).capture() as lifecycle:
34+
assert repo
3135

32-
full_query: str | None = query
36+
full_query: str | None = query
3337

34-
try:
35-
iids = [int(query)]
36-
full_query = None
37-
except ValueError:
38-
iids = None
38+
try:
39+
iids = [int(query)]
40+
full_query = None
41+
except ValueError:
42+
iids = None
3943

40-
try:
41-
response = installation.search_issues(query=full_query, project_id=repo, iids=iids)
42-
except ApiError as e:
43-
return Response({"detail": str(e)}, status=400)
44+
try:
45+
response = installation.search_issues(query=full_query, project_id=repo, iids=iids)
46+
except ApiError as e:
47+
lifecycle.record_failure(e)
48+
return Response({"detail": str(e)}, status=400)
4449

45-
assert isinstance(response, list)
46-
return Response(
47-
[
48-
{
49-
"label": "(#{}) {}".format(i["iid"], i["title"]),
50-
"value": "{}#{}".format(i["project_id"], i["iid"]),
51-
}
52-
for i in response
53-
]
54-
)
50+
assert isinstance(response, list)
51+
return Response(
52+
[
53+
{
54+
"label": "(#{}) {}".format(i["iid"], i["title"]),
55+
"value": "{}#{}".format(i["project_id"], i["iid"]),
56+
}
57+
for i in response
58+
]
59+
)
5560

5661
def handle_search_repositories(
5762
self, integration: Integration, installation: T, query: str
5863
) -> Response:
59-
assert isinstance(installation, self.installation_class)
60-
try:
61-
response = installation.search_projects(query)
62-
except ApiError as e:
63-
return Response({"detail": str(e)}, status=400)
64-
return Response(
65-
[
66-
{"label": project["name_with_namespace"], "value": project["id"]}
67-
for project in response
68-
]
69-
)
64+
with self.record_event(
65+
SCMIntegrationInteractionType.HANDLE_SEARCH_REPOSITORIES
66+
).capture() as lifecyle:
67+
assert isinstance(installation, self.installation_class)
68+
try:
69+
response = installation.search_projects(query)
70+
except ApiError as e:
71+
lifecyle.record_failure(e)
72+
return Response({"detail": str(e)}, status=400)
73+
return Response(
74+
[
75+
{"label": project["name_with_namespace"], "value": project["id"]}
76+
for project in response
77+
]
78+
)

src/sentry/integrations/source_code_management/metrics.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class SCMIntegrationInteractionType(Enum):
2525
# SourceCodeIssueIntegration (SCM only)
2626
GET_REPOSITORY_CHOICES = "GET_REPOSITORY_CHOICES"
2727

28+
# SourceCodeSearchEndpoint
29+
HANDLE_SEARCH_ISSUES = "HANDLE_SEARCH_ISSUES"
30+
HANDLE_SEARCH_REPOSITORIES = "HANDLE_SEARCH_REPOSITORIES"
31+
GET = "GET"
32+
2833
# CommitContextIntegration
2934
CREATE_COMMENT = "CREATE_COMMENT"
3035
UPDATE_COMMENT = "UPDATE_COMMENT"
@@ -39,7 +44,7 @@ def __str__(self) -> str:
3944
@dataclass
4045
class SCMIntegrationInteractionEvent(IntegrationEventLifecycleMetric):
4146
"""
42-
An instance to be recorded of a RepositoryIntegration feature call.
47+
An instance to be recorded of an SCM integration feature call.
4348
"""
4449

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

6772

6873
class LinkAllReposHaltReason(StrEnum):
69-
"""Common reasons why a link all repos task may halt without success/failure."""
74+
"""
75+
Common reasons why a link all repos task may halt without success/failure.
76+
"""
7077

7178
MISSING_INTEGRATION = "missing_integration"
7279
MISSING_ORGANIZATION = "missing_organization"
7380
RATE_LIMITED = "rate_limited"
7481
REPOSITORY_NOT_CREATED = "repository_not_created"
82+
83+
84+
class SourceCodeSearchEndpointHaltReason(StrEnum):
85+
"""
86+
Reasons why a SourceCodeSearchEndpoint method (handle_search_issues,
87+
handle_search_repositories, or get) may halt without success/failure.
88+
"""
89+
90+
NO_ISSUE_TRACKER = "no_issue_tracker"
91+
RATE_LIMITED = "rate_limited"
92+
MISSING_REPOSITORY_OR_NO_ACCESS = "missing_repository_or_no_access"
93+
MISSING_INTEGRATION = "missing_integration"
94+
SERIALIZER_ERRORS = "serializer_errors"
95+
MISSING_REPOSITORY_FIELD = "missing_repository_field"

0 commit comments

Comments
 (0)