From 80e14c4c0e206475a9d54a41fccf2b6565d2c712 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Fri, 3 Apr 2026 15:23:35 -0700 Subject: [PATCH 1/5] feat(assisted-query): Add metrics search feature flag and forward options Register gen-ai-explore-metrics-search feature flag to gate the metrics AI search bar independently from the shared gen-ai-search-agent-translate flag. Forward the full options dict to Seer instead of extracting only model_name, so strategy-specific context like metric_context can pass through. Co-Authored-By: Claude Opus 4.6 --- src/sentry/features/temporary.py | 2 ++ .../seer/endpoints/search_agent_start.py | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 00f103ab2aebce..44f806d1b6d3aa 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -124,6 +124,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:gen-ai-features", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the 'translate' functionality for GenAI on the explore > traces page manager.add("organizations:gen-ai-search-agent-translate", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable AI search bar on the explore > metrics tab + manager.add("organizations:gen-ai-explore-metrics-search", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable GenAI consent manager.add("organizations:gen-ai-consent", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable increased issue_owners rate limit for auto-assignment diff --git a/src/sentry/seer/endpoints/search_agent_start.py b/src/sentry/seer/endpoints/search_agent_start.py index 4d221cef2fe703..44ad31cff2a6f8 100644 --- a/src/sentry/seer/endpoints/search_agent_start.py +++ b/src/sentry/seer/endpoints/search_agent_start.py @@ -66,7 +66,7 @@ def send_search_agent_start_request( strategy: str = "Traces", user_email: str | None = None, timezone: str | None = None, - model_name: str | None = None, + options: dict[str, Any] | None = None, viewer_context: SeerViewerContext | None = None, ) -> dict[str, Any]: """ @@ -83,10 +83,6 @@ def send_search_agent_start_request( body["user_email"] = user_email if timezone: body["timezone"] = timezone - - options: dict[str, Any] = {} - if model_name is not None: - options["model_name"] = model_name if options: body["options"] = options @@ -128,16 +124,22 @@ def post(self, request: Request, organization: Organization) -> Response: natural_language_query = validated_data["natural_language_query"] strategy = validated_data.get("strategy", "Traces") options = validated_data.get("options") or {} - model_name = options.get("model_name") projects = self.get_projects( request, organization, project_ids=set(validated_data["project_ids"]) ) project_ids = [project.id for project in projects] - if not features.has( + has_feature = features.has( "organizations:gen-ai-search-agent-translate", organization, actor=request.user - ): + ) + if strategy == "Metrics": + has_feature = has_feature or features.has( + "organizations:gen-ai-explore-metrics-search", + organization, + actor=request.user, + ) + if not has_feature: return Response( {"detail": "Feature flag not enabled"}, status=status.HTTP_403_FORBIDDEN, @@ -173,7 +175,7 @@ def post(self, request: Request, organization: Organization) -> Response: strategy=strategy, user_email=user_email, timezone=timezone, - model_name=model_name, + options=options if options else None, viewer_context=viewer_context, ) From c143df66e3a27e76179a8c7efa47152930f683e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 3 Apr 2026 22:34:02 +0000 Subject: [PATCH 2/5] fix(seer): Add gen-ai-explore-metrics-search flag check to state endpoint The start endpoint allows access with either gen-ai-search-agent-translate or gen-ai-explore-metrics-search flag when strategy is Metrics, but the state endpoint only checked gen-ai-search-agent-translate. This caused users with only the metrics flag to successfully start a search agent but get 403 errors when polling for state. Since the state endpoint doesn't know which strategy was used, it now accepts either flag to match the start endpoint's behavior. Co-authored-by: Armen Zambrano G. Applied via @cursor push command --- src/sentry/seer/endpoints/search_agent_state.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/endpoints/search_agent_state.py b/src/sentry/seer/endpoints/search_agent_state.py index 9200eb7c901f73..b685ae7d8fd252 100644 --- a/src/sentry/seer/endpoints/search_agent_state.py +++ b/src/sentry/seer/endpoints/search_agent_state.py @@ -81,9 +81,12 @@ def get(self, request: Request, organization: Organization, run_id: str) -> Resp } } """ - if not features.has( + has_feature = features.has( "organizations:gen-ai-search-agent-translate", organization, actor=request.user - ): + ) or features.has( + "organizations:gen-ai-explore-metrics-search", organization, actor=request.user + ) + if not has_feature: return Response( {"detail": "Feature flag not enabled"}, status=status.HTTP_403_FORBIDDEN, From e4e0da628ba139d15c9fe1e6c34c7f944e601742 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Wed, 8 Apr 2026 15:40:39 -0700 Subject: [PATCH 3/5] fix(seer): Use AND for feature flags so translate flag acts as killswitch Change the state endpoint feature flag check from OR to AND so that gen-ai-search-agent-translate must always be enabled for access. This lets the translate flag serve as a global killswitch for all search agent functionality. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry/seer/endpoints/search_agent_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/seer/endpoints/search_agent_state.py b/src/sentry/seer/endpoints/search_agent_state.py index b685ae7d8fd252..13e7c08f1b7935 100644 --- a/src/sentry/seer/endpoints/search_agent_state.py +++ b/src/sentry/seer/endpoints/search_agent_state.py @@ -83,7 +83,7 @@ def get(self, request: Request, organization: Organization, run_id: str) -> Resp """ has_feature = features.has( "organizations:gen-ai-search-agent-translate", organization, actor=request.user - ) or features.has( + ) and features.has( "organizations:gen-ai-explore-metrics-search", organization, actor=request.user ) if not has_feature: From 77da5a610dd06f40ec49e438b6640db6ab069b1f Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Fri, 10 Apr 2026 09:55:37 -0700 Subject: [PATCH 4/5] fix(assisted-query): Use AND for start endpoint, OR for state endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start endpoint: change OR to AND so gen-ai-search-agent-translate acts as a global killswitch — metrics search requires both flags. State endpoint: change AND to OR since it is strategy-agnostic and only takes a run_id. Traces users with only the translate flag must still be able to poll for results. Co-Authored-By: Claude Opus 4.6 --- src/sentry/seer/endpoints/search_agent_start.py | 2 +- src/sentry/seer/endpoints/search_agent_state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/endpoints/search_agent_start.py b/src/sentry/seer/endpoints/search_agent_start.py index 44ad31cff2a6f8..a194b3e5cfdc6c 100644 --- a/src/sentry/seer/endpoints/search_agent_start.py +++ b/src/sentry/seer/endpoints/search_agent_start.py @@ -134,7 +134,7 @@ def post(self, request: Request, organization: Organization) -> Response: "organizations:gen-ai-search-agent-translate", organization, actor=request.user ) if strategy == "Metrics": - has_feature = has_feature or features.has( + has_feature = has_feature and features.has( "organizations:gen-ai-explore-metrics-search", organization, actor=request.user, diff --git a/src/sentry/seer/endpoints/search_agent_state.py b/src/sentry/seer/endpoints/search_agent_state.py index 13e7c08f1b7935..b685ae7d8fd252 100644 --- a/src/sentry/seer/endpoints/search_agent_state.py +++ b/src/sentry/seer/endpoints/search_agent_state.py @@ -83,7 +83,7 @@ def get(self, request: Request, organization: Organization, run_id: str) -> Resp """ has_feature = features.has( "organizations:gen-ai-search-agent-translate", organization, actor=request.user - ) and features.has( + ) or features.has( "organizations:gen-ai-explore-metrics-search", organization, actor=request.user ) if not has_feature: From 151c18f396cb0a767cf00f40b76f53467960e20d Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Fri, 10 Apr 2026 12:26:24 -0700 Subject: [PATCH 5/5] ref(assisted-query): Extract known options fields explicitly Instead of forwarding the raw options dict to Seer, extract only model_name and metric_context. This prevents unknown fields from passing through the unvalidated DictField serializer. Co-Authored-By: Claude Opus 4.6 --- src/sentry/seer/endpoints/search_agent_start.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/endpoints/search_agent_start.py b/src/sentry/seer/endpoints/search_agent_start.py index a194b3e5cfdc6c..2d34d8c71b9ef8 100644 --- a/src/sentry/seer/endpoints/search_agent_start.py +++ b/src/sentry/seer/endpoints/search_agent_start.py @@ -66,7 +66,8 @@ def send_search_agent_start_request( strategy: str = "Traces", user_email: str | None = None, timezone: str | None = None, - options: dict[str, Any] | None = None, + model_name: str | None = None, + metric_context: dict[str, Any] | None = None, viewer_context: SeerViewerContext | None = None, ) -> dict[str, Any]: """ @@ -83,6 +84,12 @@ def send_search_agent_start_request( body["user_email"] = user_email if timezone: body["timezone"] = timezone + + options: dict[str, Any] = {} + if model_name is not None: + options["model_name"] = model_name + if metric_context is not None: + options["metric_context"] = metric_context if options: body["options"] = options @@ -124,6 +131,8 @@ def post(self, request: Request, organization: Organization) -> Response: natural_language_query = validated_data["natural_language_query"] strategy = validated_data.get("strategy", "Traces") options = validated_data.get("options") or {} + model_name = options.get("model_name") + metric_context = options.get("metric_context") projects = self.get_projects( request, organization, project_ids=set(validated_data["project_ids"]) @@ -175,7 +184,8 @@ def post(self, request: Request, organization: Organization) -> Response: strategy=strategy, user_email=user_email, timezone=timezone, - options=options if options else None, + model_name=model_name, + metric_context=metric_context, viewer_context=viewer_context, )