Skip to content

Commit c29b65f

Browse files
committed
fix(integrations): Cache missing repo tree lookups
Cache GitHub repo tree 404 responses as empty results to avoid repeated failing requests from auto source code config. Keep non-404 API errors raising normally and cover both behaviors with tests. Fixes SENTRY-5K7G Co-Authored-By: Codex <noreply@openai.com> Made-with: Cursor
1 parent d3fe3d1 commit c29b65f

File tree

2 files changed

+51
-0
lines changed

2 files changed

+51
-0
lines changed

src/sentry/integrations/source_code_management/repo_trees.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class RepoTree(NamedTuple):
3535
# When the number of remaining API requests is less than this value, it will
3636
# fall back to the cache.
3737
MINIMUM_REQUESTS_REMAINING = 200
38+
NOT_FOUND_CACHE_SECONDS = 3600 * 24
3839

3940

4041
class RepoTreesIntegration(ABC):
@@ -211,6 +212,16 @@ def get_cached_repo_files(
211212
)
212213
cache.set(key, [], self.CACHE_SECONDS + shifted_seconds)
213214
tree = None
215+
except ApiError as error:
216+
if _is_not_found_error(error):
217+
logger.info(
218+
"Caching empty files result for missing repo or ref",
219+
extra={"repo": repo_full_name},
220+
)
221+
cache.set(key, [], NOT_FOUND_CACHE_SECONDS)
222+
tree = None
223+
else:
224+
raise
214225
if tree:
215226
# Keep files; discard directories
216227
repo_files = [node["path"] for node in tree if node["type"] == "blob"]
@@ -296,3 +307,11 @@ def should_include(file_path: str) -> bool:
296307
if any(file_path.startswith(path) for path in EXCLUDED_PATHS):
297308
return False
298309
return True
310+
311+
312+
def _is_not_found_error(error: ApiError) -> bool:
313+
if error.code == 404:
314+
return True
315+
316+
error_message = error.json.get("message") if error.json else error.text
317+
return error_message in ("Not Found", "Not Found.")

tests/sentry/integrations/github/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,38 @@ def test_get_cached_repo_files_with_all_files(self) -> None:
379379
files = self.install.get_cached_repo_files(self.repo.name, "master", 0)
380380
assert files == ["src/foo.py"]
381381

382+
@responses.activate
383+
def test_get_cached_repo_files_caches_not_found(self) -> None:
384+
responses.add(
385+
method=responses.GET,
386+
url=f"https://api.github.com/repos/{self.repo.name}/git/trees/master?recursive=1",
387+
status=404,
388+
json={"message": "Not Found"},
389+
)
390+
repo_key = f"github:repo:{self.repo.name}:source-code"
391+
assert cache.get(repo_key) is None
392+
393+
files = self.install.get_cached_repo_files(self.repo.name, "master", 0)
394+
assert files == []
395+
assert cache.get(repo_key) == []
396+
397+
# Negative-cache hit should avoid an additional API request.
398+
files = self.install.get_cached_repo_files(self.repo.name, "master", 0)
399+
assert files == []
400+
assert len(responses.calls) == 1
401+
402+
@responses.activate
403+
def test_get_cached_repo_files_raises_non_not_found_api_error(self) -> None:
404+
responses.add(
405+
method=responses.GET,
406+
url=f"https://api.github.com/repos/{self.repo.name}/git/trees/master?recursive=1",
407+
status=500,
408+
json={"message": "Server Error"},
409+
)
410+
411+
with pytest.raises(ApiError):
412+
self.install.get_cached_repo_files(self.repo.name, "master", 0)
413+
382414
@mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1")
383415
@responses.activate
384416
def test_update_comment(self, get_jwt) -> None:

0 commit comments

Comments
 (0)