Skip to content

feat(codecov): initial client for requests to codecov api #91830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

matt-codecov
Copy link

@matt-codecov matt-codecov commented May 16, 2025

background

we are bridging Codecov functionality into the Sentry product surface. at least initially, we will do this by writing Sentry API endpoints that are effectively proxies to the Codecov API. this PR introduces the class that those Sentry endpoints will use to make their requests

the Codecov API's base url is configurable via the codecov.base-url option so that, when working locally, we can direct Sentry to talk to a local instance of Codecov (or a staging instance, or something else).

i have not worked in Sentry's actual codebase before - please advise on code layout, best practices, anything else. thanks!

details

thin wrapper around requests that adds an Authorization: Bearer <JWT> header + uses a configurable base URL for requests to the Codecov API. the JWT indicates some context about the logged-in Sentry user that Codecov needs to fulfill the request.

the JWT is signed (HS256) using a secret shared between Codecov and Sentry and accessed through the codecov.api-bridge-signing-secret option. there is actually already a shared secret for signing JWTs in getsentry, and https://github.com/getsentry/getsentry/pull/17508 populates the new option with that existing secret.

relies on https://github.com/getsentry/getsentry/pull/17508
related to codecov/umbrella#149

Legal Boilerplate

Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label May 16, 2025
@matt-codecov matt-codecov force-pushed the mhammerly/feat/codecov-client branch from 14579a4 to 9a323ff Compare May 16, 2025 21:13
Copy link

codecov bot commented May 16, 2025

⚠️ Parser warning

The parser emitted a warning. Please review your JUnit XML file:

Warning while parsing testcase attributes: Limit of string is 1000 chars, for name, we got 2083 at 1:156897 in /home/runner/work/sentry/sentry/.artifacts/pytest.junit.xml

@matt-codecov matt-codecov requested a review from a team May 19, 2025 18:00
API host with the provided params and headers.

:param endpoint: The endpoint to request, without the host portion. For
examples: `/api/v2/gh/getsentry/users` or `/graphql`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: example vs examples

API host with the provided data and headers.

:param endpoint: The endpoint to request, without the host portion. For
examples: `/api/v2/gh/getsentry/users` or `/graphql`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comments here for example and the log

@matt-codecov matt-codecov requested a review from a team May 20, 2025 19:49
Comment on lines +76 to +77
if not (signing_secret := options.get("codecov.api-bridge-signing-secret")):
raise ConfigurationError()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're trying to not store secrets in runtime options, as they are stored in plain text everywhere (code + db). Instead could we use a django setting + environment var for this? That will allow us to put secrets in encrypted storage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this actually is populated by an environment variable in https://github.com/getsentry/getsentry/pull/17508, will that work?

hosted on.
"""

if not (base_url := options.get("codecov.base-url")):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could likely also be a django setting, as it is infrastructure plumbing and wouldn't be manipulated at runtime.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a way to override django settings for local development, i imagine? i'll look into that, ty

Copy link
Contributor

@michelletran-codecov michelletran-codecov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments.

Comment on lines +123 to +127
try:
response = requests.post(url, data=data, headers=headers, timeout=TIMEOUT_SECONDS)
except Exception:
logger.exception("Error when making POST request")
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts about capturing server errors and retrying?

response = requests.get(url, params=params, headers=headers, timeout=TIMEOUT_SECONDS)
except Exception:
logger.exception("Error when making GET request")
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will probably want to differentiate a 400 vs 500 response from Codecov in the API response. They should be handled differently in the Sentry REST API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, just realized that you would need a raise_for_status to actually raise the status exceptions. Anyways, we can either handle the errors here in the client, which might be easier for standardization, or leave it to the caller to handle.


def _create_jwt(self):
now = int(datetime.datetime.now(datetime.UTC).timestamp())
exp = now + 300 # 5 minutes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's make 300 a constant (with a descriptive name like FIVE_MINUTES)

joseph-sentry added a commit that referenced this pull request May 21, 2025
TODO: make this work for monolith mode, i didn't consider monolith
mode when I made these changes. I think the path forward for that is
to define another middleware that takes care of that and only operates
in monolith mode.

depends on: #91830

We want to forward webhooks Sentry receives to Codecov since Codecov
depends on receiving webhooks from the code forges it supports. We're
starting with only GitHub webhooks for now, but plan to support more
in the future.

We're using the existing WebhookPayload system to forward webhooks to
Codecov by creating an additional WebhookPayload object in the
GithubRequestParser. Codecov will do additional filtering on its end
to filter out irrelevant webhooks so we don't have to do any filtering
on Sentry's end, this is to avoid keeping track of what's relevant to
Codecov in Sentry.

I'm adding a destination_type column to the WebhookPayload so we can
distinguish between payloads meant for Codecov and other Sentry
regions because when we make the request in the drain_mailbox we want
to run different logic, using the CodecovApiClient.

I also made the choice to use a different mailbox_name so that the
forwarding doesn't interfere with the webhooks being sent to the
region silos.

Finally, because we want to forward ALL webhooks to Codecov, that
means we're forwarding installation events, a webhook that in Sentry
gets processed in the control silo, so that gets its own mailbox
identifier "installation".
joseph-sentry added a commit that referenced this pull request May 22, 2025
Forwarding webhooks for Sentry running in Monolith mode is considered
out of scope for this change

depends on: #91830

We want to forward webhooks Sentry receives to Codecov since Codecov
depends on receiving webhooks from the code forges it supports. We're
starting with only GitHub webhooks for now, but plan to support more
in the future.

We're using the existing WebhookPayload system to forward webhooks to
Codecov by creating an additional WebhookPayload object in the
GithubRequestParser. Codecov will do additional filtering on its end
to filter out irrelevant webhooks so we don't have to do any filtering
on Sentry's end, this is to avoid keeping track of what's relevant to
Codecov in Sentry.

I'm adding a destination_type column to the WebhookPayload so we can
distinguish between payloads meant for Codecov and other Sentry
regions because when we make the request in the drain_mailbox we want
to run different logic, using the CodecovApiClient.

I also made the choice to use a different mailbox_name so that the
forwarding doesn't interfere with the webhooks being sent to the
region silos.

Finally, because we want to forward ALL webhooks to Codecov, that
means we're forwarding installation events, a webhook that in Sentry
gets processed in the control silo, so that gets its own mailbox
identifier "installation".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Backend Automatically applied to PRs that change backend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants