From 3d45d741b11baffcac1610acaad5be8f6a33d60f Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 20 Feb 2026 14:25:51 -0500 Subject: [PATCH 1/3] fix(integrations): add interaction events for /repos and /installation/repositories --- src/sentry/integrations/github/client.py | 52 +++++++++++-------- .../source_code_management/metrics.py | 4 ++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/sentry/integrations/github/client.py b/src/sentry/integrations/github/client.py index fc9c916258aec8..2dc5102b1e6b46 100644 --- a/src/sentry/integrations/github/client.py +++ b/src/sentry/integrations/github/client.py @@ -430,23 +430,28 @@ def get_remaining_api_requests(self) -> int: # This method is used by RepoTreesIntegration # https://docs.github.com/en/rest/git/trees#get-a-tree def get_tree(self, repo_full_name: str, tree_sha: str) -> list[dict[str, Any]]: - # We do not cache this call since it is a rather large object - contents: dict[str, Any] = self.get( - f"/repos/{repo_full_name}/git/trees/{tree_sha}", - # Will cause all objects or subtrees referenced by the tree specified in :tree_sha - params={"recursive": 1}, - ) - # If truncated is true in the response then the number of items in the tree array exceeded our maximum limit. - # If you need to fetch more items, use the non-recursive method of fetching trees, and fetch one sub-tree at a time. - # Note: The limit for the tree array is 100,000 entries with a maximum size of 7 MB when using the recursive parameter. - # XXX: We will need to improve this by iterating through trees without using the recursive parameter - if contents.get("truncated"): - # e.g. getsentry/DataForThePeople - logger.warning( - "The tree for %s has been truncated. Use different a approach for retrieving contents of tree.", - repo_full_name, + with SCMIntegrationInteractionEvent( + interaction_type=SCMIntegrationInteractionType.GET_TREE, + provider_key=self.integration_name, + integration_id=self.integration.id, + ).capture(): + # We do not cache this call since it is a rather large object + contents: dict[str, Any] = self.get( + f"/repos/{repo_full_name}/git/trees/{tree_sha}", + # Will cause all objects or subtrees referenced by the tree specified in :tree_sha + params={"recursive": 1}, ) - return contents["tree"] + # If truncated is true in the response then the number of items in the tree array exceeded our maximum limit. + # If you need to fetch more items, use the non-recursive method of fetching trees, and fetch one sub-tree at a time. + # Note: The limit for the tree array is 100,000 entries with a maximum size of 7 MB when using the recursive parameter. + # XXX: We will need to improve this by iterating through trees without using the recursive parameter + if contents.get("truncated"): + # e.g. getsentry/DataForThePeople + logger.warning( + "The tree for %s has been truncated. Use different a approach for retrieving contents of tree.", + repo_full_name, + ) + return contents["tree"] # Used by RepoTreesIntegration def should_count_api_error(self, error: ApiError, extra: dict[str, str]) -> bool: @@ -493,11 +498,16 @@ def get_repos(self, page_number_limit: int | None = None) -> list[dict[str, Any] It uses page_size from the base class to specify how many items per page. The upper bound of requests is controlled with self.page_number_limit to prevent infinite requests. """ - return self._get_with_pagination( - "/installation/repositories", - response_key="repositories", - page_number_limit=page_number_limit, - ) + with SCMIntegrationInteractionEvent( + interaction_type=SCMIntegrationInteractionType.GET_REPOSITORIES, + provider_key=self.integration_name, + integration_id=self.integration.id, + ).capture(): + return self._get_with_pagination( + "/installation/repositories", + response_key="repositories", + page_number_limit=page_number_limit, + ) def search_repositories(self, query: bytes) -> Mapping[str, Sequence[Any]]: """ diff --git a/src/sentry/integrations/source_code_management/metrics.py b/src/sentry/integrations/source_code_management/metrics.py index 3faa1a5405bc99..7da4994f30eed2 100644 --- a/src/sentry/integrations/source_code_management/metrics.py +++ b/src/sentry/integrations/source_code_management/metrics.py @@ -55,6 +55,10 @@ class SCMIntegrationInteractionType(StrEnum): # Rate Limiting GET_RATE_LIMIT = "get_rate_limit" + # Repo Trees + GET_REPOSITORIES = "get_repositories" + GET_TREE = "get_tree" + @dataclass class SCMIntegrationInteractionEvent(IntegrationEventLifecycleMetric): From 42396ce076588e754820a8ddbd799a773adc4d11 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 20 Feb 2026 15:17:31 -0500 Subject: [PATCH 2/3] tests --- .../github/tasks/test_link_all_repos.py | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/sentry/integrations/github/tasks/test_link_all_repos.py b/tests/sentry/integrations/github/tasks/test_link_all_repos.py index 3475e691c139c7..689f1d0d51f831 100644 --- a/tests/sentry/integrations/github/tasks/test_link_all_repos.py +++ b/tests/sentry/integrations/github/tasks/test_link_all_repos.py @@ -81,7 +81,12 @@ def test_link_all_repos(self, mock_record: MagicMock, _: MagicMock) -> None: assert repos[0].name == "getsentry/sentry" assert repos[1].name == "getsentry/snuba" - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.SUCCESS + assert end1.args[0] == EventLifecycleOutcome.SUCCESS @responses.activate @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -121,7 +126,12 @@ def test_link_all_repos_api_response_keyerror( assert repos[0].name == "getsentry/snuba" - assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.SUCCESS + assert end1.args[0] == EventLifecycleOutcome.HALTED assert_halt_metric( mock_record, LinkAllReposHaltReason.REPOSITORY_NOT_CREATED.value ) # should be halt because it didn't complete successfully @@ -155,7 +165,12 @@ def test_link_all_repos_api_response_keyerror_single_repo( repos = Repository.objects.all() assert len(repos) == 0 - assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.SUCCESS + assert end1.args[0] == EventLifecycleOutcome.HALTED assert_halt_metric(mock_record, LinkAllReposHaltReason.REPOSITORY_NOT_CREATED.value) @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -198,7 +213,12 @@ def test_link_all_repos_api_error(self, mock_record: MagicMock, _: MagicMock) -> organization_id=self.organization.id, ) - assert_slo_metric(mock_record, EventLifecycleOutcome.FAILURE) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.FAILURE + assert end1.args[0] == EventLifecycleOutcome.FAILURE @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @responses.activate @@ -221,7 +241,12 @@ def test_link_all_repos_api_error_rate_limited( organization_id=self.organization.id, ) - assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.FAILURE + assert end1.args[0] == EventLifecycleOutcome.HALTED assert_halt_metric(mock_record, LinkAllReposHaltReason.RATE_LIMITED.value) @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -240,7 +265,12 @@ def test_link_all_repos_repo_creation_error( organization_id=self.organization.id, ) - assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.SUCCESS + assert end1.args[0] == EventLifecycleOutcome.HALTED assert_halt_metric(mock_record, LinkAllReposHaltReason.REPOSITORY_NOT_CREATED.value) @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -262,4 +292,9 @@ def test_link_all_repos_repo_creation_exception( organization_id=self.organization.id, ) - assert_slo_metric(mock_record, EventLifecycleOutcome.FAILURE) + assert len(mock_record.mock_calls) == 4 + start1, start2, end2, end1 = mock_record.mock_calls + assert start1.args[0] == EventLifecycleOutcome.STARTED + assert start2.args[0] == EventLifecycleOutcome.STARTED + assert end2.args[0] == EventLifecycleOutcome.SUCCESS + assert end1.args[0] == EventLifecycleOutcome.FAILURE From b5b224eb07f11c7c42d9f5b2d656f465653ad268 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 20 Feb 2026 15:29:52 -0500 Subject: [PATCH 3/3] update name --- src/sentry/integrations/github/client.py | 2 +- src/sentry/integrations/source_code_management/metrics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/integrations/github/client.py b/src/sentry/integrations/github/client.py index 2dc5102b1e6b46..c14266c3c04783 100644 --- a/src/sentry/integrations/github/client.py +++ b/src/sentry/integrations/github/client.py @@ -431,7 +431,7 @@ def get_remaining_api_requests(self) -> int: # https://docs.github.com/en/rest/git/trees#get-a-tree def get_tree(self, repo_full_name: str, tree_sha: str) -> list[dict[str, Any]]: with SCMIntegrationInteractionEvent( - interaction_type=SCMIntegrationInteractionType.GET_TREE, + interaction_type=SCMIntegrationInteractionType.GET_REPO_TREE, provider_key=self.integration_name, integration_id=self.integration.id, ).capture(): diff --git a/src/sentry/integrations/source_code_management/metrics.py b/src/sentry/integrations/source_code_management/metrics.py index 7da4994f30eed2..6cc035d5bcab32 100644 --- a/src/sentry/integrations/source_code_management/metrics.py +++ b/src/sentry/integrations/source_code_management/metrics.py @@ -57,7 +57,7 @@ class SCMIntegrationInteractionType(StrEnum): # Repo Trees GET_REPOSITORIES = "get_repositories" - GET_TREE = "get_tree" + GET_REPO_TREE = "get_repo_tree" @dataclass