Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/sentry_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import base64
import logging
import os
import time
from configparser import ConfigParser
from functools import lru_cache

import requests

Expand All @@ -15,6 +15,33 @@
SENTRY_CONFIG_API_URL = (
"https://api.github.com/repos/{owner}/.sentry/contents/sentry_config.ini"
)
GITHUB_API_TIMEOUT_SECONDS = float(os.environ.get("GITHUB_API_TIMEOUT_SECONDS", "10"))
GITHUB_API_MAX_RETRIES = int(os.environ.get("GITHUB_API_MAX_RETRIES", "2"))
GITHUB_API_RETRY_BACKOFF_SECONDS = float(
os.environ.get("GITHUB_API_RETRY_BACKOFF_SECONDS", "0.5")
)


def _github_get_with_retries(url: str, headers: dict[str, str]) -> requests.Response:
total_attempts = GITHUB_API_MAX_RETRIES + 1
for attempt in range(1, total_attempts + 1):
try:
return requests.get(
url,
headers=headers,
timeout=GITHUB_API_TIMEOUT_SECONDS,
)

Check failure

Code scanning / CodeQL

Full server-side request forgery Critical

The full URL of this request depends on a
user-provided value
.
Comment on lines +29 to +33
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
if attempt == total_attempts:
raise
wait_seconds = GITHUB_API_RETRY_BACKOFF_SECONDS * (2 ** (attempt - 1))
logger.warning(
"GitHub API request failed; retrying in %.1fs (%s/%s)",
wait_seconds,
attempt + 1,
total_attempts,
)
time.sleep(wait_seconds)


def fetch_dsn_for_github_org(org: str, token: str) -> str:
Expand All @@ -27,7 +54,7 @@
api_url = SENTRY_CONFIG_API_URL.replace("{owner}", org)

# - Get meta about sentry_config.ini file
resp = requests.get(api_url, headers=headers)
resp = _github_get_with_retries(api_url, headers)
resp.raise_for_status()
meta = resp.json()

Expand Down
42 changes: 42 additions & 0 deletions tests/test_sentry_config_file.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from __future__ import annotations

from unittest.mock import Mock
from unittest.mock import patch
from unittest import TestCase

import requests
import responses

from src.sentry_config import fetch_dsn_for_github_org
from src.sentry_config import GITHUB_API_MAX_RETRIES
from src.sentry_config import GITHUB_API_RETRY_BACKOFF_SECONDS
from src.sentry_config import GITHUB_API_TIMEOUT_SECONDS
from src.sentry_config import SENTRY_CONFIG_API_URL as api_url

expected_dsn = (
Expand Down Expand Up @@ -55,3 +61,39 @@ def test_file_missing(self) -> None:

def test_bad_contents(self) -> None:
pass

@patch("src.sentry_config.time.sleep")
@patch("src.sentry_config.requests.get")
def test_fetch_retries_connection_error_once(
self,
mock_get: Mock,
mock_sleep: Mock,
) -> None:
response = Mock()
response.raise_for_status.return_value = None
response.json.return_value = sentry_config_file_meta
mock_get.side_effect = [
requests.exceptions.ConnectionError("connection reset"),
response,
]

assert fetch_dsn_for_github_org(org, token) == expected_dsn
assert mock_get.call_count == 2
for call in mock_get.call_args_list:
assert call.kwargs["timeout"] == GITHUB_API_TIMEOUT_SECONDS
mock_sleep.assert_called_once_with(GITHUB_API_RETRY_BACKOFF_SECONDS)

@patch("src.sentry_config.time.sleep")
@patch("src.sentry_config.requests.get")
def test_fetch_raises_after_connection_error_retries_exhausted(
self,
mock_get: Mock,
mock_sleep: Mock,
) -> None:
mock_get.side_effect = requests.exceptions.ConnectionError("connection reset")

with self.assertRaises(requests.exceptions.ConnectionError):
fetch_dsn_for_github_org(org, token)

assert mock_get.call_count == GITHUB_API_MAX_RETRIES + 1
assert mock_sleep.call_count == GITHUB_API_MAX_RETRIES
Loading