Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/sentry/api/endpoints/organization_events_trends_v2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from functools import partial

import sentry_sdk
from rest_framework.exceptions import ParseError
Expand All @@ -17,7 +16,6 @@
from sentry.ratelimits.config import RateLimitConfig
from sentry.search.events.constants import METRICS_GRANULARITIES
from sentry.seer.breakpoints import detect_breakpoints
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.snuba import metrics_performance
from sentry.snuba.discover import create_result_key, zerofill
from sentry.snuba.metrics_performance import query as metrics_query
Expand Down Expand Up @@ -69,8 +67,6 @@ class OrganizationEventsNewTrendsStatsEndpoint(OrganizationEventsEndpointBase):
)

def get(self, request: Request, organization: Organization) -> Response:
viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id)

try:
snuba_params = self.get_snuba_params(request, organization)
except NoProjects:
Expand Down Expand Up @@ -284,7 +280,7 @@ def get_trends_data(stats_data, request):
) as query_thread_pool:
results = list(
query_thread_pool.map(
partial(detect_breakpoints, viewer_context=viewer_context),
detect_breakpoints,
trends_requests,
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from sentry.search.events.builder.profile_functions import ProfileTopFunctionsTimeseriesQueryBuilder
from sentry.search.events.types import QueryBuilderConfig
from sentry.seer.breakpoints import BreakpointData, BreakpointRequest, detect_breakpoints
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.snuba import functions
from sentry.snuba.dataset import Dataset
from sentry.snuba.referrer import Referrer
Expand Down Expand Up @@ -82,8 +81,6 @@ def get(self, request: Request, organization: Organization) -> Response:
if not self.has_feature(organization, request):
return Response(status=404)

viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id)

try:
snuba_params = self.get_snuba_params(request, organization)
except NoProjects:
Expand Down Expand Up @@ -189,7 +186,7 @@ def get_trends_data(stats_data) -> list[BreakpointData]:
"sort": data["trend"].as_sort(),
}

return detect_breakpoints(trends_request, viewer_context=viewer_context)["data"]
return detect_breakpoints(trends_request)["data"]

stats_data = self.get_event_stats_data(
request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from sentry.search.eap.utils import can_expose_attribute, translate_internal_to_public_alias
from sentry.search.events import fields
from sentry.seer.endpoints.compare import compare_distributions
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.snuba.referrer import Referrer
from sentry.snuba.spans_rpc import Spans
from sentry.utils import snuba_rpc
Expand Down Expand Up @@ -105,7 +104,6 @@ def get(self, request: Request, organization: Organization) -> Response:
{"referrer": Referrer.API_TRACE_EXPLORER_STATS.value},
)

viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id)
scored_attrs_rrr = compare_distributions(
baseline=cohort_2_distribution,
outliers=cohort_1_distribution,
Expand All @@ -118,7 +116,6 @@ def get(self, request: Request, organization: Organization) -> Response:
meta={
"referrer": Referrer.API_TRACE_EXPLORER_STATS.value,
},
viewer_context=viewer_context,
)
logger.info("scored_attrs_rrr: %s", scored_attrs_rrr)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from sentry.grouping.utils import hash_from_values
from sentry.models.organization import Organization
from sentry.seer.seer_setup import has_seer_access
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.utils.cache import cache

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -108,8 +107,6 @@ def get(self, request: Request, organization: Organization) -> Response:
{"detail": "AI categorization is not available for this organization."}, status=403
)

viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id)

try:
start, end = get_date_range_from_stats_period(
request.GET,
Expand Down Expand Up @@ -195,7 +192,6 @@ def get(self, request: Request, organization: Organization) -> Response:
seer_request,
timeout=SEER_TIMEOUT_S,
retries=SEER_RETRIES,
viewer_context=viewer_context,
)
except Exception:
logger.exception("Seer failed to generate user feedback label groups")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from sentry.models.group import Group, GroupStatus
from sentry.models.organization import Organization
from sentry.seer.seer_setup import has_seer_access
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.utils import metrics
from sentry.utils.cache import cache

Expand All @@ -38,15 +37,13 @@

def get_summary_from_seer(
feedback_msgs: list[str],
viewer_context: SeerViewerContext | None = None,
) -> str | None:
request_body = SummarizeFeedbacksRequest(feedbacks=feedback_msgs)
try:
response = make_summarize_feedbacks_request(
request_body,
timeout=SEER_TIMEOUT_S,
retries=SEER_RETRIES,
viewer_context=viewer_context,
)
except Exception:
logger.exception(
Expand Down Expand Up @@ -95,8 +92,6 @@ def get(self, request: Request, organization: Organization) -> Response:
{"detail": "AI summaries are not available for this organization."}, status=403
)

viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id)

try:
start, end = get_date_range_from_stats_period(
request.GET,
Expand Down Expand Up @@ -162,7 +157,7 @@ def get(self, request: Request, organization: Organization) -> Response:
if len(feedback_msgs) < MIN_FEEDBACKS_TO_SUMMARIZE:
logger.error("Too few feedbacks to summarize after enforcing the character limit")

summary = get_summary_from_seer(feedback_msgs, viewer_context=viewer_context)
summary = get_summary_from_seer(feedback_msgs)
if summary is None:
return Response(
{"detail": "Failed to generate a summary for a list of feedbacks"}, status=500
Expand Down
12 changes: 4 additions & 8 deletions src/sentry/integrations/utils/external_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from sentry.models.group import Group
from sentry.seer.signed_seer_api import (
LlmGenerateRequest,
SeerViewerContext,
make_llm_generate_request,
)
from sentry.services.eventstore.models import GroupEvent
Expand Down Expand Up @@ -63,9 +62,9 @@ class GeneratedExternalIssueDetails(TypedDict):


def _make_generate_external_issue_details_request(
group: Group, event: Any | None = None, viewer_context: SeerViewerContext | None = None
group: Group, event: Any | None = None
) -> GeneratedExternalIssueDetails | None:
logging_ctx: dict[str, Any] = {"group_id": group.id, "viewer_context": viewer_context}
logging_ctx: dict[str, Any] = {"group_id": group.id}
context = _build_event_context(group, event=event)

body = LlmGenerateRequest(
Expand All @@ -85,7 +84,7 @@ def _make_generate_external_issue_details_request(
"required": ["title", "description"],
},
)
response = make_llm_generate_request(body, timeout=10, viewer_context=viewer_context)
response = make_llm_generate_request(body, timeout=10)
logging_ctx["status_code"] = response.status
if response.status >= 400:
logger.warning("external_issues.seer_request_failed", extra=logging_ctx)
Expand Down Expand Up @@ -131,10 +130,7 @@ def maybe_generate_external_issue_details(
return empty_result

try:
viewer_context = SeerViewerContext(organization_id=organization.id, user_id=user.id)
result = _make_generate_external_issue_details_request(
group, event=event, viewer_context=viewer_context
)
result = _make_generate_external_issue_details_request(group, event=event)
except Exception:
logger.error("external_issues.generate_issue_details_failed", exc_info=True)
return empty_result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from sentry.issues.endpoints.bases.group import GroupEndpoint
from sentry.models.group import Group
from sentry.models.grouphash import GroupHash
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.seer.similarity.config import get_grouping_model_version, should_skip_seer_fallback
from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer
from sentry.seer.similarity.types import SeerSimilarIssueData, SimilarIssuesEmbeddingsRequest
Expand Down Expand Up @@ -136,12 +135,7 @@ def get(self, request: Request, group: Group) -> Response:

logger.info("Similar issues embeddings parameters", extra=similar_issues_params)

viewer_context = SeerViewerContext(
organization_id=group.project.organization.id, user_id=request.user.id
)
results, _model_used = get_similarity_data_from_seer(
similar_issues_params, viewer_context=viewer_context
)
results, _model_used = get_similarity_data_from_seer(similar_issues_params)

analytics.record(
GroupSimilarIssuesEmbeddingsCountEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def get(self, request: Request, organization: Organization) -> Response:
# breaking the stream.
try:
supergroup_data = get_supergroups_by_group_ids(
organization, [int(g["id"]) for g in groups], user_id=request.user.id
organization, [int(g["id"]) for g in groups]
)
except SeerApiError:
logger.exception("issues_with_supergroups.seer_fetch_failed")
Expand Down
16 changes: 1 addition & 15 deletions src/sentry/replays/endpoints/project_replay_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from sentry.replays.post_process import process_raw_response
from sentry.replays.query import query_replay_instance
from sentry.seer.seer_setup import has_seer_access
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.utils.tracing import start_span

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -68,7 +67,6 @@ def __init__(self, **kw) -> None:
def _make_seer_start_request(
self,
body: ReplaySummaryStartRequest,
viewer_context: SeerViewerContext | None = None,
) -> Response:
"""Make a start-summary request to Seer with error handling."""
serialized = orjson.dumps(body)
Expand All @@ -89,7 +87,6 @@ def _make_seer_start_request(
body,
timeout=5,
retries=0,
viewer_context=viewer_context,
)
except Exception:
logger.exception(
Expand All @@ -114,15 +111,13 @@ def _make_seer_start_request(
def _make_seer_state_request(
self,
body: ReplaySummaryStateRequest,
viewer_context: SeerViewerContext | None = None,
) -> Response:
"""Make a poll-state request to Seer with error handling."""
try:
response = make_replay_summary_state_request(
body,
timeout=5,
retries=0,
viewer_context=viewer_context,
)
except Exception:
logger.exception(
Expand Down Expand Up @@ -177,18 +172,13 @@ def get(self, request: Request, project: Project, replay_id: str) -> Response:
# Since this endpoint is polled, we skip checking Seer permissions here for performance.
# Both the frontend and summary generation are gated by the same permissions.

viewer_context = SeerViewerContext(
organization_id=project.organization_id, user_id=request.user.id
)

# Request Seer for the state of the summary task.
return self._make_seer_state_request(
ReplaySummaryStateRequest(
replay_id=replay_id,
organization_id=project.organization.id,
project_id=project.id,
),
viewer_context=viewer_context,
)

def post(self, request: Request, project: Project, replay_id: str) -> Response:
Expand Down Expand Up @@ -282,8 +272,4 @@ def validate_iso_timestamp(timestamp: str | None) -> str | None:
if temperature is not None:
start_request["temperature"] = temperature

viewer_context = SeerViewerContext(
organization_id=project.organization_id, user_id=request.user.id
)

return self._make_seer_start_request(start_request, viewer_context=viewer_context)
return self._make_seer_start_request(start_request)
24 changes: 6 additions & 18 deletions src/sentry/seer/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
)
from sentry.seer.models.run import SeerAgentRun, SeerRun, SeerRunType
from sentry.seer.seer_setup import has_seer_access_with_detail
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.tasks.seer.context_engine_index import build_service_map, index_org_project_knowledge
from sentry.tasks.seer.explorer_index import dispatch_explorer_index_projects
from sentry.users.models.user import User
Expand Down Expand Up @@ -360,8 +359,6 @@

self.enable_pr_context_tools = enable_pr_context_tools

self.viewer_context = self._build_viewer_context()

# Validate that category_key and category_value are provided together
if category_key == "" or category_value == "":
raise ValueError("category_key and category_value cannot be empty strings")
Expand All @@ -373,12 +370,6 @@
if not has_access:
raise SeerPermissionError(error or "Access denied")

def _build_viewer_context(self) -> SeerViewerContext:
context = SeerViewerContext(organization_id=self.organization.id)
if self.user and hasattr(self.user, "id") and self.user.id is not None:
context["user_id"] = self.user.id
return context

def start_run(
self,
prompt: str,
Expand Down Expand Up @@ -518,7 +509,6 @@
run_type=SeerRunType.EXPLORER,
body=chat_body,
on_run_created=_create_agent_run,
viewer_context=self.viewer_context,
user_id=user_id,
referrer=metadata.get("referrer") if metadata else None,
flush=True,
Expand Down Expand Up @@ -576,7 +566,6 @@
payload=payload,
agent_run_options=self._build_agent_run_options(),
),
viewer_context=self.viewer_context,
user_id=user_id,
referrer=feature_id,
flush=flush,
Expand Down Expand Up @@ -775,7 +764,7 @@
):
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)

Check notice on line 767 in src/sentry/seer/agent/client.py

View check run for this annotation

@sentry/warden / warden: security-review

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.

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


if response.status >= 400:
raise SeerApiError("Seer request failed", response.status)
Expand Down Expand Up @@ -813,10 +802,9 @@
self.organization,
poll_interval,
poll_timeout,
viewer_context=self.viewer_context,
)
else:
state = fetch_run_status(run_id, self.organization, viewer_context=self.viewer_context)
state = fetch_run_status(run_id, self.organization)

return state

Expand Down Expand Up @@ -912,7 +900,7 @@
if query is not None:
runs_body["query"] = query

response = make_agent_runs_request(runs_body, viewer_context=self.viewer_context)
response = make_agent_runs_request(runs_body)

if response.status >= 400:
raise SeerApiError("Seer request failed", response.status)
Expand All @@ -927,7 +915,7 @@
run_id=run_id,
organization_id=self.organization.id,
)
return make_agent_repos_request(body, viewer_context=self.viewer_context)
return make_agent_repos_request(body)

def push_changes(
self,
Expand Down Expand Up @@ -984,7 +972,7 @@
organization_id=self.organization.id,
payload=payload,
)
response = make_agent_update_request(update_body, viewer_context=self.viewer_context)
response = make_agent_update_request(update_body)
if response.status >= 400:
raise SeerApiError("Seer request failed", response.status)

Expand All @@ -995,7 +983,7 @@
start_time = time.time()

while True:
state = fetch_run_status(run_id, self.organization, viewer_context=self.viewer_context)
state = fetch_run_status(run_id, self.organization)

# Check if any PRs are still being created
any_creating = any(
Expand Down
Loading
Loading