From 981d1760d28914dd3da259eec61b39d9e771290f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Apr 2026 08:04:43 +0000 Subject: [PATCH] Handle GitHub API timeouts during trace generation Co-authored-by: Armen Zambrano G. --- src/github_sdk.py | 19 +++++++++++++++++-- tests/test_github_sdk.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/github_sdk.py b/src/github_sdk.py index cf28d01..3c067ef 100644 --- a/src/github_sdk.py +++ b/src/github_sdk.py @@ -29,6 +29,7 @@ def get_uuid_from_string(input_string): class GithubClient: # This transform GH jobs conclusion keywords to Sentry performance status github_status_trace_status = {"success": "ok", "failure": "internal_error"} + github_api_timeout_seconds = 10 def __init__(self, token, dsn, dry_run=False) -> None: self.token = token @@ -42,7 +43,11 @@ def __init__(self, token, dsn, dry_run=False) -> None: def _fetch_github(self, url): headers = {"Authorization": f"token {self.token}"} - req = requests.get(url, headers=headers) + req = requests.get( + url, + headers=headers, + timeout=self.github_api_timeout_seconds, + ) req.raise_for_status() return req @@ -143,7 +148,17 @@ def send_trace(self, job): f"We are ignoring '{job['name']}' because it was skipped -> {job['html_url']}", ) return - trace = self._generate_trace(job) + try: + trace = self._generate_trace(job) + except requests.exceptions.Timeout as error: + # Timeouts while reading GH metadata are usually transient. + # Dropping this single trace avoids failing the whole webhook call. + logging.warning( + "Skipping trace generation after GitHub API timeout for run %s: %s", + job.get("run_id", "unknown"), + error, + ) + return if trace: return self._send_envelope(trace) diff --git a/tests/test_github_sdk.py b/tests/test_github_sdk.py index 7f7e401..41aa5fe 100644 --- a/tests/test_github_sdk.py +++ b/tests/test_github_sdk.py @@ -2,6 +2,7 @@ import sys from datetime import datetime +from unittest import mock from unittest.mock import patch import pytest @@ -59,6 +60,37 @@ def test_ensure_raise_error_on_github_api_failure(): ) +@patch("src.github_sdk.requests.get") +def test_fetch_github_sets_timeout(mock_get): + mock_response = mock.Mock() + mock_get.return_value = mock_response + + url = "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951" + client = GithubClient(dsn=DSN, token=TOKEN) + response = client._fetch_github(url) + + assert response == mock_response + mock_get.assert_called_once_with( + url, + headers={"Authorization": f"token {TOKEN}"}, + timeout=GithubClient.github_api_timeout_seconds, + ) + + +def test_send_trace_handles_timeout_during_metadata_fetch(jobA_job): + client = GithubClient(dsn=DSN, token=TOKEN) + with patch.object( + client, + "_generate_trace", + side_effect=requests.exceptions.Timeout("timed out"), + ) as mock_generate_trace: + with patch.object(client, "_send_envelope") as mock_send_envelope: + assert client.send_trace(jobA_job) is None + + mock_generate_trace.assert_called_once_with(jobA_job) + mock_send_envelope.assert_not_called() + + @freeze_time() @responses.activate @patch("src.github_sdk.get_uuid")