From 2b7f674ad02fd1f9ebd3bd297b13a7119cab6754 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:48:22 -0700 Subject: [PATCH] fix(seer): Keep service account actors when resolving Seer viewer context An explicit SeerViewerContext user_id replaced a service-account actor with that user, so Seer's echoed context authenticated as a human. --- src/sentry/seer/signed_seer_api.py | 6 ++++-- tests/sentry/seer/test_signed_seer_api.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index ffe8718d0daa..0ba8fb954f90 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -117,7 +117,9 @@ def _resolve_viewer_context( org_id = explicit_org_id if explicit_user_id is not None: - if user_id is not None and user_id != explicit_user_id: + # A service account is its own principal; never re-attribute it to a human. + is_service_account = actor.type == ActorType.SERVICE_ACCOUNT + if (user_id is not None or is_service_account) and user_id != explicit_user_id: logger.warning( "seer.viewer_context_mismatch", extra={ @@ -132,7 +134,7 @@ def _resolve_viewer_context( actor = replace(actor, actor_id=explicit_user_id) elif actor.type == ActorType.AGENT: actor = replace(actor, delegated_user_id=explicit_user_id) - else: + elif not is_service_account: actor = Actor(type=ActorType.USER, actor_id=explicit_user_id) metrics.incr( diff --git a/tests/sentry/seer/test_signed_seer_api.py b/tests/sentry/seer/test_signed_seer_api.py index bc663e930b8d..f80fc3c70e2e 100644 --- a/tests/sentry/seer/test_signed_seer_api.py +++ b/tests/sentry/seer/test_signed_seer_api.py @@ -350,6 +350,28 @@ def test_agent_mismatch_warns_and_keeps_agent(self, mock_logger: MagicMock) -> N "user_id", } + @patch("sentry.seer.signed_seer_api.logger") + def test_service_account_is_not_reattributed_to_user(self, mock_logger: MagicMock) -> None: + ctx = ViewerContext( + actor=Actor( + type=ActorType.SERVICE_ACCOUNT, + actor_id=5, + superuser=SuperuserAccess(expires_at=1234567890), + ), + organization_id=42, + project_id=3, + ) + with viewer_context_scope(ctx): + result = _resolve_viewer_context(SeerViewerContext(organization_id=42, user_id=99)) + + assert isinstance(result, ViewerContext) + assert result.actor.type == ActorType.SERVICE_ACCOUNT + assert result.actor.actor_id == 5 + assert result.actor.superuser is None + assert result.project_id is None + mock_logger.warning.assert_called_once() + assert mock_logger.warning.call_args[1]["extra"]["field"] == "user_id" + @patch("sentry.seer.signed_seer_api.metrics") def test_match_emits_metric_with_project_id(self, mock_metrics: MagicMock) -> None: ctx = ViewerContext(