Repository navigation
Conversation
vaind
changed the base branch from
master
to
ivan/integration-access-token-rpc
October 22, 2026 07:13
Contributor
Sentry Snapshot Testing
|
vaind
added a commit
that referenced
this pull request
Oct 27, 2026
…8751) CI turns every Django API route into a TypeScript string in `knownSentryApiUrls.generated.ts`, so frontend calls to `getApiUrl()` are checked against real routes. The generator stripped regex groups and character classes but copied escapes through, so a route that matches a literal dot, such as `\.git` in the git endpoint added in #128444, came out as `'/internal/scm-git/repositories/$repositoryId\.git/$route'`. oxlint rejects that string as a useless escape, which fails lint on any PR that adds such a route. Escaped punctuation is now written as the character itself (`$repositoryId.git`). No route on master has an escape, so the generated file doesn't change here. Refs [CW-2222](https://linear.app/getsentry/issue/CW-2222/git-endpoint-view-in-sentry)
vaind
force-pushed
the
ivan/scm-git-endpoint-view
branch
from
October 27, 2026 22:08
cfdef2d to
f214b2b
Compare
This was referenced Oct 27, 2026
vaind
marked this pull request as ready for review
October 27, 2026 22:54
vaind
marked this pull request as draft
October 27, 2026 22:55
vaind
marked this pull request as ready for review
October 27, 2026 22:58
vaind
marked this pull request as draft
October 27, 2026 22:59
vaind
marked this pull request as ready for review
October 27, 2026 23:05
vaind
added a commit
to getsentry/scm-platform
that referenced
this pull request
Oct 28, 2026
When Sentry's git endpoint (getsentry/sentry#128444) refuses a request after it has fetched a provider token, for example because the provider is unreachable or rejects the token, the error report carries the token. Sentry's SDK sends the local variables of every frame in the traceback, Sentry's SDK configuration turns off the scrubber that would otherwise replace a variable named `token`, and `GitServer.handle` and `GitRelay.forward` both hold the token as a plain string. #162 fixed this for grant secrets and the grant, but left the token a `str`. ## Approach - `GetToken` returns `scm.Secret | None`, and `GitServer.handle` passes the `Secret` on to `GitRelay.forward`, so both frames show `[Filtered]`. The relay calls `reveal()` only inline, for the ASCII check and `HTTPBasicAuth`. - `GitServer.handle` refuses a token that isn't a `Secret` with a `TypeError`, deleting the local first so that error's report doesn't carry it either. A plain string from untyped code would otherwise fail every request in the relay with an `AttributeError` whose frames hold it. - `bin/git-server` wraps its tokens. `get_token` now has to wrap the token in `Secret`. Nothing calls it yet: Sentry's endpoint is still unmerged and will wrap it when it bumps to the release with this change. Not covered: an error the relay's adapter raises itself, such as Sentry's `BlacklistAdapter` refusing an internal address, passes through urllib3's frames, which hold the `Authorization` header. The relay can't mask those, so `GitRelay`'s docstring now says to report such an error without its frames' local variables; Sentry's endpoint handles that separately.
The settled layout is /api/0/internal/scm-git/repositories/{repository_id}.git/{route}. The
path carries the Sentry Repository.id that the grant must list, so GitServer's own grant check
refuses any other repository before the database, the token or the provider. The view no longer
matches the path against repository names, which collide between GitHub, GitHub Enterprise and
Cursor Origin. scm-git/hosts/ stays free for a later route that names repositories by host.
Refs CW-2222
Review fixes for the git endpoint: - Add "grant" to the SDK's request header denylist. None of its terms matched X-Scm-Git-Grant, so the grant reached spans and error events. - Answer a GitHub token that can't be minted (e.g. a suspended app) with GitServer's text/plain 502 rather than a JSON 500, which git shows only as "HTTP 500". - Limit the route's repository id to 19 ASCII digits. \d also matches other scripts' digits, and int() refuses an id of 4,301 digits with a 500. - Test org scoping with a repository that has a working integration, and test unmatched URLs at the resolver, where they reach the API catch-all. Refs CW-2222
SCM errors always carry a detail, so the refusal helper no longer falls back to the error code. The tests share a helper for the info/refs request that opens every fetch, and the inactive repository test now checks the refusal like its siblings. Refs CW-2222
Which token a repository's provider takes is per-provider knowledge, like which adapter to build, so the lookup moves from the git endpoint into sentry.scm.private.helpers next to fetch_service_provider. The GitLab and Cursor Origin branches then land in the same place as their adapters. Refs CW-2222
scm-platform's GitServer now takes an emit_error callback and tags its counts with the referrer Seer puts in each grant. The view logs each failure with what the request had resolved, because git shows the client only the message. Its own refusals (no organization, flag off) carry the same referrer tag and log line, so they read like the ones GitServer answers. Refs CW-2222
integration_service.get_access_token mints or refreshes the token in control and sends only the token to the cell, for GitHub, GitHub Enterprise, GitLab and Cursor Origin. The git endpoint drops its own switch on provider, GitLab and Cursor Origin repositories now relay instead of being refused, and the cell no longer receives the full integration metadata just to read a token. The git server also builds its repository and provider lookups with make_fetchers, so the two share one integration fetch per request. Refs CW-2222
The token RPC now leaves GitLab out until GitLab's client refreshes tokens before they expire, and answers None for it. The git server turns None into a 502, which tells git there was an outage when Sentry simply can't relay GitLab yet, so the endpoint refuses GitLab repositories as not supported before asking for a token, as it did before the switch to the RPC. Refs CW-2222
sentry-scm 2.0.0 takes signing secrets as scm.Secret, which shows as [Filtered] in error reports. Held as plain strings in a list, a failure while verifying a grant would have sent every SCM_GIT_GRANT_SECRETS entry to Sentry in the frame's locals, and GitServer now refuses to be built with them. The setting stays a list of strings and each entry is wrapped where it's read. Refs CW-2222
The grant is an HS256 JWT. The comment named only the HMAC it costs, which read as if grants were bare HMAC signatures. Refs CW-2222
A GitHub Enterprise or self-managed GitLab host that resolves to an internal address was answered with a 503 but never logged, and GitServer counts it as an unhandled_exception. Nothing named the organization or repository, so a customer's misconfigured host looked like a bug in the endpoint. The view now logs it as restricted_ip_address with the blocked address and the request's context. Refs CW-2222
When control answered the token RPC with something other than an RPC response, such as a load balancer's error page, RpcResponseException escaped fetch_access_token and git showed a bare "HTTP 500". It's now handled like an unreachable control silo: reported to Sentry and answered as the 502 that says Sentry couldn't get a token. Refs CW-2222
Only the credential proxy Seer's coding jobs go through can reach the git endpoint, so a refused request is never a stranger knocking: a missing or unsigned grant, a disabled flag or a grant that doesn't match the request means Sentry or Seer is misconfigured. A warning log doesn't surface that, and Sentry's logging integration never turns a log into an issue, so the endpoint now captures each refusal explicitly and logs it at error level. The view's own refusals, the ones GitServer hands back through emit_error, and a provider host on an internal address share one helper, so all three report the same way. Issues group by refusal code, since the detail varies within a code, and carry the repository, organization, referrer and whatever else the request had resolved as tags, which the message leaves out.
The endpoint refused GitLab only because the token RPC left it out. #128762 added GitLab to the RPC, and sentry-scm already relays to a GitLab remote with the oauth2 user, so the refusal now only blocks repositories that work.
Anyone can write a referrer into a grant they sign themselves, and the referrer tags Datadog metrics and Sentry issues. Only a missing grant was checked for leaving it out; a forged one that carries a referrer was not.
Sentry's SDK config sets data_collection, which turns off the SDK's event scrubber while every frame's local variables are still sent. A refusal raised after the token lookup has GitServer.handle and GitRelay.forward on its stack, both holding the provider token, and a host on an internal address raises from inside requests, whose frames hold the Authorization header. capture_exception sent all of that. Each refusal is now a message grouped and tagged as before, which carries no stack. The internal-address report also leaves out the address, and an exception that isn't an SCMCodedError is reported by its name, since its message could hold a URL or a credential.
Seer signs git grants from its repository definitions, which hold only the provider and external id, and scm-platform covers a request only when the grant lists its repository in the same form. With Sentry's integer id in the URL, no grant Seer can sign would ever match. The repository is looked up by external id alone. The RPC lookup also matches a repository's name, which is chosen on the provider, so a repository named after a granted external id could stand in for the granted one. The RPC endpoint keeps matching names.
A git grant only ever names a repository by provider and external id, so the git endpoint has no use for fetch_repository's lookups by Sentry's id or by name. A flag on the shared lookup left those paths a default away. The git endpoint now looks repositories up with a function that has neither, and fetch_repository is back to what the RPC endpoint had. Both still build the repository the same way.
Looking up a GitLab repository fetched its integration from control directly, bypassing the cache it shares with the provider lookup, so every GitLab request made the same RPC twice.
A provider and external id don't identify a repository across hosts. GitHub Enterprise external ids are each server's own repository ids, so two servers connected to one organization number their repositories from the same range. Repository.id stays unique however Sentry comes to tell such repositories apart, so the URL and grants name it again, and Seer signs grants with the id Sentry already sends it for each repository. This takes back the provider and external id route and the lookup only it needed, and keeps fetching a GitLab repository's integration once per request.
Seer and the credential proxy send sentry-trace with every git request, without baggage, so a fetch or push should join the trace of the Seer step that ran it. The SDK's WSGI middleware does this already, and nothing on the endpoint's path starts a trace of its own. Sentry and getsentry don't set strict_trace_continuation, which would refuse a trace that comes without baggage. Tests run without an SDK client, so nothing showed it before. The test serves a fetch inside the middleware with a client configured like Sentry's. The provider request's span is in the caller's trace, under the caller's span, and is sampled as the caller's flag says or by Sentry's sampler without one. sentry-trace isn't passed on to the provider.
scm-platform now requires get_token to return an scm.Secret, so the token shows as [Filtered] in the frames of an error report instead of in plain text. GitServer refuses a plain string with a TypeError, which failed every relayed request against the unreleased 3.0.0 code.
Seer signs git grants and Sentry only verifies them. With a shared HS256 secret, anything that can read Sentry's settings could mint a grant for any connected repository. scm-platform 3.0.0 signs grants with Ed25519 instead: Seer holds the only private key, and Sentry lists the public keys, which aren't secret, as it already does for objectstore tokens and Relay. SCM_GIT_GRANT_SECRETS becomes SCM_GIT_GRANT_PUBLIC_KEYS. An empty or unusable key list is now answered with a 503 that names the setting, since scm-platform raises on it rather than refusing the grant, and it is Sentry's misconfiguration rather than a bad grant.
scm-platform's GitRelay now takes map_error, so BlacklistAdapter's RestrictedIPAddress becomes its git_remote_refused error. GitServer then counts, reports and answers it like any other refusal, with the operation, provider and host in the report, and the endpoint no longer needs its own handler. The answer moves from a 503 to a 502: retrying a refused host never helps.
Seer retries a 503 during checkout, so a 503 has to mean a retry can work. A misconfigured SCM_GIT_GRANT_PUBLIC_KEYS never fixes itself, so it's now a 500, like any other Sentry bug or misconfiguration. Control being unreachable while Sentry fetches a provider token usually passes, so it's now a 503 rather than the 502 GitServer gives a missing token.
A local Seer signs git grants with a dev-only private key committed to its .envrc. Listing the matching public key in Sentry's dev settings lets the two talk without each developer generating a key pair. Since that private key is public, only the dev guard sets it, and a test checks that the production default stays empty.
scm-platform now raises GitGrantKeyError for every unusable grant key, so the endpoint catches that from verify_grant instead of checking the key list separately for ValueError and TypeError. Tests make keys with generate_private_key and public_key_of rather than copying the cryptography calls. A provider host on an internal address now answers 500, as scm-platform maps git_remote_refused: Seer retries a 502, and no retry reaches a host Sentry refuses to connect to.
The git endpoint answered a misconfigured key list with scm-platform's exception message. That message names a bad key by its place in the list without quoting it, but the client can't fix Sentry's settings, and CodeQL flags exception text reaching a response. The client now gets a fixed message, and the log and the Sentry issue keep the detail.
vaind
force-pushed
the
ivan/scm-git-endpoint-view
branch
from
November 2, 2026 07:09
88b7086 to
5ac8a96
Compare
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seer's coding jobs need to clone, fetch and push customer repositories without ever holding a GitHub or GitLab token. This adds an internal endpoint that speaks git's HTTP protocol and relays each request to the provider, adding the org's integration token on the way:
Repositories are named by Sentry's
Repository.id, as in Seer's grants. Provider external ids aren't unique: two GitHub Enterprise servers connected to one org can reuse the same id.Each request carries a grant in
X-Scm-Git-Grant: a short-lived token, signed by Seer, for one org, one operation (fetch or push) and a list of repositories. The view verifies the grant, checks theorganizations:scm-git-endpointflag, and hands the request to scm-platform'sscm.git(sentry-scm 3.0.0). It refuses anything the grant doesn't cover before looking up the repository or minting a token. Refusals are plain text, which git shows asremote: <reason>, and each one is reported as a Sentry issue, grouped by reason: only Seer's credential proxy can reach this route, so a refusal means something is misconfigured.integration_service.get_access_token(feat(integrations): Add a provider access token RPC for cells #128490, feat(integrations): Add GitLab to the access token RPC #128762), for GitHub, GitHub Enterprise, GitLab and Cursor Origin. They're wrapped inscm.Secret, so error reports show[Filtered].BlacklistAdapter. Hosts on internal addresses and plain-http GitLab are refused.sentry-trace, which isn't passed on to the provider.Status codes follow how Seer's checkout (getsentry/seer#9229) reacts. It retries a 413, 429, 502, 503 or 504, and fails at once on anything else. So bad grants get a 403, and bugs or misconfiguration get a 500. An unreachable control silo gets a 503.
Grant keys
Grants are signed with Ed25519. Seer holds the only private key (
SCM_GIT_GRANT_SIGNING_KEY). Sentry lists the public keys inSCM_GIT_GRANT_PUBLIC_KEYSas PEM strings. They aren't secret, so nothing that reads Sentry's settings can mint a grant..envrc. Production and self-hosted keep the empty default, and a test checks that.Before an org gets the flag
Authorization./api/0/internal/scm-git/, that nginx and load-balancer timeouts allow transfers of several minutes, and that many large clones can't starve that pool's other traffic.Refs CW-2222