diff --git a/src/github_sdk.py b/src/github_sdk.py index cf28d01..f0dfc07 100644 --- a/src/github_sdk.py +++ b/src/github_sdk.py @@ -6,11 +6,16 @@ import logging import uuid from datetime import datetime +from urllib.parse import urlparse import requests from sentry_sdk.envelope import Envelope from sentry_sdk.utils import format_timestamp +logger = logging.getLogger(__name__) + +SENTRY_HOST_SUFFIX = ".sentry.io" + class GithubSentryError(Exception): pass @@ -33,11 +38,14 @@ class GithubClient: def __init__(self, token, dsn, dry_run=False) -> None: self.token = token self.dry_run = dry_run + self.sentry_key = None + self.sentry_project_url = None if dsn: - base_uri, project_id = dsn.rsplit("/", 1) - self.sentry_key = base_uri.rsplit("@")[0].rsplit("https://")[1] - # '{BASE_URI}/api/{PROJECT_ID}/{ENDPOINT}/' - self.sentry_project_url = f"{base_uri}/api/{project_id}/envelope/" + self.sentry_key, self.sentry_project_url = _parse_sentry_dsn(dsn) + if not self.sentry_project_url: + logger.warning( + "Skipping trace ingestion because configured DSN host is invalid.", + ) def _fetch_github(self, url): headers = {"Authorization": f"token {self.token}"} @@ -108,6 +116,9 @@ def _generate_trace(self, job): return transaction def _send_envelope(self, trace): + if not self.sentry_project_url or not self.sentry_key: + logger.warning("Skipping envelope upload because DSN is not valid.") + return if self.dry_run: return envelope = Envelope() @@ -148,6 +159,25 @@ def send_trace(self, job): return self._send_envelope(trace) +def _parse_sentry_dsn(dsn): + parsed = urlparse(dsn) + project_id = parsed.path.strip("/") + + if ( + parsed.scheme != "https" + or not parsed.hostname + or not parsed.hostname.endswith(SENTRY_HOST_SUFFIX) + or not parsed.username + or not project_id + ): + return None, None + + # '{BASE_URI}/api/{PROJECT_ID}/{ENDPOINT}/' + base_uri = f"{parsed.scheme}://{parsed.netloc}" + sentry_project_url = f"{base_uri}/api/{project_id}/envelope/" + return parsed.username, sentry_project_url + + def _base_transaction(job): return { "event_id": get_uuid(), diff --git a/tests/test_github_sdk.py b/tests/test_github_sdk.py index 7f7e401..da640cf 100644 --- a/tests/test_github_sdk.py +++ b/tests/test_github_sdk.py @@ -12,6 +12,7 @@ from sentry_sdk.utils import format_timestamp from src.github_sdk import GithubClient +from src.github_sdk import _parse_sentry_dsn DSN = "https://foo@random.ingest.sentry.io/bar" TOKEN = "irrelevant" @@ -44,6 +45,18 @@ def test_initialize_without_setting_token(): assert msg == f"{prepend}__init__() missing 1 required positional argument: 'token'" +def test_reject_invalid_sentry_host(): + client = GithubClient(dsn="https://foo@sentryalert.ru/bar", token=TOKEN) + assert client.sentry_key is None + assert client.sentry_project_url is None + + +def test_parse_sentry_dsn_accepts_sentry_io_hosts(): + key, project_url = _parse_sentry_dsn("https://foo@o123.ingest.sentry.io/456") + assert key == "foo" + assert project_url == "https://foo@o123.ingest.sentry.io/api/456/envelope/" + + @responses.activate def test_ensure_raise_error_on_github_api_failure(): """We want to delegate to the app using the SDK to handle the error.""" @@ -153,6 +166,23 @@ def test_send_trace( for k, v in resp.request.headers.items(): assert envelope_headers[k] == v + +@responses.activate +def test_send_trace_does_not_post_when_dsn_is_invalid(jobA_job, jobA_runs, jobA_workflow): + 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, + ) + + client = GithubClient(dsn="https://foo@sentryalert.ru/bar", token=TOKEN) + resp = client.send_trace(jobA_job) + assert resp is None + assert all(call.request.method != "POST" for call in responses.calls) + # XXX: We will deal with this another time # assert ( # resp.request.body