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
38 changes: 34 additions & 4 deletions src/github_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
Expand Down
30 changes: 30 additions & 0 deletions tests/test_github_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down
Loading