feat(issues): Model Sentry App and org callers as distinct action-log actors#117354
Merged
Conversation
… actors A token-authenticated request can come from a Sentry App (its proxy user) or an org-scoped token, but the boundary code recorded all of them as USER(proxy_user_id) -- a machine mislabeled as a human. The real distinction is *who* made the call, not *how* (all arrive over source=api), so it belongs on the actor axis. Extend GroupActorType with SENTRY_APP (actor_id = SentryApp id) and ORG (actor_id = organization id), and add a resolve_action_actor(request) boundary helper mirroring resolve_action_source: org/legacy tokens -> ORG, integration tokens -> SENTRY_APP (resolved via application_id), everything else authenticated -> USER, else SYSTEM. internal-vs-public app is derived from SentryApp.status at read time, so both map to SENTRY_APP. Endpoints now set actor=resolve_action_actor(request) instead of hand-rolling the user/SYSTEM check. Add actor_type as a low-cardinality metric dimension. Fixes ID-1620
Gate the app lookup on request.user.is_sentry_app rather than application_id alone. An OAuth client acting on behalf of a user (e.g. the MCP) also carries an application_id but authenticates as the real user, so it must resolve to USER -- and gating on application_id would run an app lookup (uncached on a None result) on every such request. is_sentry_app is true only for an app's proxy user, so only genuine app-as-itself tokens pay the cached lookup.
0b0eb91 to
4e3e916
Compare
kcons
approved these changes
Jun 11, 2026
| return ActionSource.API | ||
|
|
||
|
|
||
| def resolve_action_actor(request: Request) -> GroupActionActor: |
| return GroupActionActor.user(user_id) | ||
|
|
||
| user = getattr(request, "user", None) | ||
| if user is not None and getattr(user, "is_authenticated", False): |
Member
There was a problem hiding this comment.
do we have to lean on getattr this much? I recall resolving some mypy confusion my just using the attribute directly, but maybe I was contextually lucky?
My brain just signals warning lights whenever I see getattr as it's as dynamic as possible, and mypy usually gives up and gives you an Any.
Member
Author
There was a problem hiding this comment.
hmm no, I realize now I just went with the AI but it went overboard here and I didnt notice. We dont need many of these, and actually even the places where type isnt obvious we can use isinstance which is prbably better, gonna refactor
…ion_actor Narrow request.auth to AuthenticatedToken and request.user to User/RpcUser so mypy type-checks the field accesses instead of getting Any from getattr. No behavior change. app_service stays a function-local import to avoid a circular import.
…ctor-types # Conflicts: # tests/sentry/issues/test_action_log.py
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.
Problem
A token-authenticated request can come from a Sentry App (authenticating as its proxy user, a real
Userrow withis_sentry_app=True) or an org-scoped token. The action-log boundary recorded all of these asUSER(proxy_user_id)— a machine mislabeled as a human. The distinction between custom-integration vs public-integration vs org-token vs personal-user-token is about who made the call, not how (they all arrive oversource=api), so it belongs on the actor axis, not source.Change
Extend
GroupActorTypewithSENTRY_APP(actor_id= SentryApp id) andORG(actor_id= organization id), plusGroupActionActor.sentry_app(...)/.org(...)constructors.Add a
resolve_action_actor(request)boundary helper mirroringresolve_action_source(request). Region-siderequest.authis anAuthenticatedTokenwhosekinddrives classification:org_auth_token/ legacyapi_keyORG(organization_id)api_tokenwithapplication_idSENTRY_APP(sentry_app_id)(resolved viaapp_service.get_by_application_id)api_tokenw/oapplication_id, or sessionUSER(user_id)SYSTEMEndpoints now set
actor=resolve_action_actor(request)instead of hand-rolling theuser.is_authenticated ? USER : SYSTEMcheck (group_details,group_integration_details×3,group_ai_autofix, bulk statusupdate).Add
actor_typeas a low-cardinality metric dimension onissues.action_log.Notes
SENTRY_APPtype —internalis justSentryApp.status == INTERNAL, derived fromactor_idat read time. No separate type.actor_id), not in a metric tag.apisource bucket —sourcestays the channel, the caller question is answered byactor_type.Tests
TestResolveActionActorcovers user (session + personal token), Sentry App token (realapp_serviceresolution), org-auth-token, legacy api-key, and unauthenticated → SYSTEM.Fixes ID-1620