Skip to content

ref(seer): remove redundant SeerViewerContext from endpoint call sites - #121021

Draft
sehr-m wants to merge 3 commits into
masterfrom
sehr/viewer-context-endpoint-cleanup
Draft

ref(seer): remove redundant SeerViewerContext from endpoint call sites#121021
sehr-m wants to merge 3 commits into
masterfrom
sehr/viewer-context-endpoint-cleanup

Conversation

@sehr-m

@sehr-m sehr-m commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Remove explicit SeerViewerContext construction from ~19 endpoint files and their helpers — ViewerContextMiddleware already sets ViewerContext via contextvar, making these redundant
  • Add seer_path to the chokepoint observe_viewer_context_propagation("seer_rpc_out") call so missing-VC warnings identify which Seer API path is affected
  • Add has_project_id to VC observation metrics for completeness tracking
  • Task/consumer call sites keep their SeerViewerContext as a fallback until VC is hoisted at those infrastructure boundaries

This is the first step of the gradual migration discussed with Greg — endpoints are safe to migrate immediately because middleware guarantees VC. Future PRs will hoist VC at task/consumer entry points, using the chokepoint warnings to identify gaps.

Test plan

  • mypy passes on all changed files
  • ruff passes on all changed files
  • 121 targeted tests pass (signed_seer_api, agent_client, search_agent_start, search_agent_state, run_questions, supergroup endpoints)
  • 190 endpoint tests pass (seer endpoints, replay summary, external issues, similar issues embeddings, feedback endpoints)
  • CI

🤖 Generated with Claude Code

Endpoints already have ViewerContext set by ViewerContextMiddleware, so
explicitly constructing SeerViewerContext and threading it to Seer API
functions is redundant. Remove it from ~19 endpoint files and their
helpers, letting _resolve_viewer_context read from the contextvar.

Task/consumer call sites keep their SeerViewerContext as a fallback until
VC is hoisted at those infrastructure boundaries.

Add seer_path to the chokepoint observe_viewer_context_propagation call
so missing-VC warnings identify which Seer API path is affected. Add
has_project_id to observation metrics for completeness tracking.
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jul 31, 2026
sehr-m added 2 commits July 31, 2026 15:22
Adds a warning at the Seer RPC chokepoint when the resolved viewer
context has an organization_id but no project_id, so uncovered paths
are visible in logs.
The warning fires on nearly every Seer call because most endpoints are
org-scoped and legitimately don't have a single project. The
has_project_id metric tag in observe_viewer_context_propagation already
provides passive observability — filter by seer_path to identify gaps.
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Backend Test Failures

Failures on e6fb703 in this run:

tests/sentry/test_viewer_context.py::TestObserve::test_no_context_tags_actor_nonelog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/test_viewer_context.py:167: in test_no_context_tags_actor_none
    assert tags == {
E   AssertionError: assert {'actor_type'... 'false', ...} == {'actor_type'... 'false', ...}
E     
E     Omitting 6 identical items, use -vv to show
E     Left contains 1 more item:
E     �[0m{�[33m'�[39;49;00m�[33mhas_project_id�[39;49;00m�[33m'�[39;49;00m: �[33m'�[39;49;00m�[33mfalse�[39;49;00m�[33m'�[39;49;00m}�[90m�[39;49;00m
E     
E     Full diff:
E     �[0m�[90m �[39;49;00m {�[90m�[39;49;00m
E     �[90m �[39;49;00m     'actor_type': 'none',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'expected': 'true',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'has_org_id': 'false',�[90m�[39;49;00m
E     �[92m+     'has_project_id': 'false',�[39;49;00m�[90m�[39;49;00m
E     �[90m �[39;49;00m     'has_user_id': 'false',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'point': 'seer_rpc_out',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'sentry.client_sample_rate': 0.001,�[90m�[39;49;00m
E     �[90m �[39;49;00m }�[90m�[39;49;00m
tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py::TraceExplorerAIQueryTest::test_query_successfullog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_trace_explorer_ai_query.py:50: in test_query_successful
    mock_send_request.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: send_translate_request(4558580351762448, 'light-bengal', [4558580351762448], 'Find slow transactions', viewer_context={'organization_id': 4558580351762448, 'user_id': 483})
E     Actual: send_translate_request(4558580351762448, 'light-bengal', [4558580351762448], 'Find slow transactions')
tests/sentry/tasks/seer/test_night_shift.py::TestRunNightShiftFeatureDelivery::test_dispatches_candidates_to_seer_featurelog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/tasks/seer/test_night_shift.py:792: in test_dispatches_candidates_to_seer_feature
    assert outbox.payload["viewer_context"] == {"organization_id": org.id}
E   AssertionError: assert None == {'organization_id': 4558580354973728}
tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py::TraceExplorerAISetupTest::test_simplelog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py:24: in test_simple
    mock_fire_setup_request.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: fire_setup_request(4558580354187280, [4558580354187280], viewer_context={'organization_id': 4558580354187280, 'user_id': 496})
E     Actual: fire_setup_request(4558580354187280, [4558580354187280])
tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py::SearchAgentTranslateEndpointTest::test_translate_successfullog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_trace_explorer_ai_translate_agentic.py:48: in test_translate_successful
    mock_send_request.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: send_translate_agentic_request(4558580353597472, 'magnetic-kitten', [4558580353663008], 'Find slow transactions', strategy='Traces', model_name=None, metric_context=None, viewer_context={'organization_id': 4558580353597472, 'user_id': 526})
E     Actual: send_translate_agentic_request(4558580353597472, 'magnetic-kitten', [4558580353663008], 'Find slow transactions', strategy='Traces', model_name=None, metric_context=None)
tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py::TraceExplorerAISetupTest::test_empty_projects_still_calls_seerlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_trace_explorer_ai_setup.py:79: in test_empty_projects_still_calls_seer
    mock_fire_setup_request.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: fire_setup_request(4558580353466400, [], viewer_context={'organization_id': 4558580353466400, 'user_id': 538})
E     Actual: fire_setup_request(4558580353466400, [])

agent_run_options["enable_streaming"] = True

response = make_agent_chat_request(chat_body, viewer_context=self.viewer_context)
response = make_agent_chat_request(chat_body)

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.

Background continue_run callers lose X-Viewer-Context header to Seer

Dropping viewer_context=self.viewer_context here also strips the viewer header for task/consumer callers of SeerExplorerClient.continue_run (e.g. dashboards/on_completion_hook.py, seer/autofix/autofix_agent.py, seer/entrypoints/operator.py), where the middleware contextvar is not set — contradicting the PR's claim that task call sites keep a fallback — so Seer receives those requests with no org/user attribution.

Evidence
  • SeerExplorerClient.continue_run now calls make_agent_chat_request(chat_body) with no viewer_context, and the same pattern applies to make_agent_runs_request/make_agent_update_request (client.py:903,975).
  • _resolve_viewer_context(None) in signed_seer_api.py:56 returns get_viewer_context(), which is None outside request middleware; when resolved is None the X-Viewer-Context header is simply not added (make_signed_seer_api_request).
  • continue_run is called from non-endpoint paths: dashboards/on_completion_hook.py:262 (task, builds client with user=None), seer/autofix/autofix_agent.py:485, and seer/entrypoints/operator.py:553, none of which are covered by ViewerContextMiddleware.
  • Previously self.viewer_context (built from the client's organization/user) was always sent, so Seer-side attribution/scoping was preserved even in background contexts; the PR body states VC is not yet hoisted at task/consumer boundaries.

Identified by Warden · security-review · 37R-U7N

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.

1 participant