Skip to content

feat(hybridcloud): Propagate ViewerContext through cross-silo RPC#112248

Merged
gricha merged 5 commits intomasterfrom
gricha/feat/rpc-viewer-context-propagation
Apr 7, 2026
Merged

feat(hybridcloud): Propagate ViewerContext through cross-silo RPC#112248
gricha merged 5 commits intomasterfrom
gricha/feat/rpc-viewer-context-propagation

Conversation

@gricha
Copy link
Copy Markdown
Member

@gricha gricha commented Apr 3, 2026

Pack ViewerContext into the RPC meta dict on the sending side (_send_to_remote_silo) and restore it via viewer_context_scope on the receiving side (InternalRpcServiceEndpoint).

Previously, cross-silo RPC handlers had no ViewerContext — the middleware saw RPC signature auth, not the original user. The real user identity was only available through AuthenticationContext (which most RPC calls don't pass). Now the contextvar is automatically propagated through meta, which was already reserved in the wire format for exactly this kind of use.

Backwards compatible — old callers send meta: {}, receivers treat missing viewer_context as None.

Also adds ViewerContext.deserialize() as the inverse of serialize().

Part of the ViewerContext RFC.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 3, 2026
@gricha gricha marked this pull request as ready for review April 4, 2026 05:32
@gricha gricha requested review from a team as code owners April 4, 2026 05:32
return settings.RPC_TIMEOUT

def _send_to_remote_silo(self, use_test_client: bool) -> Any:
from sentry.viewer_context import get_viewer_context
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a cyclic import here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no, fixed!

gricha and others added 3 commits April 7, 2026 09:50
Pack ViewerContext into the RPC meta dict on the sending side and
restore it via viewer_context_scope on the receiving side. This closes
the gap where cross-silo RPC handlers had no ViewerContext despite
the original request having one.

Uses the existing reserved meta key in the RPC wire format. Backwards
compatible — old callers send empty meta, receivers treat missing
viewer_context as None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…port

Wrap ViewerContext.deserialize() in try/except to return ParseError on
malformed viewer_context data, matching the existing AuthenticationContext
error handling pattern. Move get_viewer_context import to top-level in
service.py since there is no circular dependency.

Addresses PR review feedback from markstory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
request.data.get("meta", {}) returns None when the payload contains
"meta": null, since the default only applies for missing keys. Use
`or {}` to handle both missing and null values, preventing an
AttributeError on the subsequent .get() call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gricha gricha force-pushed the gricha/feat/rpc-viewer-context-propagation branch from efc3b5a to 1fa13d4 Compare April 7, 2026 16:50
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Backend Test Failures

Failures on 3545249 in this run:

tests/sentry/api/endpoints/test_rpc.py::RpcServiceEndpointTest::test_viewer_context_none_when_meta_emptylog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/api/endpoints/test_rpc.py:211: in test_viewer_context_none_when_meta_empty
    assert captured_contexts[0] is None
E   AssertionError: assert ViewerContext(organization_id=None, user_id=None, actor_type=<ActorType.USER: 'user'>, token=None) is None

gricha and others added 2 commits April 7, 2026 11:59
The RPC endpoint ran inside ViewerContextMiddleware, which sets a
ViewerContext for the transport request itself. When no viewer_context
was provided in meta, nullcontext() left the middleware's context
active, leaking incorrect identity to the RPC handler.

Always enter viewer_context_scope with either the caller's deserialized
context or an empty ViewerContext(actor_type=UNKNOWN) to override
the middleware's context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d1e6d1a. Configure here.


try:
with auth_context.applied_to_request(request):
with viewer_context_scope(vc), auth_context.applied_to_request(request):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Default ViewerContext always set, breaking None semantics

Low Severity

When no viewer_context is present in meta, the code unconditionally creates a default ViewerContext() and enters viewer_context_scope(vc). This means get_viewer_context() inside RPC dispatch now returns a non-None ViewerContext instance instead of None. Downstream, _resolve_viewer_context in signed_seer_api.py checks vc is None — a default ViewerContext() is truthy and not None, causing Seer API calls from RPC handlers to start sending X-Viewer-Context headers with {"actor_type":"unknown"} where no header was previously sent. Wrapping in viewer_context_scope only when vc_data is present would preserve the prior None semantics.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1e6d1a. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The intention is for viewer context to be truthy, the issue is on the seer rpc side and I'll fix it there.

Copy link
Copy Markdown
Member

@markstory markstory left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@gricha gricha merged commit f72145f into master Apr 7, 2026
79 checks passed
@gricha gricha deleted the gricha/feat/rpc-viewer-context-propagation branch April 7, 2026 20:55
george-sentry pushed a commit that referenced this pull request Apr 9, 2026
…12248)

Pack ViewerContext into the RPC `meta` dict on the sending side
(`_send_to_remote_silo`) and restore it via `viewer_context_scope` on
the receiving side (`InternalRpcServiceEndpoint`).

Previously, cross-silo RPC handlers had no ViewerContext — the
middleware saw RPC signature auth, not the original user. The real user
identity was only available through `AuthenticationContext` (which most
RPC calls don't pass). Now the contextvar is automatically propagated
through `meta`, which was already reserved in the wire format for
exactly this kind of use.

Backwards compatible — old callers send `meta: {}`, receivers treat
missing `viewer_context` as `None`.

Also adds `ViewerContext.deserialize()` as the inverse of `serialize()`.

Part of the [ViewerContext
RFC](https://www.notion.so/sentry/RFC-Unified-ViewerContext-via-ContextVar-32f8b10e4b5d81988625cb5787035e02).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants