From 85fb9f700121c937b251d952a4822fdf1fcf7a3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 15:54:36 +0000 Subject: [PATCH] Retry transient Sentry envelope failures Co-authored-by: Armen Zambrano G. --- src/github_sdk.py | 36 +++++++++++++++++++++++++++++++----- tests/test_github_sdk.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/github_sdk.py b/src/github_sdk.py index cf28d01..bb5ac5e 100644 --- a/src/github_sdk.py +++ b/src/github_sdk.py @@ -4,6 +4,7 @@ import hashlib import io import logging +import time import uuid from datetime import datetime @@ -29,6 +30,9 @@ 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"} + request_timeout_seconds = 10 + sentry_envelope_retry_attempts = 3 + sentry_envelope_retry_backoff_seconds = 0.25 def __init__(self, token, dsn, dry_run=False) -> None: self.token = token @@ -128,14 +132,36 @@ def _send_envelope(self, trace): with gzip.GzipFile(fileobj=body, mode="w") as f: envelope.serialize_into(f) - req = requests.post( - self.sentry_project_url, - data=body.getvalue(), - headers=headers, - ) + req = self._post_sentry_envelope_with_retries(body.getvalue(), headers) req.raise_for_status() return req + def _post_sentry_envelope_with_retries(self, data, headers): + retryable_errors = ( + requests.exceptions.ConnectionError, + requests.exceptions.SSLError, + requests.exceptions.Timeout, + ) + for attempt in range(1, self.sentry_envelope_retry_attempts + 1): + try: + return requests.post( + self.sentry_project_url, + data=data, + headers=headers, + timeout=self.request_timeout_seconds, + ) + except retryable_errors: + if attempt == self.sentry_envelope_retry_attempts: + raise + logging.warning( + "Transient error sending Sentry envelope; retrying attempt %s/%s", + attempt + 1, + self.sentry_envelope_retry_attempts, + ) + time.sleep( + self.sentry_envelope_retry_backoff_seconds * 2 ** (attempt - 1), + ) + def send_trace(self, job): # This can happen when the workflow is skipped and there are no steps if job["conclusion"] == "skipped": diff --git a/tests/test_github_sdk.py b/tests/test_github_sdk.py index 7f7e401..1a7e7ee 100644 --- a/tests/test_github_sdk.py +++ b/tests/test_github_sdk.py @@ -158,3 +158,38 @@ def test_send_trace( # resp.request.body # == b"\x1f\x8b\x08\x00\xf1\x16}b\x02\xff\xb5TM\x8f\xd30\x10\xbd\xef\xaf\x88|\x02\xa9m\x1c\xc7\x89\x93H\x08\xd0\x8a;\x12\x9c@\xa8\x9a\xd8\xe3&\xbb\xf9\"vX\xaa\xaa\xff\x1d{\xdb\xee\x97\x96n\xcb\x8aS\xd3\x99\xf1\xf8\xbd7o\xbc\xd9^l\x88]\x0fH\nbG\xe8\x0cH[\xf7\x1d\x99\x11\xd9w\x16;\xbb\xdc'a\x18\x9aZ\x82O\x86W\xe6\xb6\xa2\xc1ne+RD\x19\xa7\xbe\r\xfe\xf2\xf5\xb5r\xd5\t \x139H\x95g\x8c\xcbRC\x96P\xceh\x14K]\xea\xac\x14\xee\xf4\xb3\x97>\xfcW\x10=\xdebP\x81EcM\xf0\x86\xbe=\xe0\xfam\r)6\xbe\\\xa2\xff0\x03t\xbb\x9b\x81\xd3He\xb1\x14()\xcf\x13\xbdk*q\x97\xcd\xa2\x92\x96\x08)\xa0\xd2<\x8b\x04\x08\xaeb\xa6\x04Ms\x9a)\xcd\x1e\xe1r\xadgD\x81\x05\x7f\xc3U_\xbahe\xed`\x8a0\\\xd5\xb6\x9a\xca\x85\xec\xdbp\x85\xd68\xde\xe3:\xdc\xff\x8cSg\xc2$KD\xc2\x04O\xe8{Y\xa1\xbc^\x9a\xa9\xb6\xb8\xd4\xbd\x9c\xcc;;N\xbe\xf50\x9e\xd8q\x98\x9a&\x8c\xe3\x98\x0b\xb2\x9d\x91~\xf8\x9b4\n\x8d\x1c\xeb\xe1\x98z\xc6\x82\x9d\x9cv\xa4\xbf&[\xd7l28zz\x1d\xb4\x9e\xf5\xc7\xaaE\x15|\xb2\xa8\xd7\xae\x18[\xa8\x1b\xaf\xa9\x8f.\xd0G#\xf6a\xe5\xa3\x1e\xa8\x07\xe3\xfa\x8d\xce#u\xeb\xee\x80\xd6#c\x94\xb19\xe5s\x9a~\x8d\xf2\"aE$\xbeyY\x9f/a\xb4\xa0I\x11\xefJ`e\xf6R/\xefp\x9aIJ4\xc6\xa5K\xe7\rY\x1d\xe0\x84\xa0\xd4\xdc\xa1\xae\xbb\xd5\xbc\x81\xb5c\xe1\xad\xd1\xb6\xb5\xf5\xd4U.R\x16e\x90s\x95\x01\x95\x8aQ\xe7\x88(B\x10\xaa\xc44\x93\x89\x88Qs\xe5\xce\x8c8\xf4\xee\xc4S\xcd}f\xea\x96`-\xb6\x83k\x19\xcd\xc8M?^\xeb\xa6\xbf\xf1\x08\x1c\xa6\xc1:8\xb8X\xb7\x8d\x1f\xa5\x9b\xd0r\xc4\x9f\x93\xe3H\x8a\xdbQyq\x9c+\x1d\x87\xef\x9b\xdd\xcc\xbe\xa0\r\xa6!\xf0N\x9a\x1d\x04\x7f\x14\x1b`\xf4\x1bt\xd4\xcc\xf7I*X\xaaK\x0e,\xe6\x11\x17B\x92\xd3\xa6\x91.(\xa5G&\xb2+c\xf4\xae\xec\x8c\xed\xd9\xce\xf6T?;=\x82U%\xc7E\xdd?\xf0\xf3n\xb3\xe7\x95m\x9b\xb9\xed\xe7u\x0b+,\x1a\xf0\x06\xbd\x97\xe4\x9f\xce\x9e'\x1d\x08!RT\x11S\x00\x9c\xe7\xe5i\xd2=\xd0\xe4XY\x92\xbdN\xba\xde\xd8\xe0\xd2\xbf\x19\xfdd\x83;7\x1e\xc4y>{\x1e\xfd\x14\x9da\xb4\x8e\x05\xd39\xcf3x\x89\xfeaI_\xa0\xbf/K^E\xff\xb2o\x87\xc6=5\x8f\xd7\xe4I\xf4<\xba\x8e\xa2\xdbWT\x98\xe8\x88\xbb\xa7\xe1D\xba\xc9\xff\xa4\xfbc{\xf1\x07Hk>,{\x07\x00\x00" # ) + + +@freeze_time() +@responses.activate +@patch("src.github_sdk.time.sleep") +@patch("src.github_sdk.get_uuid") +def test_send_trace_retries_transient_sentry_ssl_errors( + mock_get_uuid, + mock_sleep, + jobA_job, + jobA_runs, + jobA_workflow, + uuid_list, +): + mock_get_uuid.side_effect = uuid_list + responses.get( + "https://api.github.com/repos/getsentry/sentry/actions/runs/2104746951", + json=jobA_runs, + ) + responses.get( + "https://api.github.com/repos/getsentry/sentry/actions/workflows/1174556", + json=jobA_workflow, + ) + + envelope_url = "https://foo@random.ingest.sentry.io/api/bar/envelope/" + responses.post(envelope_url, body=requests.exceptions.SSLError()) + responses.post(envelope_url, body=requests.exceptions.SSLError()) + responses.post(envelope_url) + + client = GithubClient(dsn=DSN, token=TOKEN) + resp = client.send_trace(jobA_job) + + assert resp.status_code == 200 + assert [call.request.url for call in responses.calls].count(envelope_url) == 3 + assert [call.args[0] for call in mock_sleep.call_args_list] == [0.25, 0.5]