From 80847f922503f4cb114334423df752ae34b57ba9 Mon Sep 17 00:00:00 2001
From: Hector Dearman
Date: Thu, 28 May 2026 11:17:03 +0100
Subject: [PATCH 01/14] ref(autofix): Remove the
organizations:autofix-on-explorer feature flag (#116165)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Third in a stack (follow-up to #116162 and #116164). The
`organizations:autofix-on-explorer` flag is now true everywhere, so this
PR removes it.
- Drop the flag registration from `temporary.py`.
- Collapse every `features.has("organizations:autofix-on-explorer",
...)` check to the True branch:
- `search/snuba/backend.py` — always use the
`seer_explorer_autofix_last_triggered` column
- `seer/autofix/issue_summary.py` — always route to
`trigger_autofix_agent`; remove the legacy fallback
- `seer/agent/client_utils.py` — gate only on
`organizations:seer-explorer` (collapse the dual-flag `batch_has` to a
single `features.has`)
- `seer/entrypoints/operator.py` — `trigger_autofix` always calls the
agent path; `trigger_handoff` always fetches via `fetch_run_status`
- Delete the `trigger_autofix_legacy` method on `SeerAutofixOperator`
(no callers remain) and its dead legacy imports.
- Tests: drop the legacy-path tests in `test_operator.py`, rename the
no-longer-distinguished `explorer` tests, and de-flag the remaining
tests.
Net: 9 files changed, 42 insertions, 667 deletions.
Agent transcript:
https://claudescope.sentry.dev/share/XIAoRxeHQ-AT-i3eUi_fVXryk-kKsNjn_0qaxBkUXqE
---
src/sentry/features/temporary.py | 2 -
src/sentry/search/snuba/backend.py | 8 +-
src/sentry/seer/agent/client_utils.py | 19 +-
src/sentry/seer/autofix/issue_summary.py | 39 +-
src/sentry/seer/entrypoints/operator.py | 275 +------------
.../test_organization_group_index.py | 22 +-
tests/sentry/seer/agent/test_client_utils.py | 23 +-
.../seer/entrypoints/slack/test_tasks.py | 1 -
.../sentry/seer/entrypoints/test_operator.py | 379 +-----------------
9 files changed, 42 insertions(+), 726 deletions(-)
diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py
index 25c6aa355cbe8e..3bd5327635ed7b 100644
--- a/src/sentry/features/temporary.py
+++ b/src/sentry/features/temporary.py
@@ -289,8 +289,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:seer-wizard", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable the Seer issues view
manager.add("organizations:seer-issue-view", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
- # Enable Autofix to use Seer Agent instead of legacy Celery pipeline
- manager.add("organizations:autofix-on-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable autofix introspection for early stopping of autofix runs
manager.add("organizations:seer-autofix-introspection", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable Seer Workflows in Slack (released, kept until overrides are removed)
diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py
index 5d54321b08d33a..79f0ea594fafcf 100644
--- a/src/sentry/search/snuba/backend.py
+++ b/src/sentry/search/snuba/backend.py
@@ -12,7 +12,7 @@
from django.utils import timezone
from django.utils.functional import SimpleLazyObject
-from sentry import features, quotas
+from sentry import quotas
from sentry.api.event_search import SearchFilter
from sentry.db.models.manager.base_query_set import BaseQuerySet
from sentry.exceptions import InvalidSearchQuery
@@ -595,11 +595,7 @@ def _get_queryset_conditions(
"issue.type": QCallbackCondition(lambda types: Q(type__in=types)),
"issue.priority": QCallbackCondition(lambda priorities: Q(priority__in=priorities)),
"issue.seer_actionability": QCallbackCondition(seer_actionability_filter),
- "issue.seer_last_run": ScalarCondition(
- "seer_explorer_autofix_last_triggered"
- if features.has("organizations:autofix-on-explorer", organization)
- else "seer_autofix_last_triggered"
- ),
+ "issue.seer_last_run": ScalarCondition("seer_explorer_autofix_last_triggered"),
"issue.id": QCallbackCondition(
lambda ids: Q(id__in=[int(v) for v in (ids if isinstance(ids, list) else [ids])])
),
diff --git a/src/sentry/seer/agent/client_utils.py b/src/sentry/seer/agent/client_utils.py
index 07b9e3d2eb584d..591297a8312a26 100644
--- a/src/sentry/seer/agent/client_utils.py
+++ b/src/sentry/seer/agent/client_utils.py
@@ -208,24 +208,7 @@ def has_seer_agent_access_with_detail(
if not has_access:
return False, error
- feature_names = [
- # Access to seer agent
- "organizations:seer-explorer",
- # Access to seer agent powered autofix
- "organizations:autofix-on-explorer",
- ]
-
- batch_features = features.batch_has(
- feature_names,
- organization=organization,
- actor=actor,
- )
-
- if batch_features is None:
- return False, "Feature flag not enabled"
-
- org_features = batch_features.get(f"organization:{organization.id}", {})
- if not any(bool(org_features.get(feature_name)) for feature_name in feature_names):
+ if not features.has("organizations:seer-explorer", organization, actor=actor):
return False, "Feature flag not enabled"
# Check open team membership (the agent requires this for context)
diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py
index ff6ce342973c88..af3e22daff94cd 100644
--- a/src/sentry/seer/autofix/issue_summary.py
+++ b/src/sentry/seer/autofix/issue_summary.py
@@ -18,7 +18,7 @@
from sentry.locks import locks
from sentry.models.group import Group
from sentry.net.http import connection_from_url
-from sentry.seer.autofix.autofix import _get_trace_tree_for_event, trigger_legacy_autofix
+from sentry.seer.autofix.autofix import _get_trace_tree_for_event
from sentry.seer.autofix.autofix_agent import (
AutofixStep,
NoSeerQuotaException,
@@ -52,7 +52,6 @@
from sentry.taskworker.namespaces import seer_tasks
from sentry.users.models.user import User
from sentry.users.services.user.model import RpcUser
-from sentry.users.services.user.service import user_service
from sentry.utils.cache import cache
from sentry.utils.locking import UnableToAcquireLock
@@ -176,41 +175,17 @@ def _trigger_autofix_task(
}
)
- user: User | AnonymousUser | RpcUser | None = None
- if user_id:
- user = user_service.get_user(user_id=user_id)
- if user is None:
- logger.warning(
- "_trigger_autofix_task.user_not_found",
- extra={"group_id": group_id, "user_id": user_id},
- )
- user = AnonymousUser()
- else:
- user = AnonymousUser()
-
- # Route to agent-based autofix if both feature flags are enabled
run_id: int | None = None
- if features.has("organizations:autofix-on-explorer", group.organization):
- try:
- run_id = trigger_autofix_agent(
- group=group,
- step=AutofixStep.ROOT_CAUSE,
- referrer=referrer,
- run_id=None,
- stopping_point=stopping_point,
- )
- except NoSeerQuotaException:
- pass
- else:
- response = trigger_legacy_autofix(
+ try:
+ run_id = trigger_autofix_agent(
group=group,
- event_id=event_id,
- user=user,
+ step=AutofixStep.ROOT_CAUSE,
referrer=referrer,
- auto_run_source=auto_run_source,
+ run_id=None,
stopping_point=stopping_point,
)
- run_id = response.data.get("run_id")
+ except NoSeerQuotaException:
+ pass
if run_id and SeerAutofixOperator.has_access(organization=group.project.organization):
SeerOperatorAutofixCache.migrate(from_group_id=group_id, to_run_id=run_id)
diff --git a/src/sentry/seer/entrypoints/operator.py b/src/sentry/seer/entrypoints/operator.py
index 2eee7b381a3ba8..739b1eb9033199 100644
--- a/src/sentry/seer/entrypoints/operator.py
+++ b/src/sentry/seer/entrypoints/operator.py
@@ -1,8 +1,6 @@
import logging
from typing import Any
-from rest_framework.response import Response
-
from sentry import features
from sentry.constants import DataCategory
from sentry.models.activity import Activity
@@ -13,20 +11,8 @@
from sentry.seer.agent.client_models import CodingAgentState, SeerRunState
from sentry.seer.agent.client_utils import fetch_run_status
from sentry.seer.agent.on_completion_hook import AgentOnCompletionHook
-from sentry.seer.autofix.autofix import trigger_legacy_autofix, update_legacy_autofix
-from sentry.seer.autofix.constants import AutofixReferrer, AutofixStatus
-from sentry.seer.autofix.types import (
- AutofixCreatePRPayload,
- AutofixSelectRootCausePayload,
- AutofixSelectSolutionPayload,
-)
-from sentry.seer.autofix.utils import (
- AutofixState,
- AutofixStoppingPoint,
- get_autofix_state,
- get_automation_handoff,
-)
-from sentry.seer.autofix.utils import CodingAgentState as LegacyCodingAgentState
+from sentry.seer.autofix.constants import AutofixReferrer
+from sentry.seer.autofix.utils import AutofixStoppingPoint, get_automation_handoff
from sentry.seer.entrypoints.cache import SeerOperatorAgentCache, SeerOperatorAutofixCache
from sentry.seer.entrypoints.metrics import (
SeerOperatorEventLifecycleMetric,
@@ -67,7 +53,6 @@
# entrypoint's ability to receive updates from those triggers. So 12 is plenty, even accounting for
# incidents, since a run should not take nearly that long to complete.
PROCESS_AUTOFIX_TIMEOUT_SECONDS = 60 * 5 # 5 minutes
-AUTOFIX_FALLBACK_CAUSE_ID = 0
def has_seer_autofix_entrypoint_access(
@@ -149,22 +134,13 @@ def trigger_autofix(
instruction: str | None = None,
run_id: int | None = None,
) -> None:
- if features.has("organizations:autofix-on-explorer", group.organization):
- self.trigger_autofix_agent(
- group=group,
- user=user,
- stopping_point=stopping_point,
- instruction=instruction,
- run_id=run_id,
- )
- else:
- self.trigger_autofix_legacy(
- group=group,
- user=user,
- stopping_point=stopping_point,
- instruction=instruction,
- run_id=run_id,
- )
+ self.trigger_autofix_agent(
+ group=group,
+ user=user,
+ stopping_point=stopping_point,
+ instruction=instruction,
+ run_id=run_id,
+ )
def trigger_autofix_agent(
self,
@@ -352,17 +328,8 @@ def trigger_handoff(
)
try:
- coding_agents: list[CodingAgentState] | list[LegacyCodingAgentState]
- if features.has("organizations:autofix-on-explorer", group.organization):
- agent_state = fetch_run_status(run_id=run_id, organization=group.organization)
- coding_agents = list(agent_state.coding_agents.values())
- else:
- autofix_state = get_autofix_state(
- run_id=run_id, organization_id=group.organization.id
- )
- coding_agents = (
- list(autofix_state.coding_agents.values()) if autofix_state else []
- )
+ agent_state = fetch_run_status(run_id=run_id, organization=group.organization)
+ coding_agents: list[CodingAgentState] = list(agent_state.coding_agents.values())
except Exception as e:
with SeerOperatorEventLifecycleMetric(
interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_HANDOFF_ERROR,
@@ -419,162 +386,6 @@ def trigger_handoff(
).capture():
self.entrypoint.on_trigger_handoff_success(run_id=run_id, target=target)
- def trigger_autofix_legacy(
- self,
- *,
- group: Group,
- user: User | RpcUser,
- stopping_point: AutofixStoppingPoint,
- instruction: str | None = None,
- run_id: int | None = None,
- ) -> None:
- event_lifecyle = SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.OPERATOR_TRIGGER_AUTOFIX,
- entrypoint_key=self.entrypoint.key,
- )
-
- raw_response: Response | None = None
- with event_lifecyle.capture() as lifecycle:
- lifecycle.add_extras(
- {
- "group_id": str(group.id),
- "user_id": str(user.id),
- "stopping_point": str(stopping_point),
- }
- )
- try:
- existing_state = get_autofix_state(
- group_id=group.id, organization_id=group.organization.id
- )
- except Exception as e:
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- self.entrypoint.on_trigger_autofix_error(
- error="Encountered an error while talking to Seer"
- )
- lifecycle.record_failure(failure_reason=e)
- return
- if existing_state:
- stopping_point_step = get_stopping_point_status(stopping_point, existing_state)
- lifecycle.add_extras(
- {
- "existing_run_id": str(existing_state.run_id),
- "existing_run_status": str(existing_state.status),
- }
- )
- # For now, we don't support re-runs over slack -- it causes a confusing UX without
- # reliably being able to edit messages.
- if stopping_point_step:
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ALREADY_EXISTS,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- has_complete_stage = (
- False
- if stopping_point_step.get("key")
- in {"root_cause_analysis_processing", "solution_processing"}
- else stopping_point_step.get("status") == AutofixStatus.COMPLETED
- )
- self.entrypoint.on_trigger_autofix_already_exists(
- run_id=existing_state.run_id,
- has_complete_stage=has_complete_stage,
- )
- return
-
- if not run_id:
- raw_response = trigger_legacy_autofix(
- group=group,
- user=user,
- referrer=AutofixReferrer.SLACK,
- instruction=instruction,
- stopping_point=stopping_point,
- )
- else:
- payload: (
- AutofixSelectRootCausePayload
- | AutofixSelectSolutionPayload
- | AutofixCreatePRPayload
- | None
- ) = None
- if stopping_point == AutofixStoppingPoint.SOLUTION:
- payload = AutofixSelectRootCausePayload(
- type="select_root_cause",
- cause_id=get_latest_cause_id(existing_state),
- )
- elif stopping_point == AutofixStoppingPoint.CODE_CHANGES:
- payload = AutofixSelectSolutionPayload(type="select_solution")
- elif stopping_point == AutofixStoppingPoint.OPEN_PR:
- payload = AutofixCreatePRPayload(type="create_pr")
- else:
- lifecycle.record_failure(failure_reason="invalid_stopping_point")
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- self.entrypoint.on_trigger_autofix_error(
- error="Invalid stopping point provided"
- )
- return
-
- raw_response = update_legacy_autofix(
- organization_id=group.organization.id,
- run_id=run_id,
- payload=payload,
- )
-
- error_message = raw_response.data.get("detail")
-
- # Let the entrypoint signal to the external service that no run was started :/
- if error_message:
- lifecycle.record_failure(failure_reason=error_message)
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- self.entrypoint.on_trigger_autofix_error(error=error_message)
- return
-
- run_id = raw_response.data.get("run_id") if not run_id else run_id
- if not run_id:
- lifecycle.record_failure(failure_reason="no_run_id")
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_ERROR,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- self.entrypoint.on_trigger_autofix_error(error="An unknown error has occurred")
- return
- lifecycle.add_extra("run_id", str(run_id))
-
- # Let the entrypoint signal to the external service that the run started
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_ON_TRIGGER_AUTOFIX_SUCCESS,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- self.entrypoint.on_trigger_autofix_success(run_id=run_id)
-
- # Create a cache payload that will be picked up for subsequent updates
- with SeerOperatorEventLifecycleMetric(
- interaction_type=SeerOperatorInteractionType.ENTRYPOINT_CREATE_AUTOFIX_CACHE_PAYLOAD,
- entrypoint_key=self.entrypoint.key,
- ).capture():
- cache_payload = self.entrypoint.create_autofix_cache_payload()
-
- if not cache_payload:
- return
- cache_result = SeerOperatorAutofixCache.populate_post_autofix_cache(
- entrypoint_key=str(self.entrypoint.key),
- cache_payload=cache_payload,
- run_id=run_id,
- )
- lifecycle.add_extras(
- {
- "cache_key": cache_result["key"],
- "cache_source": cache_result["source"],
- }
- )
-
def has_seer_agent_entrypoint_access(
*,
@@ -879,44 +690,6 @@ def process_autofix_updates(
ept_lifecycle.record_failure(failure_reason=e)
-def get_stopping_point_status(
- stopping_point: AutofixStoppingPoint, autofix_state: AutofixState
-) -> dict | None:
- """
- Gets the most recent matching step state from a given stopping point.
- """
- # The most recent of a repeated step is at the end of the list, that's what we want to surface
- steps = reversed(autofix_state.steps)
- match stopping_point:
- case AutofixStoppingPoint.ROOT_CAUSE:
- step = next(
- (
- step
- for step in steps
- if step.get("key") in {"root_cause_analysis", "root_cause_analysis_processing"}
- ),
- None,
- )
- case AutofixStoppingPoint.SOLUTION:
- step = next(
- (step for step in steps if step.get("key") in {"solution", "solution_processing"}),
- None,
- )
- case AutofixStoppingPoint.CODE_CHANGES:
- step = next((step for step in steps if step.get("key") == "changes"), None)
- case AutofixStoppingPoint.OPEN_PR:
- step = next(
- (
- step
- for step in steps
- if step.get("key") == "changes"
- and any(change.get("pull_request") for change in step.get("changes", []))
- ),
- None,
- )
- return step
-
-
def get_autofix_explorer_status(
stopping_point: AutofixStoppingPoint, autofix_state: SeerRunState
) -> bool | None:
@@ -975,32 +748,6 @@ def get_autofix_explorer_status(
return None
-def get_latest_cause_id(autofix_state: AutofixState | None) -> int:
- """
- Gets the latest cause_id from a given autofix state.
- """
- if not autofix_state:
- return AUTOFIX_FALLBACK_CAUSE_ID
- root_cause_step = next(
- (
- step
- # If there are multiple RCA steps, we want the latest, so we reverse the list
- for step in reversed(autofix_state.steps)
- if step.get("key") == "root_cause_analysis"
- ),
- None,
- )
- if not root_cause_step:
- return AUTOFIX_FALLBACK_CAUSE_ID
-
- root_causes = root_cause_step.get("causes", [])
- if not root_causes:
- return AUTOFIX_FALLBACK_CAUSE_ID
-
- # The most recent cause is at the end of the list
- return root_causes[-1].get("id", AUTOFIX_FALLBACK_CAUSE_ID)
-
-
class SeerOperatorCompletionHook(AgentOnCompletionHook):
"""Completion hook that notifies all entrypoints when a Seer Agent run finishes.
diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py
index 7039f5bd06573f..9c438eb823254a 100644
--- a/tests/sentry/issues/endpoints/test_organization_group_index.py
+++ b/tests/sentry/issues/endpoints/test_organization_group_index.py
@@ -531,7 +531,7 @@ def test_perf_issue(self) -> None:
assert response.data[0]["id"] == str(perf_group.id)
def test_has_seer_last_run(self) -> None:
- """Test filtering issues by whether they have seer_autofix_last_triggered set."""
+ """Test filtering issues by whether they have seer_explorer_autofix_last_triggered set."""
event1 = self.store_event(
data={
"fingerprint": ["no-seer-group"],
@@ -563,29 +563,17 @@ def test_has_seer_last_run(self) -> None:
self.login_as(user=self.user)
- # Query for issues that have seer_autofix_last_triggered set
+ # Query for issues that have seer_explorer_autofix_last_triggered set
response = self.get_success_response(query="has:issue.seer_last_run")
assert len(response.data) == 1
- assert response.data[0]["id"] == str(group_with_legacy_seer.id)
+ assert response.data[0]["id"] == str(group_with_explorer_seer.id)
- # Query for issues that do NOT have seer_autofix_last_triggered set
+ # Query for issues that do NOT have seer_explorer_autofix_last_triggered set
response = self.get_success_response(query="!has:issue.seer_last_run")
assert len(response.data) == 2
- assert response.data[0]["id"] == str(group_with_explorer_seer.id)
+ assert response.data[0]["id"] == str(group_with_legacy_seer.id)
assert response.data[1]["id"] == str(group_without_seer.id)
- # Query for issues that have seer_explorer_autofix_last_triggered set
- with self.feature("organizations:autofix-on-explorer"):
- response = self.get_success_response(query="has:issue.seer_last_run")
- assert len(response.data) == 1
- assert response.data[0]["id"] == str(group_with_explorer_seer.id)
-
- # Query for issues that do NOT have seer_explorer_autofix_last_triggered set
- response = self.get_success_response(query="!has:issue.seer_last_run")
- assert len(response.data) == 2
- assert response.data[0]["id"] == str(group_with_legacy_seer.id)
- assert response.data[1]["id"] == str(group_without_seer.id)
-
def test_lookup_by_event_id(self) -> None:
project = self.project
project.update_option("sentry:resolve_age", 1)
diff --git a/tests/sentry/seer/agent/test_client_utils.py b/tests/sentry/seer/agent/test_client_utils.py
index 4b9f2bd7585f9b..a1f9e9c1830791 100644
--- a/tests/sentry/seer/agent/test_client_utils.py
+++ b/tests/sentry/seer/agent/test_client_utils.py
@@ -32,36 +32,18 @@ def test_hide_ai_features_option_set(self) -> None:
result = has_seer_agent_access_with_detail(self.org, self.user)
assert result == (False, "AI features are disabled for this organization.")
- def test_no_explorer_flags_enabled(self) -> None:
+ def test_no_explorer_flag_enabled(self) -> None:
with self.feature("organizations:gen-ai-features"):
result = has_seer_agent_access_with_detail(self.org, self.user)
assert result == (False, "Feature flag not enabled")
- def test_only_seer_explorer_flag(self) -> None:
+ def test_seer_explorer_flag_enabled(self) -> None:
with self.feature(
{"organizations:gen-ai-features": True, "organizations:seer-explorer": True}
):
result = has_seer_agent_access_with_detail(self.org, self.user)
assert result == (True, None)
- def test_only_autofix_on_explorer_flag(self) -> None:
- with self.feature(
- {"organizations:gen-ai-features": True, "organizations:autofix-on-explorer": True}
- ):
- result = has_seer_agent_access_with_detail(self.org, self.user)
- assert result == (True, None)
-
- def test_all_explorer_flags_enabled(self) -> None:
- with self.feature(
- {
- "organizations:gen-ai-features": True,
- "organizations:seer-explorer": True,
- "organizations:autofix-on-explorer": True,
- }
- ):
- result = has_seer_agent_access_with_detail(self.org, self.user)
- assert result == (True, None)
-
def test_allow_joinleave_disabled(self) -> None:
self.org.flags.allow_joinleave = False
self.org.save()
@@ -69,7 +51,6 @@ def test_allow_joinleave_disabled(self) -> None:
{
"organizations:gen-ai-features": True,
"organizations:seer-explorer": True,
- "organizations:autofix-on-explorer": True,
}
):
result = has_seer_agent_access_with_detail(self.org, self.user)
diff --git a/tests/sentry/seer/entrypoints/slack/test_tasks.py b/tests/sentry/seer/entrypoints/slack/test_tasks.py
index 8f146bf18c1058..b8092855c75bd9 100644
--- a/tests/sentry/seer/entrypoints/slack/test_tasks.py
+++ b/tests/sentry/seer/entrypoints/slack/test_tasks.py
@@ -41,7 +41,6 @@
_SEER_SLACK_FEATURES = {
"organizations:gen-ai-features": True,
"organizations:seer-explorer": True,
- "organizations:autofix-on-explorer": True,
}
diff --git a/tests/sentry/seer/entrypoints/test_operator.py b/tests/sentry/seer/entrypoints/test_operator.py
index 6cc0fad8f1f15c..e96c3148c06fa9 100644
--- a/tests/sentry/seer/entrypoints/test_operator.py
+++ b/tests/sentry/seer/entrypoints/test_operator.py
@@ -3,8 +3,6 @@
from typing import Any, TypedDict, cast
from unittest.mock import Mock, patch
-from rest_framework.response import Response
-
from fixtures.seer.webhooks import MOCK_RUN_ID
from sentry.models.activity import Activity
from sentry.models.organization import Organization
@@ -16,16 +14,12 @@
RepoPRState,
SeerRunState,
)
-from sentry.seer.autofix.constants import AutofixReferrer, AutofixStatus
+from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.utils import (
- AutofixState,
AutofixStoppingPoint,
CodingAgentProviderType,
- CodingAgentStatus,
)
-from sentry.seer.autofix.utils import CodingAgentState as LegacyCodingAgentState
from sentry.seer.entrypoints.operator import (
- AUTOFIX_FALLBACK_CAUSE_ID,
SEER_EVENT_TO_ACTIVITY_TYPE,
SeerAgentOperator,
SeerAutofixOperator,
@@ -113,22 +107,6 @@ def _set_automation_handoff(
self.project.update_option("sentry:seer_automation_handoff_target", target.value)
self.project.update_option("sentry:seer_automation_handoff_integration_id", 789)
- def _build_autofix_state_with_agents(
- self, agents: dict[str, LegacyCodingAgentState]
- ) -> AutofixState:
- return AutofixState(
- run_id=MOCK_RUN_ID,
- request={
- "organization_id": self.organization.id,
- "project_id": self.project.id,
- "issue": {"id": self.group.id, "title": "test"},
- "repos": [],
- },
- updated_at=datetime.now(),
- status=AutofixStatus.PROCESSING,
- coding_agents=agents,
- )
-
@patch("sentry.seer.entrypoints.operator.has_seer_access", return_value=True)
def test_has_access_with_seer(self, _mock_has_seer_access):
MockNoAccessEntrypoint = Mock(spec=SeerAutofixEntrypoint)
@@ -163,234 +141,6 @@ def test_has_access_without_seer(self, _mock_has_seer_access):
entrypoint_key=cast(SeerEntrypointKey, entrypoint_key),
)
- @patch(
- "sentry.seer.entrypoints.operator.update_legacy_autofix",
- return_value=Response({"run_id": MOCK_RUN_ID}, status=202),
- )
- @patch(
- "sentry.seer.entrypoints.operator.trigger_legacy_autofix",
- return_value=Response({"run_id": MOCK_RUN_ID}, status=202),
- )
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- def test_trigger_autofix_pathway(
- self,
- mock_get_autofix_state,
- mock_trigger_autofix_helper,
- mock_update_autofix_helper,
- ):
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
- assert mock_trigger_autofix_helper.call_count == 1
- assert mock_update_autofix_helper.call_count == 0
- mock_trigger_autofix_helper.reset_mock()
-
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.SOLUTION,
- run_id=MOCK_RUN_ID,
- )
- assert mock_trigger_autofix_helper.call_count == 0
- assert mock_update_autofix_helper.call_count == 1
-
- @patch(
- "sentry.seer.entrypoints.operator.trigger_legacy_autofix",
- return_value=Response({"run_id": MOCK_RUN_ID}, status=202),
- )
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- def test_trigger_autofix_success(self, mock_get_autofix_state, mock_trigger_autofix_helper):
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
- assert mock_trigger_autofix_helper.call_count == 1
- assert self.entrypoint.autofix_errors == []
- assert self.entrypoint.autofix_run_ids == [MOCK_RUN_ID]
-
- @patch("sentry.seer.entrypoints.operator.trigger_legacy_autofix")
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- def test_trigger_autofix_already_exists(
- self, mock_get_autofix_state, mock_trigger_autofix_helper
- ):
- existing_rca_step_state = {
- "key": "root_cause_analysis",
- "status": AutofixStatus.COMPLETED,
- }
- existing_state = AutofixState(
- run_id=MOCK_RUN_ID,
- request={
- "organization_id": self.organization.id,
- "project_id": self.project.id,
- "issue": {"id": self.group.id, "title": "test"},
- "repos": [],
- },
- updated_at=datetime.now(),
- status=AutofixStatus.PROCESSING,
- steps=[existing_rca_step_state],
- )
- mock_get_autofix_state.return_value = existing_state
-
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
-
- mock_trigger_autofix_helper.assert_not_called()
- assert self.entrypoint.autofix_already_exists_states == [(existing_state.run_id, True)]
- assert self.entrypoint.autofix_run_ids == []
- assert self.entrypoint.autofix_errors == []
-
- @patch(
- "sentry.seer.entrypoints.operator.trigger_legacy_autofix",
- return_value=Response({"run_id": MOCK_RUN_ID}, status=202),
- )
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- def test_trigger_autofix_proceeds_when_completed(
- self, mock_get_autofix_state, mock_trigger_autofix_helper
- ):
- existing_state = AutofixState(
- run_id=MOCK_RUN_ID,
- request={
- "organization_id": self.organization.id,
- "project_id": self.project.id,
- "issue": {"id": self.group.id, "title": "test"},
- "repos": [],
- },
- updated_at=datetime.now(),
- status=AutofixStatus.COMPLETED,
- )
- mock_get_autofix_state.return_value = existing_state
-
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
-
- mock_trigger_autofix_helper.assert_called_once()
- assert self.entrypoint.autofix_already_exists_states == []
- assert self.entrypoint.autofix_run_ids == [MOCK_RUN_ID]
-
- @patch("sentry.seer.entrypoints.operator.trigger_legacy_autofix")
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- def test_trigger_autofix_error(self, mock_get_autofix_state, mock_trigger_autofix_helper):
- mock_trigger_autofix_helper.return_value = Response(
- {"detail": "Invalid request"}, status=400
- )
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
- mock_trigger_autofix_helper.return_value = Response({"run_id": None}, status=202)
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
- assert mock_trigger_autofix_helper.call_count == 2
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- run_id=MOCK_RUN_ID,
- )
- assert self.entrypoint.autofix_errors == [
- "Invalid request",
- "An unknown error has occurred",
- "Invalid stopping point provided",
- ]
- assert self.entrypoint.autofix_run_ids == []
-
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_success(self, mock_trigger_handoff_helper, mock_get_state):
- self._set_automation_handoff()
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
- mock_trigger_handoff_helper.assert_called_once()
- assert mock_trigger_handoff_helper.call_args.kwargs["referrer"] == AutofixReferrer.SLACK
- assert self.entrypoint.handoff_successes == [
- (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT)
- ]
- assert self.entrypoint.handoff_already_exists == []
- assert self.entrypoint.handoff_errors == []
-
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_already_exists_running(
- self, mock_trigger_handoff_helper, mock_get_state
- ):
- self._set_automation_handoff()
- mock_get_state.return_value = self._build_autofix_state_with_agents(
- {
- "agent-1": LegacyCodingAgentState(
- id="agent-1",
- status=CodingAgentStatus.RUNNING,
- provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT,
- name="Cursor",
- started_at=datetime.now(),
- )
- }
- )
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
- mock_trigger_handoff_helper.assert_not_called()
- assert self.entrypoint.handoff_already_exists == [
- (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, False)
- ]
- assert self.entrypoint.handoff_successes == []
-
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_already_exists_completed(
- self, mock_trigger_handoff_helper, mock_get_state
- ):
- self._set_automation_handoff()
- mock_get_state.return_value = self._build_autofix_state_with_agents(
- {
- "agent-1": LegacyCodingAgentState(
- id="agent-1",
- status=CodingAgentStatus.COMPLETED,
- provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT,
- name="Cursor",
- started_at=datetime.now(),
- )
- }
- )
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
- mock_trigger_handoff_helper.assert_not_called()
- assert self.entrypoint.handoff_already_exists == [
- (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, True)
- ]
-
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_proceeds_when_all_agents_failed(
- self, mock_trigger_handoff_helper, mock_get_state
- ):
- self._set_automation_handoff()
- mock_get_state.return_value = self._build_autofix_state_with_agents(
- {
- "agent-1": LegacyCodingAgentState(
- id="agent-1",
- status=CodingAgentStatus.FAILED,
- provider=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT,
- name="Cursor",
- started_at=datetime.now(),
- )
- }
- )
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
- mock_trigger_handoff_helper.assert_called_once()
- assert self.entrypoint.handoff_successes == [
- (MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT)
- ]
- assert self.entrypoint.handoff_already_exists == []
-
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
def test_trigger_handoff_no_config_is_silent_halt(self, mock_trigger_handoff_helper):
self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
@@ -399,29 +149,16 @@ def test_trigger_handoff_no_config_is_silent_halt(self, mock_trigger_handoff_hel
assert self.entrypoint.handoff_already_exists == []
assert self.entrypoint.handoff_errors == []
- @patch(
- "sentry.seer.entrypoints.operator.get_autofix_state",
- side_effect=Exception("seer down"),
- )
- @patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_state_fetch_error_calls_error_hook(
- self, mock_trigger_handoff_helper, mock_get_state
- ):
- self._set_automation_handoff()
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
- mock_trigger_handoff_helper.assert_not_called()
- assert self.entrypoint.handoff_errors == ["Encountered an error while talking to Seer"]
- assert self.entrypoint.handoff_successes == []
-
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
+ @patch("sentry.seer.entrypoints.operator.fetch_run_status", return_value=None)
@patch(
"sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff",
side_effect=RuntimeError("boom"),
)
def test_trigger_handoff_launch_error_calls_error_hook(
- self, mock_trigger_handoff_helper, mock_get_state
+ self, mock_trigger_handoff_helper, mock_fetch_status
):
self._set_automation_handoff()
+ mock_fetch_status.return_value = self._build_explorer_state_with_agents({})
self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
assert self.entrypoint.handoff_errors == [
"Encountered an error while launching the coding agent"
@@ -441,11 +178,10 @@ def _build_explorer_state_with_agents(
@patch("sentry.seer.entrypoints.operator.fetch_run_status")
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_explorer_success(self, mock_trigger_handoff_helper, mock_fetch_status):
+ def test_trigger_handoff_success(self, mock_trigger_handoff_helper, mock_fetch_status):
self._set_automation_handoff()
mock_fetch_status.return_value = self._build_explorer_state_with_agents({})
- with self.feature("organizations:autofix-on-explorer"):
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
+ self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
mock_trigger_handoff_helper.assert_called_once()
assert mock_trigger_handoff_helper.call_args.kwargs["referrer"] == AutofixReferrer.SLACK
assert self.entrypoint.handoff_successes == [
@@ -456,7 +192,7 @@ def test_trigger_handoff_explorer_success(self, mock_trigger_handoff_helper, moc
@patch("sentry.seer.entrypoints.operator.fetch_run_status")
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_explorer_already_exists_running(
+ def test_trigger_handoff_already_exists_running(
self, mock_trigger_handoff_helper, mock_fetch_status
):
self._set_automation_handoff()
@@ -471,8 +207,7 @@ def test_trigger_handoff_explorer_already_exists_running(
)
}
)
- with self.feature("organizations:autofix-on-explorer"):
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
+ self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
mock_trigger_handoff_helper.assert_not_called()
assert self.entrypoint.handoff_already_exists == [
(MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, False)
@@ -480,7 +215,7 @@ def test_trigger_handoff_explorer_already_exists_running(
@patch("sentry.seer.entrypoints.operator.fetch_run_status")
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_explorer_already_exists_completed(
+ def test_trigger_handoff_already_exists_completed(
self, mock_trigger_handoff_helper, mock_fetch_status
):
self._set_automation_handoff()
@@ -495,8 +230,7 @@ def test_trigger_handoff_explorer_already_exists_completed(
)
}
)
- with self.feature("organizations:autofix-on-explorer"):
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
+ self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
mock_trigger_handoff_helper.assert_not_called()
assert self.entrypoint.handoff_already_exists == [
(MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, True)
@@ -504,7 +238,7 @@ def test_trigger_handoff_explorer_already_exists_completed(
@patch("sentry.seer.entrypoints.operator.fetch_run_status")
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_explorer_proceeds_when_all_agents_failed(
+ def test_trigger_handoff_proceeds_when_all_agents_failed(
self, mock_trigger_handoff_helper, mock_fetch_status
):
self._set_automation_handoff()
@@ -519,8 +253,7 @@ def test_trigger_handoff_explorer_proceeds_when_all_agents_failed(
)
}
)
- with self.feature("organizations:autofix-on-explorer"):
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
+ self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
mock_trigger_handoff_helper.assert_called_once()
assert self.entrypoint.handoff_successes == [
(MOCK_RUN_ID, CodingAgentProviderType.CURSOR_BACKGROUND_AGENT)
@@ -532,39 +265,15 @@ def test_trigger_handoff_explorer_proceeds_when_all_agents_failed(
side_effect=Exception("seer down"),
)
@patch("sentry.seer.autofix.autofix_agent.trigger_coding_agent_handoff")
- def test_trigger_handoff_explorer_state_fetch_error_calls_error_hook(
+ def test_trigger_handoff_state_fetch_error_calls_error_hook(
self, mock_trigger_handoff_helper, mock_fetch_status
):
self._set_automation_handoff()
- with self.feature("organizations:autofix-on-explorer"):
- self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
+ self.operator.trigger_handoff(group=self.group, run_id=MOCK_RUN_ID)
mock_trigger_handoff_helper.assert_not_called()
assert self.entrypoint.handoff_errors == ["Encountered an error while talking to Seer"]
assert self.entrypoint.handoff_successes == []
- @patch(
- "sentry.seer.entrypoints.operator.trigger_legacy_autofix",
- return_value=Response({"run_id": MOCK_RUN_ID}, status=202),
- )
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- @patch("sentry.seer.entrypoints.cache.SeerOperatorAutofixCache.populate_post_autofix_cache")
- def test_trigger_autofix_creates_cache_payload(
- self,
- mock_populate_post_autofix_cache,
- mock_get_autofix_state,
- mock_trigger_autofix_helper,
- ):
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
- )
- mock_populate_post_autofix_cache.assert_called_with(
- entrypoint_key=MockAutofixEntrypoint.key,
- run_id=MOCK_RUN_ID,
- cache_payload=self.entrypoint.create_autofix_cache_payload(),
- )
-
@patch.object(SeerAutofixOperator, "has_access", return_value=True)
@patch.dict(
"sentry.seer.entrypoints.operator.autofix_entrypoint_registry.registrations",
@@ -693,66 +402,6 @@ def test_process_autofix_updates_skips_entrypoint_without_access(
cache_payload=cache_payload,
)
- @patch("sentry.seer.entrypoints.operator.update_legacy_autofix")
- @patch("sentry.seer.entrypoints.operator.get_autofix_state", return_value=None)
- def test_solution_stopping_point_sends_select_root_cause(
- self, _mock_get_autofix_state, mock_update_autofix
- ):
- mock_update_autofix.return_value = Response({"run_id": MOCK_RUN_ID}, status=202)
-
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.SOLUTION,
- run_id=MOCK_RUN_ID,
- )
-
- mock_update_autofix.assert_called_once()
- call_kwargs = mock_update_autofix.call_args.kwargs
- assert call_kwargs["organization_id"] == self.group.organization.id
- payload = call_kwargs["payload"]
- assert payload["type"] == "select_root_cause"
- assert payload["cause_id"] == AUTOFIX_FALLBACK_CAUSE_ID
-
- @patch("sentry.seer.entrypoints.operator.update_legacy_autofix")
- @patch("sentry.seer.entrypoints.operator.get_autofix_state")
- def test_solution_stopping_point_uses_cause_id_from_state(
- self, mock_get_autofix_state, mock_update_autofix
- ):
- mock_update_autofix.return_value = Response({"run_id": MOCK_RUN_ID}, status=202)
- existing_state = AutofixState(
- run_id=MOCK_RUN_ID,
- request={
- "organization_id": self.organization.id,
- "project_id": self.project.id,
- "issue": {"id": self.group.id, "title": "test"},
- "repos": [],
- },
- updated_at=datetime.now(),
- status=AutofixStatus.PROCESSING,
- steps=[
- {
- "key": "root_cause_analysis",
- "status": AutofixStatus.COMPLETED,
- "causes": [{"id": 12}, {"id": 34}],
- },
- ],
- )
- mock_get_autofix_state.return_value = existing_state
-
- self.operator.trigger_autofix(
- group=self.group,
- user=self.user,
- stopping_point=AutofixStoppingPoint.SOLUTION,
- run_id=MOCK_RUN_ID,
- )
-
- mock_update_autofix.assert_called_once()
- call_kwargs = mock_update_autofix.call_args.kwargs
- payload = call_kwargs["payload"]
- assert payload["type"] == "select_root_cause"
- assert payload["cause_id"] == 34
-
def test_can_trigger_autofix_returns_false_without_seer_access(self) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False
From cd21d1680813e20ca94eb3b2ce3978ad377f7d7f Mon Sep 17 00:00:00 2001
From: Ogi
Date: Thu, 28 May 2026 12:24:08 +0200
Subject: [PATCH 02/14] fix(conversations): use 24h statsPeriod on detail page
back link (#116361)
---
static/app/views/explore/conversations/layout.spec.tsx | 2 +-
static/app/views/explore/conversations/layout.tsx | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/static/app/views/explore/conversations/layout.spec.tsx b/static/app/views/explore/conversations/layout.spec.tsx
index 4e9e15201f3343..ea8dfb61f728fd 100644
--- a/static/app/views/explore/conversations/layout.spec.tsx
+++ b/static/app/views/explore/conversations/layout.spec.tsx
@@ -45,7 +45,7 @@ describe('ConversationsLayout', () => {
expect(within(topBar).getByText('6c5b72fc')).toBeInTheDocument();
expect(within(topBar).getByRole('link', {name: 'Conversations'})).toHaveAttribute(
'href',
- `/organizations/${organization.slug}/explore/conversations/?environment=prod&project=1&statsPeriod=7d`
+ `/organizations/${organization.slug}/explore/conversations/?environment=prod&project=1&statsPeriod=24h`
);
});
});
diff --git a/static/app/views/explore/conversations/layout.tsx b/static/app/views/explore/conversations/layout.tsx
index 2942daddcdfdaa..c1fe760fafd1a6 100644
--- a/static/app/views/explore/conversations/layout.tsx
+++ b/static/app/views/explore/conversations/layout.tsx
@@ -87,7 +87,10 @@ function ConversationsHeader() {
crumbs={[
{
label: CONVERSATIONS_SIDEBAR_LABEL,
- to: conversationsBaseUrl,
+ to: {
+ pathname: conversationsBaseUrl,
+ query: {statsPeriod: '24h', start: undefined, end: undefined},
+ },
preservePageFilters: true,
},
{
From 2f00af4578adb08b14ae1390d7ea215af8ec36bc Mon Sep 17 00:00:00 2001
From: Priscila Oliveira
Date: Thu, 28 May 2026 12:48:09 +0200
Subject: [PATCH 03/14] ref(forms): Migrate RequestIntegrationModal to TanStack
form system (#115990)
closes https://linear.app/getsentry/issue/DE-1246/migrate
---------
Co-authored-by: Claude Opus 4.7
---
.../RequestIntegrationModal.tsx | 116 ++++++++++--------
1 file changed, 64 insertions(+), 52 deletions(-)
diff --git a/static/app/views/settings/organizationIntegrations/integrationRequest/RequestIntegrationModal.tsx b/static/app/views/settings/organizationIntegrations/integrationRequest/RequestIntegrationModal.tsx
index ee0d2c41c8e6cc..d84fda70e0f984 100644
--- a/static/app/views/settings/organizationIntegrations/integrationRequest/RequestIntegrationModal.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationRequest/RequestIntegrationModal.tsx
@@ -1,17 +1,18 @@
-import {Fragment, useState} from 'react';
import {useMutation} from '@tanstack/react-query';
+import {z} from 'zod';
-import {Button} from '@sentry/scraps/button';
+import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form';
+import {Stack} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
-import {TextareaField} from 'sentry/components/forms/fields/textareaField';
import {t} from 'sentry/locale';
import type {IntegrationType} from 'sentry/types/integrations';
import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
-import {useApi} from 'sentry/utils/useApi';
+import {fetchMutation} from 'sentry/utils/queryClient';
import {useOrganization} from 'sentry/utils/useOrganization';
-import {TextBlock} from 'sentry/views/settings/components/text/textBlock';
+
type Props = {
name: string;
onSuccess: () => void;
@@ -19,34 +20,40 @@ type Props = {
type: IntegrationType;
} & ModalRenderProps;
+const schema = z.object({
+ message: z.string(),
+});
+
/**
* This modal serves as a non-owner's confirmation step before sending
* organization owners an email requesting a new organization integration. It
* lets the user attach an optional message to be included in the email.
*/
-export function RequestIntegrationModal(props: Props) {
- const [isSending, setIsSending] = useState(false);
- const [message, setMessage] = useState('');
+export function RequestIntegrationModal({
+ Header,
+ Body,
+ Footer,
+ CloseButton,
+ name,
+ slug,
+ type,
+ closeModal,
+ onSuccess,
+}: Props) {
const organization = useOrganization();
- const api = useApi({persistInFlight: true});
-
- const {Header, Body, Footer, CloseButton, name, slug, type, closeModal, onSuccess} =
- props;
- const endpoint = `/organizations/${organization.slug}/integration-requests/`;
const sendRequestMutation = useMutation({
- mutationFn: () => {
- return api.requestPromise(endpoint, {
+ mutationFn: (data: z.infer) =>
+ fetchMutation({
+ url: `/organizations/${organization.slug}/integration-requests/`,
method: 'POST',
data: {
providerSlug: slug,
providerType: type,
- message,
+ message: data.message,
},
- });
- },
+ }),
onMutate: () => {
- setIsSending(true);
trackIntegrationAnalytics('integrations.request_install', {
integration_type: type,
integration: slug,
@@ -55,55 +62,60 @@ export function RequestIntegrationModal(props: Props) {
},
onSuccess: () => {
addSuccessMessage(t('Request successfully sent.'));
- setIsSending(false);
onSuccess();
closeModal();
},
onError: () => {
- addErrorMessage('Error sending the request');
- setIsSending(false);
+ addErrorMessage(t('Error sending the request'));
},
});
- const buttonText = isSending ? t('Sending Request') : t('Send Request');
+ const form = useScrapsForm({
+ ...defaultFormOptions,
+ defaultValues: {message: ''},
+ validators: {onDynamic: schema},
+ onSubmit: ({value}) => sendRequestMutation.mutateAsync(value).catch(() => {}),
+ });
return (
-
+
{t('Request %s Installation', name)}
-
- {t(
- 'Looks like your organization owner, manager, or admin needs to install %s. Want to send them a request?',
- name
- )}
-
-
- {t(
- '(Optional) You’ve got good reasons for installing the %s Integration. Share them with your organization owner.',
- name
- )}
-
-
-
- {t(
- 'When you click “Send Request”, we’ll email your request to your organization’s owners. So just keep that in mind.'
- )}
-
+
+
+ {t(
+ 'Looks like your organization owner, manager, or admin needs to install %s. Want to send them a request?',
+ name
+ )}
+
+
+ {t(
+ 'You’ve got good reasons for installing the %s Integration. Share them with your organization owner.',
+ name
+ )}
+
+
+ {field => (
+
+ )}
+
+
+ {t(
+ 'When you click “Send Request”, we’ll email your request to your organization’s owners. So just keep that in mind.'
+ )}
+
+
-
+
);
}
From b0870c64e314ae9b2b94d1ec29253bc01ff79169 Mon Sep 17 00:00:00 2001
From: Simon Hellmayr
Date: Thu, 28 May 2026 13:12:53 +0200
Subject: [PATCH 04/14] feat(dynamic-sampling): add sliding window calculation
to per-org (#116083)
- Sliding window retrieves the total 24h volume from EAP, extrapolates
to the monthly volume, and then gets the sample rate from getsentry's
tier system
- If available, this sample rate is used as the base for project sample
rates for the automatic configurations
- Add the sliding window calculation to the new per-org pipeline
---
.../per_org/tasks/configuration.py | 43 +++++++++++-
.../dynamic_sampling/per_org/tasks/queries.py | 16 +++--
.../per_org/tasks/test_configuration.py | 65 +++++++++++++++++++
.../per_org/tasks/test_queries.py | 23 ++++---
4 files changed, 133 insertions(+), 14 deletions(-)
diff --git a/src/sentry/dynamic_sampling/per_org/tasks/configuration.py b/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
index 24c91aace8f12f..5c6ef598d5d083 100644
--- a/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
+++ b/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
@@ -2,16 +2,20 @@
from abc import ABC, abstractmethod
from collections.abc import Mapping
+from datetime import timedelta
from django.core.exceptions import ObjectDoesNotExist
from sentry import options, quotas
from sentry.constants import SAMPLING_MODE_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus
+from sentry.dynamic_sampling.per_org.tasks.queries import get_eap_organization_volume
from sentry.dynamic_sampling.per_org.tasks.telemetry import (
DynamicSamplingException,
DynamicSamplingStatus,
)
from sentry.dynamic_sampling.rules.utils import ProjectId
+from sentry.dynamic_sampling.tasks.common import compute_sliding_window_sample_rate
+from sentry.dynamic_sampling.tasks.helpers.sliding_window import FALLBACK_SLIDING_WINDOW_SIZE
from sentry.dynamic_sampling.types import DynamicSamplingMode, SamplingMeasure
from sentry.dynamic_sampling.utils import has_custom_dynamic_sampling
from sentry.models.options.project_option import ProjectOption
@@ -50,12 +54,17 @@ class BaseDynamicSamplingConfiguration(ABC):
def __init__(self, organization: Organization) -> None:
self.organization = organization
+ self.sliding_window_sample_rate: TargetSampleRate = None
@property
@abstractmethod
def is_enabled(self) -> bool:
raise NotImplementedError
+ @abstractmethod
+ def get_sample_rate(self) -> TargetSampleRate:
+ raise NotImplementedError
+
@property
def is_span_based(self) -> bool:
return self.measure == SamplingMeasure.SPANS
@@ -79,12 +88,15 @@ def _get_projects(self) -> list[Project]:
class NoDynamicSamplingConfiguration(BaseDynamicSamplingConfiguration):
def __init__(self) -> None:
- pass
+ self.sliding_window_sample_rate: TargetSampleRate = None
@property
def is_enabled(self) -> bool:
return False
+ def get_sample_rate(self) -> TargetSampleRate:
+ return None
+
class AutomaticDynamicSamplingConfiguration(BaseDynamicSamplingConfiguration):
sample_rate: TargetSampleRate
@@ -99,11 +111,34 @@ def __init__(self, organization: Organization) -> None:
except ObjectDoesNotExist as exc:
raise DynamicSamplingException(DynamicSamplingStatus.NO_SUBSCRIPTION) from exc
self.projects = self._get_projects()
+ self.sliding_window_sample_rate = self._get_sliding_window_sample_rate()
@property
def is_enabled(self) -> bool:
return self.sample_rate is not None
+ def get_sample_rate(self) -> TargetSampleRate:
+ if self.sliding_window_sample_rate is not None:
+ return self.sliding_window_sample_rate
+ return self.sample_rate
+
+ def _get_sliding_window_sample_rate(self) -> TargetSampleRate:
+ if not self.projects:
+ return None
+
+ org_volume_24h = get_eap_organization_volume(
+ self, time_interval=timedelta(hours=FALLBACK_SLIDING_WINDOW_SIZE)
+ )
+ if org_volume_24h is None:
+ return None
+
+ return compute_sliding_window_sample_rate(
+ org_id=self.organization.id,
+ project_id=None,
+ total_root_count=org_volume_24h.total,
+ window_size=FALLBACK_SLIDING_WINDOW_SIZE,
+ )
+
class CustomDynamicSamplingOrganizationConfiguration(BaseDynamicSamplingConfiguration):
sample_rate: TargetSampleRate
@@ -121,6 +156,9 @@ def __init__(self, organization: Organization) -> None:
def is_enabled(self) -> bool:
return True
+ def get_sample_rate(self) -> TargetSampleRate:
+ return self.sample_rate
+
class CustomDynamicSamplingProjectConfiguration(BaseDynamicSamplingConfiguration):
project_target_sample_rates: ProjectTargetSampleRates
@@ -138,6 +176,9 @@ def is_enabled(self) -> bool:
sample_rate is not None for sample_rate in self.project_target_sample_rates.values()
)
+ def get_sample_rate(self) -> TargetSampleRate:
+ return None
+
def _get_project_target_sample_rates(self) -> ProjectTargetSampleRates:
project_sample_rates = ProjectOption.objects.get_value_bulk(
self.projects, "sentry:target_sample_rate"
diff --git a/src/sentry/dynamic_sampling/per_org/tasks/queries.py b/src/sentry/dynamic_sampling/per_org/tasks/queries.py
index af77a229a97332..7b18516249105a 100644
--- a/src/sentry/dynamic_sampling/per_org/tasks/queries.py
+++ b/src/sentry/dynamic_sampling/per_org/tasks/queries.py
@@ -5,17 +5,18 @@
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from enum import StrEnum
-from typing import Any, Literal
+from typing import Any, Literal, Protocol
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ExtrapolationMode
-from sentry.dynamic_sampling.per_org.tasks.configuration import BaseDynamicSamplingConfiguration
from sentry.dynamic_sampling.rules.utils import ProjectId
from sentry.dynamic_sampling.tasks.boost_low_volume_transactions import ProjectTransactions
from sentry.dynamic_sampling.tasks.common import (
ACTIVE_ORGS_VOLUMES_DEFAULT_TIME_INTERVAL,
OrganizationDataVolume,
)
+from sentry.models.organization import Organization
+from sentry.models.project import Project
from sentry.search.eap.constants import SAMPLING_MODE_HIGHEST_ACCURACY
from sentry.search.eap.types import SearchResolverConfig
from sentry.search.events.types import SnubaParams
@@ -23,6 +24,11 @@
from sentry.snuba.spans_rpc import Spans
+class OrganizationVolumeConfig(Protocol):
+ organization: Organization
+ projects: list[Project]
+
+
class DynamicSamplingQueryFilters(StrEnum):
IS_SEGMENT = "sentry.is_segment:true"
@@ -86,7 +92,7 @@ def run_eap_spans_table_query_in_chunks(
def get_eap_organization_volume(
- config: BaseDynamicSamplingConfiguration,
+ config: OrganizationVolumeConfig,
time_interval: timedelta = ACTIVE_ORGS_VOLUMES_DEFAULT_TIME_INTERVAL,
) -> OrganizationDataVolume | None:
end_time = datetime.now(UTC)
@@ -128,7 +134,7 @@ def get_eap_organization_volume(
def get_eap_project_volumes(
- config: BaseDynamicSamplingConfiguration,
+ config: OrganizationVolumeConfig,
time_interval: timedelta = timedelta(hours=1),
) -> list[ProjectVolume]:
end_time = datetime.now(UTC)
@@ -177,7 +183,7 @@ def get_eap_project_volumes(
def get_eap_transaction_volumes(
- config: BaseDynamicSamplingConfiguration,
+ config: OrganizationVolumeConfig,
time_interval: timedelta = ACTIVE_ORGS_VOLUMES_DEFAULT_TIME_INTERVAL,
order_by_volume: Literal["asc", "desc"] = "asc",
max_transactions: int = 100,
diff --git a/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py b/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
index 3648da8ef7e3a1..b9d2c0d7e647fd 100644
--- a/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
+++ b/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from collections.abc import Callable
+from datetime import timedelta
from typing import NamedTuple
from unittest.mock import patch
@@ -18,6 +19,8 @@
DynamicSamplingException,
DynamicSamplingStatus,
)
+from sentry.dynamic_sampling.tasks.common import OrganizationDataVolume
+from sentry.dynamic_sampling.tasks.helpers.sliding_window import FALLBACK_SLIDING_WINDOW_SIZE
from sentry.dynamic_sampling.types import DynamicSamplingMode, SamplingMeasure
from sentry.models.organization import Organization
from sentry.testutils.cases import TestCase
@@ -66,6 +69,66 @@ def test_subscription_backed_org_uses_blended_sample_rate(self) -> None:
with pytest.raises(AttributeError):
getattr(configuration, "project_target_sample_rates")
get_blended_sample_rate.assert_called_once_with(organization_id=org.id)
+ assert configuration.get_sample_rate() == 0.5
+
+ def test_subscription_backed_org_uses_eap_sliding_window_sample_rate(self) -> None:
+ org = self.create_organization()
+ self.create_project(organization=org)
+ sliding_window_volume = OrganizationDataVolume(org_id=org.id, total=1000, indexed=250)
+
+ with (
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
+ return_value=0.5,
+ ),
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.get_eap_organization_volume",
+ return_value=sliding_window_volume,
+ ) as get_volume,
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.compute_sliding_window_sample_rate",
+ return_value=0.25,
+ ) as compute_sample_rate,
+ ):
+ configuration = get_configuration(org.id)
+
+ assert isinstance(configuration, AutomaticDynamicSamplingConfiguration)
+ assert configuration.get_sample_rate() == 0.25
+ get_volume.assert_called_once()
+ assert get_volume.call_args.kwargs["time_interval"] == timedelta(
+ hours=FALLBACK_SLIDING_WINDOW_SIZE
+ )
+ compute_sample_rate.assert_called_once_with(
+ org_id=org.id,
+ project_id=None,
+ total_root_count=1000,
+ window_size=FALLBACK_SLIDING_WINDOW_SIZE,
+ )
+
+ def test_subscription_backed_org_falls_back_to_blended_sample_rate_without_volume(
+ self,
+ ) -> None:
+ org = self.create_organization()
+ self.create_project(organization=org)
+
+ with (
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
+ return_value=0.5,
+ ),
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.get_eap_organization_volume",
+ return_value=None,
+ ),
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.compute_sliding_window_sample_rate",
+ ) as compute_sample_rate,
+ ):
+ configuration = get_configuration(org.id)
+
+ assert isinstance(configuration, AutomaticDynamicSamplingConfiguration)
+ assert configuration.get_sample_rate() == 0.5
+ compute_sample_rate.assert_not_called()
def test_subscription_backed_org_without_sample_rate_is_disabled(self) -> None:
org = self.create_organization()
@@ -142,6 +205,7 @@ def test_org_mode_custom_dynamic_sampling_uses_org_target_sample_rate(self) -> N
measure_case.expected_measure == SamplingMeasure.SEGMENTS
)
assert configuration.sample_rate == 0.3
+ assert configuration.get_sample_rate() == 0.3
with pytest.raises(AttributeError):
getattr(configuration, "project_target_sample_rates")
get_blended_sample_rate.assert_not_called()
@@ -182,6 +246,7 @@ def test_project_mode_custom_dynamic_sampling_stores_project_sample_rates(self)
project.id: 0.2,
project_without_rate.id: None,
}
+ assert configuration.get_sample_rate() is None
with pytest.raises(AttributeError):
getattr(configuration, "sample_rate")
get_blended_sample_rate.assert_not_called()
diff --git a/tests/sentry/dynamic_sampling/per_org/tasks/test_queries.py b/tests/sentry/dynamic_sampling/per_org/tasks/test_queries.py
index af2b7a4f30f575..7a0770184c6c16 100644
--- a/tests/sentry/dynamic_sampling/per_org/tasks/test_queries.py
+++ b/tests/sentry/dynamic_sampling/per_org/tasks/test_queries.py
@@ -84,9 +84,15 @@ def get_config(
self,
organization: Organization,
) -> BaseDynamicSamplingConfiguration:
- with patch(
- "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
- return_value=1.0,
+ with (
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
+ return_value=1.0,
+ ),
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.get_eap_organization_volume",
+ return_value=None,
+ ),
):
return get_configuration(organization.id)
@@ -207,8 +213,8 @@ def test_get_eap_project_volumes_without_traffic(self) -> None:
self.create_project(organization=organization)
with patch(
- "sentry.dynamic_sampling.per_org.tasks.queries.Spans.run_table_query",
- return_value={"data": []},
+ "sentry.dynamic_sampling.per_org.tasks.queries.run_eap_spans_table_query_in_chunks",
+ return_value=[],
):
project_volumes = get_eap_project_volumes(
self.get_config(organization), time_interval=timedelta(hours=1)
@@ -259,8 +265,8 @@ def test_get_eap_project_volumes_without_projects(self) -> None:
organization = self.create_organization()
with patch(
- "sentry.dynamic_sampling.per_org.tasks.queries.Spans.run_table_query",
- return_value={"data": []},
+ "sentry.dynamic_sampling.per_org.tasks.queries.run_eap_spans_table_query_in_chunks",
+ return_value=[],
) as run_table_query:
project_volumes = get_eap_project_volumes(
self.get_config(organization), time_interval=timedelta(hours=1)
@@ -268,7 +274,8 @@ def test_get_eap_project_volumes_without_projects(self) -> None:
assert project_volumes == []
run_table_query.assert_called_once()
- assert run_table_query.call_args.kwargs["params"].projects == []
+ query = run_table_query.call_args.args[0]
+ assert query["params"].projects == []
class EAPTransactionVolumesTest(TestCase, SnubaTestCase, SpanTestCase):
From 61bae642c2aea9cc7f084990f6eaba4af030145b Mon Sep 17 00:00:00 2001
From: Simon Hellmayr
Date: Thu, 28 May 2026 13:17:55 +0200
Subject: [PATCH 05/14] chore(dynamic-sampling): with multiple org volumes,
make sure their duration is clear in scheduler (#116367)
just a variable rename, but important for readability, we're going to
query different durations
---
src/sentry/dynamic_sampling/per_org/tasks/scheduler.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/sentry/dynamic_sampling/per_org/tasks/scheduler.py b/src/sentry/dynamic_sampling/per_org/tasks/scheduler.py
index 86b92bcaa5de1c..6a710e6178292b 100644
--- a/src/sentry/dynamic_sampling/per_org/tasks/scheduler.py
+++ b/src/sentry/dynamic_sampling/per_org/tasks/scheduler.py
@@ -108,8 +108,8 @@ def run_calculations_per_org_task(org_id: OrganizationId) -> DynamicSamplingStat
if not config.projects:
return DynamicSamplingStatus.ORG_HAS_NO_PROJECTS
- org_volume = get_eap_organization_volume(config)
- if org_volume is None:
+ org_volume_5m = get_eap_organization_volume(config)
+ if org_volume_5m is None:
return DynamicSamplingStatus.NO_ORG_VOLUME
if config.should_balance_projects:
From 1ed25cbf0f14fb070cd811b272fa1df183df8dab Mon Sep 17 00:00:00 2001
From: Simon Hellmayr
Date: Thu, 28 May 2026 14:53:40 +0200
Subject: [PATCH 06/14] ref(dynamic-sampling): only run sliding window
calculations when config is enabled (#116371)
- Add an enabled check to the config init to prevent queries from being
run if DS is not enabled for this org
---
.../dynamic_sampling/per_org/tasks/configuration.py | 2 ++
.../per_org/tasks/test_configuration.py | 13 ++++++++++---
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/sentry/dynamic_sampling/per_org/tasks/configuration.py b/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
index 5c6ef598d5d083..a7635681759279 100644
--- a/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
+++ b/src/sentry/dynamic_sampling/per_org/tasks/configuration.py
@@ -110,6 +110,8 @@ def __init__(self, organization: Organization) -> None:
)
except ObjectDoesNotExist as exc:
raise DynamicSamplingException(DynamicSamplingStatus.NO_SUBSCRIPTION) from exc
+ if not self.is_enabled:
+ return
self.projects = self._get_projects()
self.sliding_window_sample_rate = self._get_sliding_window_sample_rate()
diff --git a/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py b/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
index b9d2c0d7e647fd..8fda8542883540 100644
--- a/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
+++ b/tests/sentry/dynamic_sampling/per_org/tasks/test_configuration.py
@@ -132,15 +132,22 @@ def test_subscription_backed_org_falls_back_to_blended_sample_rate_without_volum
def test_subscription_backed_org_without_sample_rate_is_disabled(self) -> None:
org = self.create_organization()
+ self.create_project(organization=org)
- with patch(
- "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
- return_value=None,
+ with (
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.quotas.backend.get_blended_sample_rate",
+ return_value=None,
+ ),
+ patch(
+ "sentry.dynamic_sampling.per_org.tasks.configuration.get_eap_organization_volume",
+ ) as get_volume,
):
configuration = get_configuration(org.id)
assert isinstance(configuration, NoDynamicSamplingConfiguration)
assert not configuration.is_enabled
+ get_volume.assert_not_called()
with pytest.raises(AttributeError):
getattr(configuration, "measure")
with pytest.raises(AttributeError):
From 42e4849a003884ff50f883ea6f8db59c28208624 Mon Sep 17 00:00:00 2001
From: Evan Purkhiser
Date: Thu, 28 May 2026 09:52:54 -0400
Subject: [PATCH 07/14] fix(discord): Route App Directory install through API
pipeline modal (#116375)
Discord App Directory installs were broken: `build_integration` was
rewritten to read state from top-level keys when legacy pipeline views
were removed in #113323, but the surviving
`DiscordExtensionConfigurationView` still bound state under
`state["discord"]`, so users hit `Invalid guild ID. The Discord guild ID
must be entirely numeric.` after authorizing in Discord's App Directory.
This rewires the App Directory entry point through the API pipeline:
- Adds an initial-data serializer accepting `code`, `guild_id`, and
`use_configure`. `OrganizationPipelineEndpoint` binds each validated
field to top-level pipeline state at initialize time.
- Branches `DiscordOAuthApiStep.get_step_data` on `use_configure`: when
set, return `{appDirectoryInstall: True, code, guildId, state}` so the
frontend can advance immediately instead of opening a popup. The
returned `state` is the pipeline's own signature, so `handle_post`'s
existing CSRF check passes unchanged with no conditional.
- `build_integration` reads `use_configure` from top-level state to pick
`configure_url` vs `setup_url` for the OAuth token-exchange
`redirect_uri` parameter (Discord requires that value to byte-exactly
match what was used at authorize).
- Replaces `DiscordExtensionConfigurationView` with a small
`RedirectView` that forwards `/extensions/discord/configure/?...` to
`/extensions/discord/link/?...`. The URL has to keep resolving because
Discord's App Directory listing has it registered as the OAuth
`redirect_uri`; we can't remove it without Discord-side coordination.
Frontend changes (link view triggers the pipeline modal with the URL
params as `initialData`, and the Discord OAuth step component
auto-advances when `appDirectoryInstall` is set) ship in a follow-up PR.
---
.../integrations/discord/integration.py | 37 ++++++--
src/sentry/integrations/discord/urls.py | 10 +--
.../discord/views/configure_redirect.py | 16 ++++
.../web/discord_extension_configuration.py | 13 ---
.../integrations/parsers/discord.py | 6 +-
.../integrations/discord/test_integration.py | 90 +++++++++++++++++--
6 files changed, 138 insertions(+), 34 deletions(-)
create mode 100644 src/sentry/integrations/discord/views/configure_redirect.py
delete mode 100644 src/sentry/integrations/web/discord_extension_configuration.py
diff --git a/src/sentry/integrations/discord/integration.py b/src/sentry/integrations/discord/integration.py
index 06430ac58c1671..0acb6959223f6a 100644
--- a/src/sentry/integrations/discord/integration.py
+++ b/src/sentry/integrations/discord/integration.py
@@ -148,6 +148,19 @@ class DiscordOAuthApiSerializer(CamelSnakeSerializer):
guild_id = CharField(required=True)
+class DiscordInitialDataSerializer(CamelSnakeSerializer):
+ """Initial pipeline data for App Directory-originated Discord installs.
+
+ When a user installs from Discord's App Directory, Discord initiates OAuth
+ and redirects back to Sentry with `code` and `guild_id`. The frontend
+ forwards them here so the pipeline can skip its own OAuth step.
+ """
+
+ code = CharField(required=False)
+ guild_id = CharField(required=False)
+ use_configure = CharField(required=False)
+
+
class DiscordOAuthApiStep:
"""API-mode OAuth step for Discord integration setup.
@@ -170,7 +183,18 @@ def __init__(
self.scopes = scopes
self.redirect_url = redirect_url
- def get_step_data(self, pipeline: IntegrationPipeline, request: HttpRequest) -> dict[str, str]:
+ def get_step_data(self, pipeline: IntegrationPipeline, request: HttpRequest) -> dict[str, Any]:
+ # App Directory installs arrive with OAuth already complete: code and
+ # guild_id are bound to state via initialData. Signal the frontend to
+ # advance immediately using those values instead of opening a popup.
+ if pipeline.fetch_state("use_configure"):
+ return {
+ "appDirectoryInstall": True,
+ "code": pipeline.fetch_state("code"),
+ "guildId": pipeline.fetch_state("guild_id"),
+ "state": pipeline.signature,
+ }
+
params = urlencode(
{
"client_id": self.client_id,
@@ -245,6 +269,9 @@ def get_pipeline_api_steps(self) -> ApiPipelineSteps[IntegrationPipeline]:
),
]
+ def get_initial_data_serializer_cls(self) -> type[DiscordInitialDataSerializer]:
+ return DiscordInitialDataSerializer
+
def build_integration(self, state: Mapping[str, Any]) -> IntegrationData:
guild_id = str(state.get("guild_id"))
@@ -258,11 +285,9 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData:
except (ApiError, AttributeError):
guild_name = guild_id
- discord_config = state.get(IntegrationProviderSlug.DISCORD.value, {})
- if isinstance(discord_config, dict):
- use_configure = discord_config.get("use_configure") == "1"
- else:
- use_configure = False
+ # App Directory installs initiated OAuth with configure_url as the
+ # redirect_uri, so token exchange must echo it back.
+ use_configure = state.get("use_configure") == "1"
url = self.configure_url if use_configure else self.setup_url
auth_code = str(state.get("code"))
diff --git a/src/sentry/integrations/discord/urls.py b/src/sentry/integrations/discord/urls.py
index 2e6657bc57758c..7ae5b50b64ac70 100644
--- a/src/sentry/integrations/discord/urls.py
+++ b/src/sentry/integrations/discord/urls.py
@@ -1,9 +1,7 @@
from django.urls import re_path
from sentry.integrations.discord.spec import DiscordMessagingSpec
-from sentry.integrations.web.discord_extension_configuration import (
- DiscordExtensionConfigurationView,
-)
+from sentry.integrations.discord.views.configure_redirect import DiscordConfigureRedirectView
from .webhooks.base import DiscordInteractionsEndpoint
@@ -13,10 +11,12 @@
DiscordInteractionsEndpoint.as_view(),
name="sentry-integration-discord-interactions",
),
- # Discord App Directory extension install flow
+ # Discord App Directory's redirect_uri lands here after the user authorizes
+ # in Discord. We forward the OAuth params to the link view, which opens the
+ # install pipeline modal to finish the install.
re_path(
r"^configure/$",
- DiscordExtensionConfigurationView.as_view(),
+ DiscordConfigureRedirectView.as_view(),
name="discord-extension-configuration",
),
]
diff --git a/src/sentry/integrations/discord/views/configure_redirect.py b/src/sentry/integrations/discord/views/configure_redirect.py
new file mode 100644
index 00000000000000..d9f7145fa9324e
--- /dev/null
+++ b/src/sentry/integrations/discord/views/configure_redirect.py
@@ -0,0 +1,16 @@
+from django.views.generic.base import RedirectView
+
+from sentry.web.frontend.base import control_silo_view
+
+
+@control_silo_view
+class DiscordConfigureRedirectView(RedirectView):
+ """OAuth redirect target for Discord App Directory installs.
+
+ Forwards `code` and `guild_id` from Discord's OAuth callback to the
+ integration link view, which picks an org and opens the install pipeline.
+ """
+
+ url = "/extensions/discord/link/"
+ query_string = True
+ permanent = False
diff --git a/src/sentry/integrations/web/discord_extension_configuration.py b/src/sentry/integrations/web/discord_extension_configuration.py
deleted file mode 100644
index 854cabf32982af..00000000000000
--- a/src/sentry/integrations/web/discord_extension_configuration.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from sentry.integrations.types import IntegrationProviderSlug
-from sentry.web.frontend.base import control_silo_view
-
-from .integration_extension_configuration import IntegrationExtensionConfigurationView
-
-
-@control_silo_view
-class DiscordExtensionConfigurationView(IntegrationExtensionConfigurationView):
- provider = IntegrationProviderSlug.DISCORD.value
- external_provider_key = IntegrationProviderSlug.DISCORD.value
-
- def map_params_to_state(self, params):
- return {"use_configure": "1", **params}
diff --git a/src/sentry/middleware/integrations/parsers/discord.py b/src/sentry/middleware/integrations/parsers/discord.py
index 390e84ff8c1d38..041a45540c62aa 100644
--- a/src/sentry/middleware/integrations/parsers/discord.py
+++ b/src/sentry/middleware/integrations/parsers/discord.py
@@ -12,6 +12,7 @@
from sentry.hybridcloud.outbox.category import WebhookProviderIdentifier
from sentry.integrations.discord.message_builder.base.flags import EPHEMERAL_FLAG
from sentry.integrations.discord.requests.base import DiscordRequest, DiscordRequestError
+from sentry.integrations.discord.views.configure_redirect import DiscordConfigureRedirectView
from sentry.integrations.discord.views.link_identity import DiscordLinkIdentityView
from sentry.integrations.discord.views.unlink_identity import DiscordUnlinkIdentityView
from sentry.integrations.discord.webhooks.base import DiscordInteractionsEndpoint
@@ -22,9 +23,6 @@
)
from sentry.integrations.models.integration import Integration
from sentry.integrations.types import EXTERNAL_PROVIDERS, ExternalProviders
-from sentry.integrations.web.discord_extension_configuration import (
- DiscordExtensionConfigurationView,
-)
from sentry.middleware.integrations.tasks import convert_to_async_discord_response
from sentry.types.cell import Cell
@@ -38,7 +36,7 @@ class DiscordRequestParser(BaseRequestParser):
control_classes = [
DiscordLinkIdentityView,
DiscordUnlinkIdentityView,
- DiscordExtensionConfigurationView,
+ DiscordConfigureRedirectView,
]
# Dynamically set to avoid RawPostDataException from double reads
diff --git a/tests/sentry/integrations/discord/test_integration.py b/tests/sentry/integrations/discord/test_integration.py
index 1605e4df8eb62b..68c0236e6a1a52 100644
--- a/tests/sentry/integrations/discord/test_integration.py
+++ b/tests/sentry/integrations/discord/test_integration.py
@@ -366,12 +366,11 @@ def _get_pipeline_url(self) -> str:
args=[self.organization.slug, IntegrationPipeline.pipeline_name],
)
- def _initialize_pipeline(self) -> Any:
- return self.client.post(
- self._get_pipeline_url(),
- data={"action": "initialize", "provider": "discord"},
- format="json",
- )
+ def _initialize_pipeline(self, initial_data: dict[str, Any] | None = None) -> Any:
+ payload: dict[str, Any] = {"action": "initialize", "provider": "discord"}
+ if initial_data is not None:
+ payload["initialData"] = initial_data
+ return self.client.post(self._get_pipeline_url(), data=payload, format="json")
def _advance_step(self, data: dict[str, Any]) -> Any:
return self.client.post(self._get_pipeline_url(), data=data, format="json")
@@ -459,3 +458,82 @@ def test_full_pipeline_flow(self, mock_set_application_command: mock.MagicMock)
organization_id=self.organization.id,
integration=integration,
).exists()
+
+ @responses.activate
+ def test_app_directory_initialize_returns_auto_advance_data(self) -> None:
+ resp = self._initialize_pipeline(
+ initial_data={
+ "code": "discord-auth-code",
+ "guildId": self.guild_id,
+ "useConfigure": "1",
+ }
+ )
+ assert resp.status_code == 200
+ assert resp.data["step"] == "oauth_login"
+ data = resp.data["data"]
+ assert data["appDirectoryInstall"] is True
+ assert data["code"] == "discord-auth-code"
+ assert data["guildId"] == self.guild_id
+ assert "state" in data
+ assert "oauthUrl" not in data
+
+ @responses.activate
+ @mock.patch("sentry.integrations.discord.client.DiscordClient.set_application_command")
+ def test_app_directory_full_flow(self, mock_set_application_command: mock.MagicMock) -> None:
+ responses.add(
+ responses.GET,
+ url=f"{DiscordClient.base_url}{GUILD_URL.format(guild_id=self.guild_id)}",
+ match=[header_matcher({"Authorization": f"Bot {self.bot_token}"})],
+ json={"id": self.guild_id, "name": self.guild_name},
+ )
+ responses.add(
+ responses.GET,
+ url=f"{DiscordClient.base_url}{APPLICATION_COMMANDS_URL.format(application_id=self.application_id)}",
+ match=[header_matcher({"Authorization": f"Bot {self.bot_token}"})],
+ json=COMMANDS,
+ )
+ responses.add(
+ responses.POST,
+ url=f"{DISCORD_BASE_URL}/oauth2/token",
+ json={"access_token": "access_token"},
+ )
+ responses.add(
+ responses.GET,
+ url=f"{DiscordClient.base_url}/users/@me",
+ json={"id": "user_1234"},
+ )
+ responses.add(
+ responses.GET,
+ url=f"{DiscordClient.base_url}/users/@me/guilds/{self.guild_id}/member",
+ json={},
+ )
+
+ resp = self._initialize_pipeline(
+ initial_data={
+ "code": "discord-auth-code",
+ "guildId": self.guild_id,
+ "useConfigure": "1",
+ }
+ )
+ data = resp.data["data"]
+ assert data["appDirectoryInstall"] is True
+ pipeline_signature = data["state"]
+
+ resp = self._advance_step(
+ {
+ "code": data["code"],
+ "state": pipeline_signature,
+ "guildId": data["guildId"],
+ }
+ )
+ assert resp.status_code == 200
+ assert resp.data["status"] == "complete"
+
+ # Token exchange must echo `configure_url` as redirect_uri because OAuth
+ # was initiated from Discord's App Directory with that URL.
+ token_calls = [c for c in responses.calls if c.request.url.endswith("/oauth2/token")]
+ assert len(token_calls) == 1
+ token_body = token_calls[0].request.body
+ if isinstance(token_body, bytes):
+ token_body = token_body.decode()
+ assert "configure" in token_body
From c35ded6b21ebc74006d4ffc0fc599fd940550685 Mon Sep 17 00:00:00 2001
From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com>
Date: Thu, 28 May 2026 10:24:45 -0400
Subject: [PATCH 08/14] test(dashboards): validate prebuilt widget layouts and
lengths (#116217)
---
.../dashboards/utils/prebuiltConfigs.spec.ts | 43 +++++++++++++++++++
.../dashboards/utils/prebuiltConfigs.tsx | 22 +++++++++-
.../prebuiltConfigs/ai/aiAgentsModels.ts | 7 ++-
.../prebuiltConfigs/ai/aiAgentsOverview.ts | 7 ++-
.../utils/prebuiltConfigs/ai/aiAgentsTools.ts | 7 ++-
.../utils/prebuiltConfigs/ai/mcpOverview.ts | 7 ++-
.../utils/prebuiltConfigs/ai/mcpPrompts.ts | 7 ++-
.../utils/prebuiltConfigs/ai/mcpResources.ts | 7 ++-
.../utils/prebuiltConfigs/ai/mcpTools.ts | 7 ++-
.../backendOverview/backendOverview.ts | 9 ++--
.../utils/prebuiltConfigs/caches/caches.ts | 9 ++--
.../frontendAssets/frontendAssets.ts | 9 ++--
.../frontendAssets/frontendAssetsDetails.ts | 11 +++--
.../frontendOverview/frontendOverview.ts | 9 ++--
.../prebuiltConfigs/http/domainSummary.ts | 11 +++--
.../utils/prebuiltConfigs/http/http.ts | 9 ++--
.../laravelOverview/laravelOverview.ts | 13 +++---
.../prebuiltConfigs/mobileSessionHealth.ts | 11 +++--
.../prebuiltConfigs/mobileVitals/appStarts.ts | 28 ++++++------
.../mobileVitals/mobileVitals.ts | 28 ++++++------
.../mobileVitals/screenLoads.ts | 29 +++++++------
.../mobileVitals/screenRendering.ts | 4 +-
.../nextJsOverview/nextJsOverview.ts | 13 +++---
.../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 13 ++++--
.../prebuiltConfigs/queues/queueCharts.ts | 5 ++-
.../prebuiltConfigs/queues/queueDetails.ts | 11 +++--
.../utils/prebuiltConfigs/queues/queues.ts | 9 ++--
.../utils/spaceWidgetsEquallyOnRow.ts | 12 ++++--
28 files changed, 246 insertions(+), 111 deletions(-)
create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs.spec.ts
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs.spec.ts b/static/app/views/dashboards/utils/prebuiltConfigs.spec.ts
new file mode 100644
index 00000000000000..0ee4a66b71b070
--- /dev/null
+++ b/static/app/views/dashboards/utils/prebuiltConfigs.spec.ts
@@ -0,0 +1,43 @@
+import {NUM_DESKTOP_COLS} from 'sentry/views/dashboards/constants';
+import {
+ PREBUILT_DASHBOARDS,
+ PrebuiltDashboardId,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+
+// Must match the limits enforced by the backend serializer at
+// src/sentry/api/serializers/rest_framework/dashboard.py.
+const MAX_WIDGET_DESCRIPTION_LENGTH = 350;
+const MAX_WIDGET_TITLE_LENGTH = 255;
+const MAX_DASHBOARD_TITLE_LENGTH = 255;
+
+const entries = Object.entries(PREBUILT_DASHBOARDS) as Array<
+ [`${PrebuiltDashboardId}`, (typeof PREBUILT_DASHBOARDS)[PrebuiltDashboardId]]
+>;
+
+describe('PREBUILT_DASHBOARDS', () => {
+ it.each(entries)('dashboard %s has a title within the backend limit', (_id, config) => {
+ expect(config.title.length).toBeLessThanOrEqual(MAX_DASHBOARD_TITLE_LENGTH);
+ });
+
+ describe.each(entries)('dashboard %s widgets', (_id, config) => {
+ it.each(
+ config.widgets.map((widget, index) => [index, widget.title, widget] as const)
+ )('widget %i (%s) passes backend validation', (_index, _title, widget) => {
+ expect(widget.title.length).toBeLessThanOrEqual(MAX_WIDGET_TITLE_LENGTH);
+ expect(widget.description?.length ?? 0).toBeLessThanOrEqual(
+ MAX_WIDGET_DESCRIPTION_LENGTH
+ );
+
+ // x and w bounds are enforced by PrebuiltWidgetLayout's literal-union
+ // types; only the wide fields and the cross-field x+w invariant
+ // need runtime checks.
+ const layout = widget.layout;
+ if (layout) {
+ expect(layout.y).toBeGreaterThanOrEqual(0);
+ expect(layout.h).toBeGreaterThanOrEqual(1);
+ expect(layout.minH).toBeGreaterThanOrEqual(1);
+ expect(layout.x + layout.w).toBeLessThanOrEqual(NUM_DESKTOP_COLS);
+ }
+ });
+ });
+});
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs.tsx b/static/app/views/dashboards/utils/prebuiltConfigs.tsx
index 31bfbbe2047240..29ef57c650612f 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs.tsx
+++ b/static/app/views/dashboards/utils/prebuiltConfigs.tsx
@@ -1,5 +1,9 @@
import type {Project} from 'sentry/types/project';
-import {type DashboardDetails} from 'sentry/views/dashboards/types';
+import {
+ type DashboardDetails,
+ type Widget,
+ type WidgetLayout,
+} from 'sentry/views/dashboards/types';
import {AI_AGENTS_MODELS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels';
import {AI_AGENTS_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview';
import {AI_AGENTS_TOOLS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools';
@@ -90,7 +94,21 @@ type OnboardingConfig =
type: 'overview';
};
-export type PrebuiltDashboard = Omit & {
+// Narrow x/w to literal unions so the desktop-grid invariants (x in [0,5],
+// w in [1,6]) are checked at compile time for prebuilt configs. User-created
+// widgets stay on the wider `WidgetLayout` because their layouts come from
+// react-grid-layout arithmetic, not hand-written literals.
+export type PrebuiltWidgetLayout = Omit & {
+ w: 1 | 2 | 3 | 4 | 5 | 6;
+ x: 0 | 1 | 2 | 3 | 4 | 5;
+};
+
+export type PrebuiltWidget = Omit & {
+ layout?: PrebuiltWidgetLayout | null;
+};
+
+export type PrebuiltDashboard = Omit & {
+ widgets: PrebuiltWidget[];
onboarding?: OnboardingConfig;
};
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts
index 0fcbd8738c360b..ab59d25dfcf320 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {AI_AGENTS_MODELS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -97,7 +100,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const MODELS_TABLE = {
+const MODELS_TABLE: PrebuiltWidget = {
id: 'ai-agents-models-table',
title: t('Models'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts
index 64dcdcf3b20a29..85494331b30fb9 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts
@@ -2,7 +2,10 @@ import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable';
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, MAX_TABLE_LIMIT, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {AI_AGENTS_OVERVIEW_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {
WIDGET_COLUMN_LABELS,
@@ -206,7 +209,7 @@ const SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const AGENTS_TRACES_TABLE = {
+const AGENTS_TRACES_TABLE: PrebuiltWidget = {
id: 'ai-agents-traces-table',
title: t('Traces'),
description: t('Agent traces with duration, token, cost, and tool usage.'),
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts
index acc1cfd9ad15b0..1ade63e690a12d 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {AI_AGENTS_TOOLS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -56,7 +59,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const TOOLS_TABLE = {
+const TOOLS_TABLE: PrebuiltWidget = {
id: 'ai-agents-tools-table',
title: t('Tools'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts
index 4402cbdc225d2d..fa9974cdec535d 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts
@@ -1,6 +1,9 @@
import {t} from 'sentry/locale';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {MCP_OVERVIEW_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -164,7 +167,7 @@ const SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const OVERVIEW_TABLE = {
+const OVERVIEW_TABLE: PrebuiltWidget = {
id: 'mcp-overview-table',
title: t('MCP Overview'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts
index 914f705a9440b1..f958f2b4d2ec91 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {MCP_PROMPTS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -76,7 +79,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const PROMPTS_TABLE = {
+const PROMPTS_TABLE: PrebuiltWidget = {
id: 'mcp-prompts-table',
title: t('Prompts'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts
index 363a577582af87..767082f0129821 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {MCP_RESOURCES_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -76,7 +79,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const RESOURCES_TABLE = {
+const RESOURCES_TABLE: PrebuiltWidget = {
id: 'mcp-resources-table',
title: t('Resources'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts
index cf49305811d13d..e620c6e94a57f4 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {MCP_TOOLS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -76,7 +79,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 3, minH: 3}
);
-const TOOLS_TABLE = {
+const TOOLS_TABLE: PrebuiltWidget = {
id: 'mcp-tools-table',
title: t('Tools'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
index 48105ec26d72fd..f34bc777f10d4a 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
@@ -2,8 +2,11 @@ import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable';
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/backendOverview/settings';
import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings';
import {
@@ -215,7 +218,7 @@ const TABLE_FIELDS = [
`sum(${SpanFields.SPAN_DURATION})`,
];
-const TRANSACTIONS_TABLE: Widget = {
+const TRANSACTIONS_TABLE: PrebuiltWidget = {
id: 'backend-overview-transactions-table',
title: t('Transactions'),
description: '',
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/caches/caches.ts b/static/app/views/dashboards/utils/prebuiltConfigs/caches/caches.ts
index 8cef72382c079b..ae6de6c7e52dd7 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/caches/caches.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/caches/caches.ts
@@ -1,6 +1,9 @@
import {t} from 'sentry/locale';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/caches/settings';
import {
WIDGET_COLUMN_LABELS,
@@ -55,7 +58,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
0
);
-const TRANSACTION_TABLE: Widget = {
+const TRANSACTION_TABLE: PrebuiltWidget = {
id: 'transaction-table',
title: t('Transactions'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssets.ts b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssets.ts
index de895c04bfad78..a5d8833a1b1745 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssets.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssets.ts
@@ -1,8 +1,11 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings';
import {
WIDGET_COLUMN_LABELS,
@@ -57,7 +60,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
{h: 2, minH: 2}
);
-const ASSETS_TABLE: Widget = {
+const ASSETS_TABLE: PrebuiltWidget = {
id: 'assets-table',
title: t('Assets'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts
index 9272c872298c75..75e72de8a2fe84 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts
@@ -1,14 +1,17 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings';
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
import type {DefaultDetailWidgetFields} from 'sentry/views/dashboards/widgets/detailsWidget/types';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const ASSET_DESCRIPTION_WIDGET: Widget = {
+const ASSET_DESCRIPTION_WIDGET: PrebuiltWidget = {
id: 'domain-widget',
title: t('Example Asset'),
displayType: DisplayType.DETAILS,
@@ -229,7 +232,7 @@ const THIRD_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
3
);
-const ASSETS_TABLE_WIDGET: Widget = {
+const ASSETS_TABLE_WIDGET: PrebuiltWidget = {
id: 'assets-table-widget',
title: t('Pages Containing This Asset'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/frontendOverview/frontendOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/frontendOverview/frontendOverview.ts
index b1310ac4da9373..e9d532a49f5140 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/frontendOverview/frontendOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/frontendOverview/frontendOverview.ts
@@ -1,8 +1,11 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
DASHBOARD_TITLE,
FRONTEND_SDK_NAMES,
@@ -188,7 +191,7 @@ const TABLE_FIELDS = [
'performance_score(measurements.score.total)',
];
-const TRANSACTIONS_TABLE: Widget = {
+const TRANSACTIONS_TABLE: PrebuiltWidget = {
id: 'frontend-overview-table',
title: t('Transactions'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts
index bcb9055eb1e55f..b513b02cdcee71 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts
@@ -1,8 +1,11 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
PERCENTAGE_3XX,
PERCENTAGE_4XX,
@@ -23,7 +26,7 @@ import {ModuleName, SpanFields} from 'sentry/views/insights/types';
const FILTER_STRING = MutableSearch.fromQueryObject(BASE_FILTERS).formatString();
-const DOMAIN_WIDGET: Widget = {
+const DOMAIN_WIDGET: PrebuiltWidget = {
id: 'domain-widget',
title: t('Domain'),
displayType: DisplayType.DETAILS,
@@ -229,7 +232,7 @@ const CHART_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
2
);
-const TRANSACTIONS_TABLE: Widget = {
+const TRANSACTIONS_TABLE: PrebuiltWidget = {
id: 'transactions-table',
title: t('Transactions Making Requests to This Domain'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts b/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts
index bf1f86319c37d0..6a1257b73bb426 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts
@@ -2,8 +2,11 @@ import {t} from 'sentry/locale';
import {RATE_UNIT_TITLE, RateUnit} from 'sentry/utils/discover/fields';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import {type PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
PERCENTAGE_3XX,
PERCENTAGE_4XX,
@@ -80,7 +83,7 @@ const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
0
);
-const DOMAIN_TABLE: Widget = {
+const DOMAIN_TABLE: PrebuiltWidget = {
id: 'domain-table',
title: t('Domains'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/laravelOverview/laravelOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/laravelOverview/laravelOverview.ts
index 9d424168b99561..7bd98c8c5afd2a 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/laravelOverview/laravelOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/laravelOverview/laravelOverview.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
BACKEND_OVERVIEW_FIRST_ROW_WIDGETS,
BACKEND_OVERVIEW_SECOND_ROW_WIDGETS,
@@ -13,7 +16,7 @@ import {
} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {SpanFields} from 'sentry/views/insights/types';
-const PATHS_TABLE: Widget = {
+const PATHS_TABLE: PrebuiltWidget = {
id: 'paths-table',
title: t('Paths'),
displayType: DisplayType.TABLE,
@@ -64,7 +67,7 @@ const PATHS_TABLE: Widget = {
},
};
-const COMMANDS_TABLE: Widget = {
+const COMMANDS_TABLE: PrebuiltWidget = {
id: 'commands-table',
title: t('Commands'),
displayType: DisplayType.TABLE,
@@ -110,7 +113,7 @@ const COMMANDS_TABLE: Widget = {
},
};
-const JOBS_TABLE: Widget = {
+const JOBS_TABLE: PrebuiltWidget = {
id: 'jobs-table',
title: t('Jobs'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileSessionHealth.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileSessionHealth.ts
index 235432226cc315..0466949140b528 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileSessionHealth.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileSessionHealth.ts
@@ -1,6 +1,9 @@
import {t} from 'sentry/locale';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
@@ -143,7 +146,7 @@ const THIRD_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
4
);
-const CRASH_RATE_TABLE: Widget = {
+const CRASH_RATE_TABLE: PrebuiltWidget = {
id: 'crash-rate-table',
title: t('Crash Free Rate by Project'),
displayType: DisplayType.TABLE,
@@ -163,7 +166,7 @@ const CRASH_RATE_TABLE: Widget = {
layout: {x: 0, y: 6, w: 6, h: 2, minH: 2},
};
-const RELEASE_TABLE: Widget = {
+const RELEASE_TABLE: PrebuiltWidget = {
id: 'release-table',
title: t('Releases'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts
index 30887745c40226..45a4701d07ff18 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts
@@ -1,7 +1,7 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {Widget} from 'sentry/views/dashboards/types';
+import type {PrebuiltWidget} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
COLD_START_CONDITION,
@@ -14,7 +14,7 @@ import {APP_STARTS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuilt
import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const AVG_COLD_STARTS_BIG_NUMBER_WIDGET: Widget = {
+const AVG_COLD_STARTS_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-cold-starts-big-number',
title: t('Average Cold Start'),
description: '',
@@ -41,7 +41,7 @@ const AVG_COLD_STARTS_BIG_NUMBER_WIDGET: Widget = {
},
};
-const TOTAL_COLD_START_COUNT_BIG_NUMBER_WIDGET: Widget = {
+const TOTAL_COLD_START_COUNT_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'total-cold-start-count-big-number',
title: t('Cold Start Count'),
description: '',
@@ -68,7 +68,7 @@ const TOTAL_COLD_START_COUNT_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_WARM_STARTS_BIG_NUMBER_WIDGET: Widget = {
+const AVG_WARM_STARTS_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-warm-starts-big-number',
title: t('Average Warm Start'),
description: '',
@@ -95,7 +95,7 @@ const AVG_WARM_STARTS_BIG_NUMBER_WIDGET: Widget = {
},
};
-const TOTAL_WARM_START_COUNT_BIG_NUMBER_WIDGET: Widget = {
+const TOTAL_WARM_START_COUNT_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'total-warm-start-count-big-number',
title: t('Warm Start Count'),
description: '',
@@ -122,7 +122,7 @@ const TOTAL_WARM_START_COUNT_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_COLD_START_LINE_WIDGET: Widget = {
+const AVG_COLD_START_LINE_WIDGET: PrebuiltWidget = {
id: 'avg-cold-start-line',
title: t('Average Cold Start'),
description: '',
@@ -150,7 +150,7 @@ const AVG_COLD_START_LINE_WIDGET: Widget = {
},
};
-const AVG_WARM_START_LINE_WIDGET: Widget = {
+const AVG_WARM_START_LINE_WIDGET: PrebuiltWidget = {
id: 'avg-warm-start-line',
title: t('Average Warm Start'),
description: '',
@@ -178,7 +178,7 @@ const AVG_WARM_START_LINE_WIDGET: Widget = {
},
};
-const COLD_START_DEVICE_DISTRIBUTION_WIDGET: Widget = {
+const COLD_START_DEVICE_DISTRIBUTION_WIDGET: PrebuiltWidget = {
id: 'cold-start-device-distribution-bar',
title: t('Cold Start Device Distribution'),
description: '',
@@ -205,7 +205,7 @@ const COLD_START_DEVICE_DISTRIBUTION_WIDGET: Widget = {
},
};
-const WARM_START_DEVICE_DISTRIBUTION_WIDGET: Widget = {
+const WARM_START_DEVICE_DISTRIBUTION_WIDGET: PrebuiltWidget = {
id: 'warm-start-device-distribution-bar',
title: t('Warm Start Device Distribution'),
description: '',
@@ -232,7 +232,7 @@ const WARM_START_DEVICE_DISTRIBUTION_WIDGET: Widget = {
},
};
-const COLD_OPERATIONS_TABLE: Widget = {
+const COLD_OPERATIONS_TABLE: PrebuiltWidget = {
id: 'cold-operations-table',
title: t('Cold Start Operations'),
description: '',
@@ -270,7 +270,7 @@ const COLD_OPERATIONS_TABLE: Widget = {
},
};
-const WARM_OPERATIONS_TABLE: Widget = {
+const WARM_OPERATIONS_TABLE: PrebuiltWidget = {
id: 'warm-operations-table',
title: t('Warm Start Operations'),
description: '',
@@ -308,19 +308,19 @@ const WARM_OPERATIONS_TABLE: Widget = {
},
};
-const HEADER_ROW_WIDGETS: Widget[] = [
+const HEADER_ROW_WIDGETS: PrebuiltWidget[] = [
AVG_COLD_STARTS_BIG_NUMBER_WIDGET,
AVG_WARM_STARTS_BIG_NUMBER_WIDGET,
TOTAL_COLD_START_COUNT_BIG_NUMBER_WIDGET,
TOTAL_WARM_START_COUNT_BIG_NUMBER_WIDGET,
];
-const FIRST_ROW_WIDGETS: Widget[] = [
+const FIRST_ROW_WIDGETS: PrebuiltWidget[] = [
AVG_COLD_START_LINE_WIDGET,
AVG_WARM_START_LINE_WIDGET,
];
-const SECOND_ROW_WIDGETS: Widget[] = [
+const SECOND_ROW_WIDGETS: PrebuiltWidget[] = [
COLD_START_DEVICE_DISTRIBUTION_WIDGET,
WARM_START_DEVICE_DISTRIBUTION_WIDGET,
];
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts
index 6240ac7d66bc0b..e81cbd9da5aedc 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts
@@ -1,7 +1,7 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {Widget} from 'sentry/views/dashboards/types';
+import type {PrebuiltWidget} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
APP_START_TABLE_CONDITION,
@@ -18,7 +18,7 @@ import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mob
import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const COLD_START_BIG_NUMBER_WIDGET: Widget = {
+const COLD_START_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'cold-start-big-number',
title: t('Average Cold App Start'),
description: 'Average cold app start duration',
@@ -51,7 +51,7 @@ const COLD_START_BIG_NUMBER_WIDGET: Widget = {
},
};
-const WARM_START_BIG_NUMBER_WIDGET: Widget = {
+const WARM_START_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'warm-start-big-number',
title: t('Average Warm App Start'),
description: 'Average warm app start duration',
@@ -84,7 +84,7 @@ const WARM_START_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_TTID_BIG_NUMBER_WIDGET: Widget = {
+const AVG_TTID_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-ttid-big-number',
title: t('Average TTID'),
description: 'Average time to initial display',
@@ -111,7 +111,7 @@ const AVG_TTID_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_TTFD_BIG_NUMBER_WIDGET: Widget = {
+const AVG_TTFD_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-ttfd-big-number',
title: t('Average TTFD'),
description: 'Average time to full display',
@@ -140,7 +140,7 @@ const AVG_TTFD_BIG_NUMBER_WIDGET: Widget = {
// Uses the Sessions (Release) dataset, so most dashboard global filters (which target Spans)
// don't apply. Still valuable as a top-level health signal alongside the span-based vitals.
-const CRASH_FREE_SESSION_RATE_BIG_NUMBER_WIDGET: Widget = {
+const CRASH_FREE_SESSION_RATE_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'crash-free-session-rate-big-number',
title: t('Crash Free Session Rate'),
description:
@@ -168,7 +168,7 @@ const CRASH_FREE_SESSION_RATE_BIG_NUMBER_WIDGET: Widget = {
},
};
-const SLOW_FRAME_RATE_WIDGET: Widget = {
+const SLOW_FRAME_RATE_WIDGET: PrebuiltWidget = {
id: 'slow-frame-rate-big-number',
title: t('Slow Frame Rate'),
description:
@@ -205,7 +205,7 @@ const SLOW_FRAME_RATE_WIDGET: Widget = {
},
};
-const FROZEN_FRAME_RATE_WIDGET: Widget = {
+const FROZEN_FRAME_RATE_WIDGET: PrebuiltWidget = {
id: 'frozen-frame-rate-big-number',
title: t('Frozen Frame Rate'),
description:
@@ -242,7 +242,7 @@ const FROZEN_FRAME_RATE_WIDGET: Widget = {
},
};
-const AVG_FRAME_DELAY_WIDGET: Widget = {
+const AVG_FRAME_DELAY_WIDGET: PrebuiltWidget = {
id: 'avg-frame-delay-big-number',
title: t('Average Frame Delay'),
description: 'Average frame delay',
@@ -269,7 +269,7 @@ const AVG_FRAME_DELAY_WIDGET: Widget = {
},
};
-const APP_START_TABLE: Widget = {
+const APP_START_TABLE: PrebuiltWidget = {
id: 'app-start-table',
title: t('App Starts'),
description: t(
@@ -320,7 +320,7 @@ const APP_START_TABLE: Widget = {
},
};
-const SCREEN_RENDERING_TABLE: Widget = {
+const SCREEN_RENDERING_TABLE: PrebuiltWidget = {
id: 'screen-rendering-table',
title: t('Screen Rendering'),
description:
@@ -370,7 +370,7 @@ const SCREEN_RENDERING_TABLE: Widget = {
},
};
-const SCREEN_LOAD_TABLE: Widget = {
+const SCREEN_LOAD_TABLE: PrebuiltWidget = {
id: 'screen-load-table',
title: t('Screen Loads'),
description: '',
@@ -414,14 +414,14 @@ const SCREEN_LOAD_TABLE: Widget = {
},
};
-const FIRST_ROW_WIDGETS: Widget[] = [
+const FIRST_ROW_WIDGETS: PrebuiltWidget[] = [
COLD_START_BIG_NUMBER_WIDGET,
WARM_START_BIG_NUMBER_WIDGET,
AVG_TTID_BIG_NUMBER_WIDGET,
AVG_TTFD_BIG_NUMBER_WIDGET,
];
-const SECOND_ROW_WIDGETS: Widget[] = [
+const SECOND_ROW_WIDGETS: PrebuiltWidget[] = [
SLOW_FRAME_RATE_WIDGET,
FROZEN_FRAME_RATE_WIDGET,
AVG_FRAME_DELAY_WIDGET,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts
index 73eb34a2f75425..b084e6721380bf 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts
@@ -1,7 +1,7 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {Widget} from 'sentry/views/dashboards/types';
+import type {PrebuiltWidget} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
SCREEN_LOAD_CONDITION,
@@ -13,7 +13,7 @@ import {
import {SCREEN_LOADS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const AVG_TTID_BIG_NUMBER_WIDGET: Widget = {
+const AVG_TTID_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-ttid-big-number',
title: t('Average TTID'),
description: '',
@@ -40,7 +40,7 @@ const AVG_TTID_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_TTFD_BIG_NUMBER_WIDGET: Widget = {
+const AVG_TTFD_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'avg-ttfd-big-number',
title: t('Average TTFD'),
description: '',
@@ -67,7 +67,7 @@ const AVG_TTFD_BIG_NUMBER_WIDGET: Widget = {
},
};
-const TOTAL_COUNT_BIG_NUMBER_WIDGET: Widget = {
+const TOTAL_COUNT_BIG_NUMBER_WIDGET: PrebuiltWidget = {
id: 'total-count-big-number',
title: t('Total Count'),
description: '',
@@ -94,7 +94,7 @@ const TOTAL_COUNT_BIG_NUMBER_WIDGET: Widget = {
},
};
-const AVG_TTID_LINE_WIDGET: Widget = {
+const AVG_TTID_LINE_WIDGET: PrebuiltWidget = {
id: 'average-ttid-line',
title: t('Average TTID'),
description: '',
@@ -122,7 +122,7 @@ const AVG_TTID_LINE_WIDGET: Widget = {
},
};
-const AVG_TTFD_LINE_WIDGET: Widget = {
+const AVG_TTFD_LINE_WIDGET: PrebuiltWidget = {
id: 'average-ttfd-line',
title: t('Average TTFD'),
description: '',
@@ -150,7 +150,7 @@ const AVG_TTFD_LINE_WIDGET: Widget = {
},
};
-const TOTAL_COUNT_LINE_WIDGET: Widget = {
+const TOTAL_COUNT_LINE_WIDGET: PrebuiltWidget = {
id: 'total-count-line',
title: t('Total Count'),
description: '',
@@ -178,7 +178,7 @@ const TOTAL_COUNT_LINE_WIDGET: Widget = {
},
};
-const TTID_BAR_CHART_WIDGET: Widget = {
+const TTID_BAR_CHART_WIDGET: PrebuiltWidget = {
id: 'ttid-device-class-bar',
title: t('TTID by Device Class'),
description: '',
@@ -206,7 +206,7 @@ const TTID_BAR_CHART_WIDGET: Widget = {
},
};
-const TTFD_BAR_CHART_WIDGET: Widget = {
+const TTFD_BAR_CHART_WIDGET: PrebuiltWidget = {
id: 'ttfd-device-class-bar',
title: t('TTFD by Device Class'),
description: '',
@@ -234,7 +234,7 @@ const TTFD_BAR_CHART_WIDGET: Widget = {
},
};
-const SPAN_OPERATIONS_TABLE: Widget = {
+const SPAN_OPERATIONS_TABLE: PrebuiltWidget = {
id: 'span-operations-table',
title: t('Span Operations'),
description: '',
@@ -283,19 +283,22 @@ const SPAN_OPERATIONS_TABLE: Widget = {
},
};
-const HEADER_ROW_WIDGETS: Widget[] = [
+const HEADER_ROW_WIDGETS: PrebuiltWidget[] = [
AVG_TTID_BIG_NUMBER_WIDGET,
AVG_TTFD_BIG_NUMBER_WIDGET,
TOTAL_COUNT_BIG_NUMBER_WIDGET,
];
-const SECOND_ROW_WIDGETS: Widget[] = [
+const SECOND_ROW_WIDGETS: PrebuiltWidget[] = [
AVG_TTID_LINE_WIDGET,
AVG_TTFD_LINE_WIDGET,
TOTAL_COUNT_LINE_WIDGET,
];
-const THIRD_ROW_WIDGETS: Widget[] = [TTID_BAR_CHART_WIDGET, TTFD_BAR_CHART_WIDGET];
+const THIRD_ROW_WIDGETS: PrebuiltWidget[] = [
+ TTID_BAR_CHART_WIDGET,
+ TTFD_BAR_CHART_WIDGET,
+];
export const MOBILE_VITALS_SCREEN_LOADS_PREBUILT_CONFIG: PrebuiltDashboard = {
dateCreated: '',
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts
index b06c0013606766..1b84de1da4b2a3 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts
@@ -1,13 +1,13 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import type {Widget} from 'sentry/views/dashboards/types';
+import type {PrebuiltWidget} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {SCREEN_RENDERING_SPAN_OPERATIONS_CONDITION} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/constants';
import {SCREEN_RENDERING_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const SPAN_OPERATIONS_TABLE: Widget = {
+const SPAN_OPERATIONS_TABLE: PrebuiltWidget = {
id: 'span-operations-table',
title: t('Span Operations'),
description: '',
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview.ts
index f6d18ff0c89ba5..f033717223763c 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview.ts
@@ -1,8 +1,11 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/nextJsOverview/settings';
import {
WIDGET_COLUMN_LABELS,
@@ -141,7 +144,7 @@ const CLIENT_TRANSACTIONS_TABLE_FIELDS = [
`performance_score(${SpanFields.TOTAL_SCORE})`,
];
-const CLIENT_TRANSACTIONS_TABLE: Widget = {
+const CLIENT_TRANSACTIONS_TABLE: PrebuiltWidget = {
id: 'client-transactions-table',
title: t('Client Transactions'),
displayType: DisplayType.TABLE,
@@ -193,7 +196,7 @@ const SERVER_TRANSACTIONS_TABLE_FIELDS = [
`sum(${SpanFields.SPAN_DURATION})`,
];
-const SERVER_TRANSACTIONS_TABLE: Widget = {
+const SERVER_TRANSACTIONS_TABLE: PrebuiltWidget = {
id: 'server-transactions-table',
title: t('Server Transactions'),
displayType: DisplayType.TABLE,
@@ -234,7 +237,7 @@ const SERVER_TRANSACTIONS_TABLE: Widget = {
},
};
-const SERVER_TREE_WIDGET: Widget = {
+const SERVER_TREE_WIDGET: PrebuiltWidget = {
...SERVER_TREE_WIDGET_TEMPLATE,
layout: {
x: 0,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts
index b8e8d18bd842c7..e66218a5f45a4a 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {DurationUnit, SizeUnit} from 'sentry/utils/discover/fields';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
import {traceMetricField} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField';
@@ -198,7 +201,11 @@ const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow(
3
);
-const WIDGETS: Widget[] = [...KPI_WIDGETS, ...MEMORY_WIDGETS, ...CORRELATION_WIDGETS];
+const WIDGETS: PrebuiltWidget[] = [
+ ...KPI_WIDGETS,
+ ...MEMORY_WIDGETS,
+ ...CORRELATION_WIDGETS,
+];
export const NODE_RUNTIME_METRICS_PREBUILT_CONFIG: PrebuiltDashboard = {
dateCreated: '',
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueCharts.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueCharts.ts
index 68df7e0f56fe4c..7651de8fd85681 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueCharts.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueCharts.ts
@@ -1,8 +1,9 @@
import {t} from 'sentry/locale';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {PrebuiltWidget} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {SpanFields} from 'sentry/views/insights/types';
-export const QUEUE_CHARTS: Widget[] = [
+export const QUEUE_CHARTS: PrebuiltWidget[] = [
{
id: 'average-duration-widget',
title: t('Average Duration'),
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts
index 4a64a03f9b3a69..8215cd052adca9 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts
@@ -1,6 +1,9 @@
import {t} from 'sentry/locale';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {QUEUE_CHARTS} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueCharts';
import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
@@ -127,7 +130,7 @@ const FIRST_ROW_WIDGTS = spaceWidgetsEquallyOnRow(
const SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow([...QUEUE_CHARTS], 1);
-const PRODUCER_TABLE: Widget = {
+const PRODUCER_TABLE: PrebuiltWidget = {
id: 'producer-table',
title: t('Producer Transactions'),
displayType: DisplayType.TABLE,
@@ -162,7 +165,7 @@ const PRODUCER_TABLE: Widget = {
},
};
-const CONSUMER_TABLE: Widget = {
+const CONSUMER_TABLE: PrebuiltWidget = {
id: 'consumer-table',
title: t('Consumer Transactions'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queues.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queues.ts
index 777937b8b714c7..8bed62d629238a 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queues.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queues.ts
@@ -1,7 +1,10 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
-import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
-import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltDashboard,
+ PrebuiltWidget,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {QUEUE_CHARTS} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueCharts';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/settings';
import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
@@ -10,7 +13,7 @@ import {ModuleName, SpanFields} from 'sentry/views/insights/types';
const FIRST_ROW_WIDGETS = spaceWidgetsEquallyOnRow([...QUEUE_CHARTS], 0);
-const DESTINATION_TABLE: Widget = {
+const DESTINATION_TABLE: PrebuiltWidget = {
id: 'destination-table',
title: t('Destinations'),
displayType: DisplayType.TABLE,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow.ts b/static/app/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow.ts
index 84e32ae3f2c82c..3c67328bd4efcd 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow.ts
@@ -1,11 +1,15 @@
import {NUM_DESKTOP_COLS} from 'sentry/views/dashboards/constants';
import type {Widget, WidgetLayout} from 'sentry/views/dashboards/types';
+import type {
+ PrebuiltWidget,
+ PrebuiltWidgetLayout,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
export function spaceWidgetsEquallyOnRow(
widgets: Widget[],
y: number,
height: Pick = {h: 2, minH: 2}
-): Widget[] {
+): PrebuiltWidget[] {
if (widgets.length > NUM_DESKTOP_COLS) {
throw new Error(
`Expected no more than ${NUM_DESKTOP_COLS} widgets, got ${widgets.length}`
@@ -18,12 +22,14 @@ export function spaceWidgetsEquallyOnRow(
const widgetWidth = Math.floor(NUM_DESKTOP_COLS / widgets.length);
+ // Casts are safe: the early-return above caps widgets.length at
+ // NUM_DESKTOP_COLS, so widgetWidth in [1,6] and idx*widgetWidth in [0,5].
return widgets.map((widget, idx) => ({
...widget,
layout: {
- x: idx * widgetWidth,
+ x: (idx * widgetWidth) as PrebuiltWidgetLayout['x'],
y,
- w: widgetWidth,
+ w: widgetWidth as PrebuiltWidgetLayout['w'],
...height,
},
}));
From 012d8ec2967902084d5d01f43898c0dbc3b208a4 Mon Sep 17 00:00:00 2001
From: "sentry-junior[bot]"
<264270552+sentry-junior[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 14:45:23 +0000
Subject: [PATCH 09/14] fix(feedback): remove extra padding from LayoutGrid
component (#116377)
Replaces the previous breakpoint-specific padding on `LayoutGrid` with a
flat `padding: 8px 16px` (`space.md space.xl`) applied at all
breakpoints.
Action taken on behalf of Ryan Albrecht.
**Before**
**After**
---
[View Session in
Sentry](https://sentry.sentry.io/traces/?project=4510944073809921&query=gen_ai.conversation.id%3A%22slack%3AC0B63QA6RGA%3A1779975838.713209%22)
---------
Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
---
static/app/views/feedback/feedbackListPage.tsx | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/static/app/views/feedback/feedbackListPage.tsx b/static/app/views/feedback/feedbackListPage.tsx
index 29e715f9ece3e2..49208026cf9a9a 100644
--- a/static/app/views/feedback/feedbackListPage.tsx
+++ b/static/app/views/feedback/feedbackListPage.tsx
@@ -314,11 +314,7 @@ const LayoutGrid = styled('div')<{hideTop?: boolean}>`
gap: ${p => p.theme.space.xl};
place-items: stretch;
- padding: ${p => p.theme.space.xl};
-
- @media (min-width: ${p => p.theme.breakpoints.lg}) {
- padding: ${p => p.theme.space.xl} ${p => p.theme.space['3xl']};
- }
+ padding: ${p => p.theme.space.lg} ${p => p.theme.space.xl};
grid-template-rows: max-content minmax(0, 1fr);
grid-template-areas:
From 18bc507570ff54f2a87ac0af8961918204efa41e Mon Sep 17 00:00:00 2001
From: Mark Story
Date: Thu, 28 May 2026 10:59:24 -0400
Subject: [PATCH 10/14] chore(typing) Fix typing issues in relocations
(#116301)
Remove relocations code from the typing ignore list and fix up the
typing issues in those modules.
---
pyproject.toml | 13 -------------
src/sentry/relocation/api/endpoints/index.py | 5 +++--
src/sentry/relocation/models/relocation.py | 6 +++---
src/sentry/relocation/tasks/process.py | 12 ++++++------
src/sentry/relocation/tasks/transfer.py | 3 ++-
src/sentry/relocation/utils.py | 6 +++---
6 files changed, 17 insertions(+), 28 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index cbf0f462485b2a..b8882810da83d0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1286,19 +1286,6 @@ module = [
"sentry.release_health.tasks",
"sentry.releases",
"sentry.releases.endpoints.*",
- "sentry.relocation.api.endpoints",
- "sentry.relocation.api.endpoints.abort",
- "sentry.relocation.api.endpoints.cancel",
- "sentry.relocation.api.endpoints.details",
- "sentry.relocation.api.endpoints.index",
- "sentry.relocation.api.endpoints.pause",
- "sentry.relocation.api.endpoints.public_key",
- "sentry.relocation.api.endpoints.recover",
- "sentry.relocation.api.endpoints.retry",
- "sentry.relocation.api.endpoints.unpause",
- "sentry.relocation.models.*",
- "sentry.relocation.tasks.*",
- "sentry.relocation.utils",
"sentry.remote_subscriptions.*",
"sentry.replays",
"sentry.replays._case_studies.*",
diff --git a/src/sentry/relocation/api/endpoints/index.py b/src/sentry/relocation/api/endpoints/index.py
index e6a44d5cbcdbc2..f9e5c6d573e477 100644
--- a/src/sentry/relocation/api/endpoints/index.py
+++ b/src/sentry/relocation/api/endpoints/index.py
@@ -3,6 +3,7 @@
from datetime import timedelta
from functools import reduce
from string import Template
+from typing import Any
from django.db import router
from django.db.models import Q
@@ -53,7 +54,7 @@
RELOCATION_FILE_SIZE_MEDIUM = 100 * 1024**2
-def get_relocation_size_category(size) -> str:
+def get_relocation_size_category(size: int) -> str:
if size < RELOCATION_FILE_SIZE_SMALL:
return "small"
elif size < RELOCATION_FILE_SIZE_MEDIUM:
@@ -81,7 +82,7 @@ def should_throttle_relocation(relocation_bucket_size: str) -> bool:
return True
-class RelocationsPostSerializer(serializers.Serializer):
+class RelocationsPostSerializer(serializers.Serializer[dict[str, Any]]):
file = serializers.FileField(required=True)
orgs = serializers.CharField(required=True, allow_blank=False, allow_null=False)
owner = serializers.CharField(
diff --git a/src/sentry/relocation/models/relocation.py b/src/sentry/relocation/models/relocation.py
index 3e9e9891ecb84b..42406e23fab43b 100644
--- a/src/sentry/relocation/models/relocation.py
+++ b/src/sentry/relocation/models/relocation.py
@@ -12,7 +12,7 @@
from sentry.db.models.fields.uuid import UUIDField
-def default_guid():
+def default_guid() -> str:
return uuid4().hex
@@ -57,7 +57,7 @@ def get_in_progress_choices(cls) -> list[tuple[int, str]]:
return [(key.value, key.name) for key in cls if key.name != "COMPLETED"]
@classmethod
- def max_value(cls):
+ def max_value(cls) -> int:
return max(item.value for item in cls)
class Status(Enum):
@@ -242,7 +242,7 @@ def __str__(self) -> str:
else:
raise ValueError("Cannot extract a filename from `RelocationFile.Kind.UNKNOWN`.")
- def to_filename(self, ext: str):
+ def to_filename(self, ext: str) -> str:
return str(self) + "." + ext
relocation = FlexibleForeignKey("sentry.Relocation")
diff --git a/src/sentry/relocation/tasks/process.py b/src/sentry/relocation/tasks/process.py
index 9b7341f075cea6..72db7295c9cdb7 100644
--- a/src/sentry/relocation/tasks/process.py
+++ b/src/sentry/relocation/tasks/process.py
@@ -971,11 +971,11 @@ class NextTask:
the task to be scheduled at some later point in the execution.
"""
- task: Task
+ task: Task[..., Any]
args: list[Any]
countdown: int | None = None
- def schedule(self):
+ def schedule(self) -> None:
"""
Run the `.apply_async()` call defined by this future.
"""
@@ -1126,7 +1126,7 @@ def validating_start(uuid: str) -> None:
):
cb_client = CloudBuildClient()
- def camel_to_snake_keep_underscores(value):
+ def camel_to_snake_keep_underscores(value: str) -> str:
match = re.search(r"(_++)$", value)
converted = camel_to_snake_case(value)
return converted + (match.group(0) if match else "")
@@ -1708,11 +1708,11 @@ def completed(uuid: str) -> None:
processing_deadline_duration=FAST_TIME_LIMIT,
silo_mode=SiloMode.CELL,
)
-def noop():
+def noop() -> None:
pass
-TASK_MAP: dict[OrderedTask, Task] = {
+TASK_MAP: dict[OrderedTask, Task[..., Any]] = {
OrderedTask.NONE: noop,
OrderedTask.UPLOADING_START: uploading_start,
OrderedTask.UPLOADING_COMPLETE: uploading_complete,
@@ -1735,7 +1735,7 @@ def noop():
assert set(OrderedTask._member_map_.keys()) == {k.name for k in TASK_MAP.keys()}
-def get_first_task_for_step(target_step: Relocation.Step) -> Task | None:
+def get_first_task_for_step(target_step: Relocation.Step) -> Task[..., Any] | None:
min_task: OrderedTask | None = None
for ordered_task, step in TASK_TO_STEP.items():
if step == target_step:
diff --git a/src/sentry/relocation/tasks/transfer.py b/src/sentry/relocation/tasks/transfer.py
index 6e6b314b4e3e4e..c1e0f25c11a791 100644
--- a/src/sentry/relocation/tasks/transfer.py
+++ b/src/sentry/relocation/tasks/transfer.py
@@ -1,4 +1,5 @@
import logging
+from typing import Any
from django.db.models import Subquery
from django.utils import timezone
@@ -46,7 +47,7 @@ def find_relocation_transfer_region() -> None:
def _find_relocation_transfer(
model_cls: type[BaseRelocationTransfer],
- process_task: Task,
+ process_task: Task[..., Any],
) -> None:
"""
Advance the scheduled_for time for all transfers that are
diff --git a/src/sentry/relocation/utils.py b/src/sentry/relocation/utils.py
index 0b42c92dd7b882..2191085490efad 100644
--- a/src/sentry/relocation/utils.py
+++ b/src/sentry/relocation/utils.py
@@ -650,7 +650,7 @@ def make_cloudbuild_step_args(indent: int, args: list[str]) -> str:
# The set of arguments to invoke a "docker compose" in a cloudbuild step is tedious and repetitive -
# better to just handle it here.
@lru_cache(maxsize=1)
-def get_docker_compose_cmd():
+def get_docker_compose_cmd() -> str:
return make_cloudbuild_step_args(
3,
[
@@ -666,7 +666,7 @@ def get_docker_compose_cmd():
# The set of arguments to invoke a "docker compose run" in a cloudbuild step is tedious and
# repetitive - better to just handle it here.
@lru_cache(maxsize=1)
-def get_docker_compose_run():
+def get_docker_compose_run() -> str:
return make_cloudbuild_step_args(
3,
[
@@ -678,7 +678,7 @@ def get_docker_compose_run():
@lru_cache(maxsize=1)
-def get_relocations_bucket_name():
+def get_relocations_bucket_name() -> str:
"""
When using the local FileSystemStorage (ie, in tests), we use a contrived bucket name, since
this is really just an alias for a bespoke local directory in that case.
From de5caf4a7da97252d0e4ff7b4c29f98deb3a6a25 Mon Sep 17 00:00:00 2001
From: Nick
Date: Thu, 28 May 2026 11:59:51 -0300
Subject: [PATCH 11/14] ref(search-query-builder): Break up contexts (#116126)
The goal of this PR is to break up the context for the search query
builder into smaller more focused contexts.
Currently the single context is quite large and can probably be narrowed
down for better maintainability of the search query builder.
Closes EXP-963
---------
Co-authored-by: OpenAI Codex
---
.../searchQueryBuilder/askSeer/askSeer.tsx | 4 +-
.../askSeer/askSeerFeedback.tsx | 4 +-
.../askSeer/askSeerOption.tsx | 4 +-
.../askSeerCombobox/askSeerComboBox.spec.tsx | 4 +-
.../askSeerCombobox/askSeerComboBox.tsx | 4 +-
.../askSeerPollingComboBox.tsx | 4 +-
.../askSeerCombobox/queryTokens.tsx | 4 +-
.../components/searchQueryBuilder/context.tsx | 288 ++++++++++++------
.../formattedQuery.spec.tsx | 6 +-
.../searchQueryBuilder/formattedQuery.tsx | 4 +-
.../searchQueryBuilder/hooks/useOnChange.tsx | 4 +-
.../hooks/useQueryBuilderGridItem.tsx | 4 +-
.../hooks/useSelectOnDrag.tsx | 4 +-
.../searchQueryBuilder/hooks/useUndoStack.tsx | 4 +-
.../searchQueryBuilder/index.spec.tsx | 6 +-
.../searchQueryBuilder/index.stories.tsx | 12 +-
.../components/searchQueryBuilder/index.tsx | 21 +-
.../plainTextQueryInput.tsx | 11 +-
.../selectionKeyHandler.tsx | 10 +-
.../searchQueryBuilder/tokenizedQueryGrid.tsx | 14 +-
.../searchQueryBuilder/tokens/boolean.tsx | 11 +-
.../searchQueryBuilder/tokens/combobox.tsx | 15 +-
.../tokens/deletableToken.tsx | 4 +-
.../tokens/filter/aggregateKey.tsx | 8 +-
.../tokens/filter/filter.tsx | 18 +-
.../tokens/filter/filterKey.tsx | 4 +-
.../tokens/filter/filterKeyCombobox.tsx | 19 +-
.../tokens/filter/filterOperator.tsx | 19 +-
.../tokens/filter/functionDescription.tsx | 4 +-
.../tokens/filter/parametersCombobox.tsx | 11 +-
.../tokens/filter/useAggregateParamVisual.tsx | 4 +-
.../tokens/filter/valueCombobox.tsx | 16 +-
.../tokens/filterKeyListBox/index.tsx | 18 +-
.../filterKeyListBox/keyDescription.tsx | 4 +-
.../filterKeyListBox/useFilterKeyListBox.tsx | 27 +-
.../useRecentSearchFilters.tsx | 10 +-
.../filterKeyListBox/useRecentSearches.tsx | 4 +-
.../searchQueryBuilder/tokens/freeText.tsx | 19 +-
.../tokens/useSortedFilterKeyItems.tsx | 9 +-
.../results/issueListSeerComboBox.tsx | 17 +-
.../results/resultsSearchQueryBuilder.tsx | 4 +-
.../schemaHints/schemaHintsList.spec.tsx | 31 +-
.../schemaHints/schemaHintsList.tsx | 8 +-
static/app/views/explore/logs/logsTab.tsx | 4 +-
.../explore/logs/logsTabSeerComboBox.tsx | 16 +-
.../explore/metrics/metricToolbar/filter.tsx | 4 +-
.../metrics/metricsTabSeerComboBox.tsx | 16 +-
.../explore/spans/spansTabSearchSection.tsx | 4 +-
.../explore/spans/spansTabSeerComboBox.tsx | 16 +-
.../views/issueList/issueListSeerComboBox.tsx | 17 +-
static/app/views/issueList/issueSearch.tsx | 4 +-
51 files changed, 469 insertions(+), 312 deletions(-)
diff --git a/static/app/components/searchQueryBuilder/askSeer/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer/askSeer.tsx
index b1cf2b84fa530c..3467f365ba13c1 100644
--- a/static/app/components/searchQueryBuilder/askSeer/askSeer.tsx
+++ b/static/app/components/searchQueryBuilder/askSeer/askSeer.tsx
@@ -11,13 +11,13 @@ import {
AskSeerListItem,
AskSeerPane,
} from 'sentry/components/searchQueryBuilder/askSeer/components';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderAI} from 'sentry/components/searchQueryBuilder/context';
import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
export function AskSeer({state}: {state: ComboBoxState}) {
const organization = useOrganization();
- const {displayAskSeerFeedback} = useSearchQueryBuilder();
+ const {displayAskSeerFeedback} = useSearchQueryBuilderAI();
const isMutating = useIsMutating({
mutationKey: [setupCheckQueryKey(organization.slug)],
diff --git a/static/app/components/searchQueryBuilder/askSeer/askSeerFeedback.tsx b/static/app/components/searchQueryBuilder/askSeer/askSeerFeedback.tsx
index 0e2e82b9d5da9c..4933a8efa82f9a 100644
--- a/static/app/components/searchQueryBuilder/askSeer/askSeerFeedback.tsx
+++ b/static/app/components/searchQueryBuilder/askSeer/askSeerFeedback.tsx
@@ -5,7 +5,7 @@ import {Text} from '@sentry/scraps/text';
import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {AskSeerLabel} from 'sentry/components/searchQueryBuilder/askSeer/components';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderAI} from 'sentry/components/searchQueryBuilder/context';
import {IconSeer, IconThumb} from 'sentry/icons';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
@@ -15,7 +15,7 @@ export function AskSeerFeedback() {
const organization = useOrganization();
const analyticsArea = useAnalyticsArea();
const {setDisplayAskSeerFeedback, askSeerNLQueryRef, askSeerSuggestedQueryRef} =
- useSearchQueryBuilder();
+ useSearchQueryBuilderAI();
const handleClick = (type: 'positive' | 'negative') => {
trackAnalytics('ai_query.feedback', {
diff --git a/static/app/components/searchQueryBuilder/askSeer/askSeerOption.tsx b/static/app/components/searchQueryBuilder/askSeer/askSeerOption.tsx
index 0729dae919811b..f45ca54121a8a4 100644
--- a/static/app/components/searchQueryBuilder/askSeer/askSeerOption.tsx
+++ b/static/app/components/searchQueryBuilder/askSeer/askSeerOption.tsx
@@ -11,7 +11,7 @@ import {
AskSeerLabel,
AskSeerListItem,
} from 'sentry/components/searchQueryBuilder/askSeer/components';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderAI} from 'sentry/components/searchQueryBuilder/context';
import {IconSeer} from 'sentry/icons';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
@@ -21,7 +21,7 @@ export const ASK_SEER_ITEM_KEY = 'ask_seer';
export function AskSeerOption({state}: {state: ComboBoxState}) {
const ref = useRef(null);
- const {setDisplayAskSeer, aiSearchBadgeType} = useSearchQueryBuilder();
+ const {setDisplayAskSeer, aiSearchBadgeType} = useSearchQueryBuilderAI();
const analyticsArea = useAnalyticsArea();
const organization = useOrganization();
diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.spec.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.spec.tsx
index de70de119131f0..6d6b2a89521e41 100644
--- a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.spec.tsx
+++ b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox.spec.tsx
@@ -7,7 +7,7 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar
import {AskSeerComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import {fetchMutation} from 'sentry/utils/queryClient';
@@ -116,7 +116,7 @@ describe('AskSeerComboBox', () => {
it('closes seer search when close button is clicked', async () => {
function TestComponent() {
- const {displayAskSeer, setDisplayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer, setDisplayAskSeer} = useSearchQueryBuilderAI();
return displayAskSeer ? (
({
autoSubmitSeer,
setAutoSubmitSeer,
enableAISearch,
- } = useSearchQueryBuilder();
+ } = useSearchQueryBuilderAI();
const analyticsArea = useAnalyticsArea();
diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox.tsx
index 362f04cd0967fd..a27df53b420ebc 100644
--- a/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox.tsx
+++ b/static/app/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox.tsx
@@ -28,7 +28,7 @@ import {
generateQueryTokensString,
isNoneOfTheseItem,
} from 'sentry/components/searchQueryBuilder/askSeerCombobox/utils';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderAI} from 'sentry/components/searchQueryBuilder/context';
import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/tokens/useSearchTokenCombobox';
import {IconClose, IconMegaphone, IconSearch} from 'sentry/icons';
import {t} from 'sentry/locale';
@@ -139,7 +139,7 @@ export function AskSeerPollingComboBox({
autoSubmitSeer,
setAutoSubmitSeer,
enableAISearch,
- } = useSearchQueryBuilder();
+ } = useSearchQueryBuilderAI();
const analyticsArea = useAnalyticsArea();
diff --git a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx
index c811b3ef5a7f48..c6dfc69b16c1c6 100644
--- a/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx
+++ b/static/app/components/searchQueryBuilder/askSeerCombobox/queryTokens.tsx
@@ -4,7 +4,7 @@ import {Flex} from '@sentry/scraps/layout';
import type {QueryTokensProps} from 'sentry/components/searchQueryBuilder/askSeerCombobox/types';
import {formatDateRange} from 'sentry/components/searchQueryBuilder/askSeerCombobox/utils';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
import {ProvidedFormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
import {t} from 'sentry/locale';
@@ -19,7 +19,7 @@ export function QueryTokens({
visualizations,
}: QueryTokensProps) {
const tokens = [];
- const {getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
const parsedQuery = query ? parseQueryBuilderValue(query, getFieldDefinition) : null;
if (query && parsedQuery?.length) {
tokens.push(
diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx
index 07a6373cc2fe57..7a5c88e6bc4fda 100644
--- a/static/app/components/searchQueryBuilder/context.tsx
+++ b/static/app/components/searchQueryBuilder/context.tsx
@@ -35,72 +35,120 @@ import {useDimensions} from 'sentry/utils/useDimensions';
import {useOrganization} from 'sentry/utils/useOrganization';
import {usePrevious} from 'sentry/utils/usePrevious';
-interface SearchQueryBuilderContextData {
- actionBarRef: React.RefObject;
- aiSearchBadgeType: 'alpha' | 'beta';
- askSeerNLQueryRef: React.RefObject;
- askSeerSuggestedQueryRef: React.RefObject;
- autoSubmitSeer: boolean;
+interface SearchQueryBuilderStateContextData {
clearSearchQuery: (options?: {reopenDropdown?: boolean}) => void;
committedQuery: string;
- consumeReopenDropdownOnQueryClear: () => void;
- currentInputValueRef: React.RefObject;
+ dispatch: Dispatch;
+ focusOverride: FocusOverride | null;
+ handleSearch: (query: string) => void;
+ parseQuery: (query: string) => ParseResult | null;
+ parsedQuery: ParseResult | null;
+ query: string;
+}
+
+interface SearchQueryBuilderConfigContextData {
+ caseInsensitive: CaseInsensitive | undefined;
disabled: boolean;
disallowFreeText: boolean;
disallowLogicalOperators: boolean;
disallowWildcard: boolean;
- dispatch: Dispatch;
- displayAskSeer: boolean;
- displayAskSeerFeedback: boolean;
- enableAISearch: boolean;
- filterKeyMenuWidth: number;
+ filterKeyAliases: TagCollection | undefined;
filterKeySections: FilterKeySection[];
filterKeys: TagCollection;
- focusOverride: FocusOverride | null;
- // @deprecated: remove this, it's constant now
- gaveSeerConsent: true;
getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null;
getSuggestedFilterKey: (key: string) => string | null;
+ getTagKeys: GetTagKeys | undefined;
getTagValues: GetTagValues;
- handleSearch: (query: string) => void;
invalidFilterKeys: string[];
- parseQuery: (query: string) => ParseResult | null;
- parsedQuery: ParseResult | null;
- query: string;
- reopenDropdownOnQueryClear: boolean;
+ matchKeySuggestions: Array<{key: string; valuePattern: RegExp}> | undefined;
+ namespace: string | undefined;
+ onCaseInsensitiveClick: ((value: CaseInsensitive) => void) | undefined;
+ placeholder: string | undefined;
+ recentSearches: SavedSearchType | undefined;
+ replaceRawSearchKeys: string[] | undefined;
searchSource: string;
+}
+
+interface SearchQueryBuilderLayoutContextData {
+ actionBarRef: React.RefObject;
+ currentInputValueRef: React.RefObject;
+ filterKeyMenuWidth: number;
+ portalTarget: HTMLElement | null | undefined;
+ size: 'small' | 'normal';
+ wrapperRef: React.RefObject;
+}
+
+interface SearchQueryBuilderAIContextData {
+ aiSearchBadgeType: 'alpha' | 'beta';
+ askSeerNLQueryRef: React.RefObject;
+ askSeerSuggestedQueryRef: React.RefObject;
+ autoSubmitSeer: boolean;
+ displayAskSeer: boolean;
+ displayAskSeerFeedback: boolean;
+ enableAISearch: boolean;
setAutoSubmitSeer: (enabled: boolean) => void;
setDisplayAskSeer: (enabled: boolean) => void;
setDisplayAskSeerFeedback: (enabled: boolean) => void;
- size: 'small' | 'normal';
- wrapperRef: React.RefObject;
- caseInsensitive?: CaseInsensitive;
- filterKeyAliases?: TagCollection;
- getTagKeys?: GetTagKeys;
- matchKeySuggestions?: Array<{key: string; valuePattern: RegExp}>;
- namespace?: string;
- onCaseInsensitiveClick?: (value: CaseInsensitive) => void;
- placeholder?: string;
- /**
- * The element to render the combobox popovers into.
- */
- portalTarget?: HTMLElement | null;
- recentSearches?: SavedSearchType;
- replaceRawSearchKeys?: string[];
}
-export function useSearchQueryBuilder() {
- const context = useContext(SearchQueryBuilderContext);
- if (!context) {
- throw new Error(
- 'useSearchQueryBuilder must be used within a SearchQueryBuilderProvider'
- );
+interface SearchQueryBuilderInteractionContextData {
+ consumeReopenDropdownOnQueryClear: () => void;
+ reopenDropdownOnQueryClear: boolean;
+}
+
+function useRequiredContext(context: React.Context, hookName: string) {
+ const contextValue = useContext(context);
+ if (!contextValue) {
+ throw new Error(`${hookName} must be used within a SearchQueryBuilderProvider`);
}
- return context;
+ return contextValue;
+}
+
+export function useSearchQueryBuilderState() {
+ return useRequiredContext(SearchQueryBuilderStateContext, 'useSearchQueryBuilderState');
+}
+
+export function useSearchQueryBuilderConfig() {
+ return useRequiredContext(
+ SearchQueryBuilderConfigContext,
+ 'useSearchQueryBuilderConfig'
+ );
+}
+
+export function useSearchQueryBuilderLayout() {
+ return useRequiredContext(
+ SearchQueryBuilderLayoutContext,
+ 'useSearchQueryBuilderLayout'
+ );
+}
+
+export function useSearchQueryBuilderAI() {
+ return useRequiredContext(SearchQueryBuilderAIContext, 'useSearchQueryBuilderAI');
}
-export const SearchQueryBuilderContext =
- createContext(null);
+export function useSearchQueryBuilderInteraction() {
+ return useRequiredContext(
+ SearchQueryBuilderInteractionContext,
+ 'useSearchQueryBuilderInteraction'
+ );
+}
+
+export function useHasSearchQueryBuilderProvider() {
+ return useContext(SearchQueryBuilderProviderContext);
+}
+
+const SearchQueryBuilderStateContext =
+ createContext(null);
+const SearchQueryBuilderConfigContext =
+ createContext(null);
+const SearchQueryBuilderLayoutContext =
+ createContext(null);
+const SearchQueryBuilderAIContext = createContext(
+ null
+);
+const SearchQueryBuilderInteractionContext =
+ createContext(null);
+const SearchQueryBuilderProviderContext = createContext(false);
export function SearchQueryBuilderProvider({
children,
@@ -267,96 +315,136 @@ export function SearchQueryBuilderProvider({
const size =
searchBarWidth && searchBarWidth < 600 ? ('small' as const) : ('normal' as const);
- const contextValue = useMemo((): SearchQueryBuilderContextData => {
+ const stateValue = useMemo((): SearchQueryBuilderStateContextData => {
return {
- ...state,
- aiSearchBadgeType,
- invalidFilterKeys: stableInvalidFilterKeys,
+ clearSearchQuery,
+ committedQuery: state.committedQuery,
+ dispatch,
+ focusOverride: state.focusOverride,
+ handleSearch,
+ parseQuery,
+ parsedQuery,
+ query: state.query,
+ };
+ }, [
+ clearSearchQuery,
+ dispatch,
+ handleSearch,
+ parseQuery,
+ parsedQuery,
+ state.committedQuery,
+ state.focusOverride,
+ state.query,
+ ]);
+
+ const configValue = useMemo((): SearchQueryBuilderConfigContextData => {
+ return {
+ caseInsensitive,
disabled,
disallowFreeText: Boolean(disallowFreeText),
disallowLogicalOperators: Boolean(disallowLogicalOperators),
disallowWildcard: Boolean(disallowWildcard),
- enableAISearch,
- parseQuery,
- parsedQuery,
+ filterKeyAliases,
filterKeySections: filterKeySections ?? [],
- filterKeyMenuWidth,
filterKeys: stableFilterKeys,
+ getFieldDefinition: stableFieldDefinitionGetter,
getSuggestedFilterKey: stableGetSuggestedFilterKey,
- getTagValues,
getTagKeys,
- getFieldDefinition: stableFieldDefinitionGetter,
- dispatch,
- clearSearchQuery,
- consumeReopenDropdownOnQueryClear,
- wrapperRef,
- actionBarRef,
- handleSearch,
+ getTagValues,
+ invalidFilterKeys: stableInvalidFilterKeys,
+ matchKeySuggestions,
+ namespace,
+ onCaseInsensitiveClick,
placeholder,
recentSearches,
- namespace,
- searchSource,
- size,
- portalTarget,
- autoSubmitSeer,
- setAutoSubmitSeer,
- displayAskSeer,
- setDisplayAskSeer: setDisplayAskSeerState,
replaceRawSearchKeys,
- matchKeySuggestions,
- filterKeyAliases,
- gaveSeerConsent: true,
- currentInputValueRef,
- reopenDropdownOnQueryClear,
- displayAskSeerFeedback,
- setDisplayAskSeerFeedback,
- askSeerNLQueryRef,
- askSeerSuggestedQueryRef,
- caseInsensitive,
- onCaseInsensitiveClick,
+ searchSource,
};
}, [
- aiSearchBadgeType,
- autoSubmitSeer,
caseInsensitive,
- clearSearchQuery,
- consumeReopenDropdownOnQueryClear,
disabled,
disallowFreeText,
disallowLogicalOperators,
disallowWildcard,
- dispatch,
- displayAskSeer,
- displayAskSeerFeedback,
- enableAISearch,
filterKeyAliases,
- filterKeyMenuWidth,
filterKeySections,
getTagKeys,
getTagValues,
- handleSearch,
stableInvalidFilterKeys,
matchKeySuggestions,
+ namespace,
onCaseInsensitiveClick,
- parseQuery,
- parsedQuery,
placeholder,
- portalTarget,
recentSearches,
- reopenDropdownOnQueryClear,
- namespace,
replaceRawSearchKeys,
searchSource,
- size,
stableFieldDefinitionGetter,
stableFilterKeys,
stableGetSuggestedFilterKey,
- state,
]);
+ const layoutValue = useMemo((): SearchQueryBuilderLayoutContextData => {
+ return {
+ actionBarRef,
+ currentInputValueRef,
+ filterKeyMenuWidth,
+ portalTarget,
+ size,
+ wrapperRef,
+ };
+ }, [
+ actionBarRef,
+ currentInputValueRef,
+ filterKeyMenuWidth,
+ portalTarget,
+ size,
+ wrapperRef,
+ ]);
+
+ const aiValue = useMemo((): SearchQueryBuilderAIContextData => {
+ return {
+ aiSearchBadgeType,
+ askSeerNLQueryRef,
+ askSeerSuggestedQueryRef,
+ autoSubmitSeer,
+ displayAskSeer,
+ displayAskSeerFeedback,
+ enableAISearch,
+ setAutoSubmitSeer,
+ setDisplayAskSeer: setDisplayAskSeerState,
+ setDisplayAskSeerFeedback,
+ };
+ }, [
+ aiSearchBadgeType,
+ askSeerNLQueryRef,
+ askSeerSuggestedQueryRef,
+ autoSubmitSeer,
+ displayAskSeer,
+ displayAskSeerFeedback,
+ enableAISearch,
+ setDisplayAskSeerFeedback,
+ ]);
+
+ const interactionValue = useMemo((): SearchQueryBuilderInteractionContextData => {
+ return {
+ consumeReopenDropdownOnQueryClear,
+ reopenDropdownOnQueryClear,
+ };
+ }, [consumeReopenDropdownOnQueryClear, reopenDropdownOnQueryClear]);
+
return (
-
- {children}
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
);
}
diff --git a/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx b/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx
index 355ab1dd934468..d15a41aaa4343f 100644
--- a/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx
+++ b/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx
@@ -15,10 +15,12 @@ const FILTER_KEYS: TagCollection = {
};
jest.mock('sentry/components/searchQueryBuilder/context', () => ({
- useSearchQueryBuilder: () => ({
- size: 'normal',
+ useSearchQueryBuilderConfig: () => ({
getFieldDefinition: () => null,
}),
+ useSearchQueryBuilderLayout: () => ({
+ size: 'normal',
+ }),
}));
describe('FormattedQuery', () => {
diff --git a/static/app/components/searchQueryBuilder/formattedQuery.tsx b/static/app/components/searchQueryBuilder/formattedQuery.tsx
index 0b6361b4a82066..c4c736a134dbf6 100644
--- a/static/app/components/searchQueryBuilder/formattedQuery.tsx
+++ b/static/app/components/searchQueryBuilder/formattedQuery.tsx
@@ -5,7 +5,7 @@ import {Text} from '@sentry/scraps/text';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderConfig,
} from 'sentry/components/searchQueryBuilder/context';
import {AggregateKeyVisual} from 'sentry/components/searchQueryBuilder/tokens/filter/aggregateKey';
import {FilterValueText} from 'sentry/components/searchQueryBuilder/tokens/filter/filter';
@@ -54,7 +54,7 @@ function FilterKey({token}: {token: TokenResult}) {
}
function Filter({token}: {token: TokenResult}) {
- const {getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
const label = useMemo(
() =>
getOperatorInfo({
diff --git a/static/app/components/searchQueryBuilder/hooks/useOnChange.tsx b/static/app/components/searchQueryBuilder/hooks/useOnChange.tsx
index 9e6d9889847db3..4a513bcf37888a 100644
--- a/static/app/components/searchQueryBuilder/hooks/useOnChange.tsx
+++ b/static/app/components/searchQueryBuilder/hooks/useOnChange.tsx
@@ -1,11 +1,11 @@
import type {SearchQueryBuilderProps} from 'sentry/components/searchQueryBuilder';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderState} from 'sentry/components/searchQueryBuilder/context';
import {queryIsValid} from 'sentry/components/searchQueryBuilder/utils';
import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender';
import {usePrevious} from 'sentry/utils/usePrevious';
export function useOnChange({onChange}: Pick) {
- const {committedQuery, handleSearch, parseQuery} = useSearchQueryBuilder();
+ const {committedQuery, handleSearch, parseQuery} = useSearchQueryBuilderState();
const previousCommittedQuery = usePrevious(committedQuery);
diff --git a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderGridItem.tsx b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderGridItem.tsx
index 69cfc0fdf52b00..6910465a9db716 100644
--- a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderGridItem.tsx
+++ b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderGridItem.tsx
@@ -5,7 +5,7 @@ import {isMac} from '@react-aria/utils';
import type {ListState} from '@react-stately/list';
import type {FocusableElement, Node} from '@react-types/shared';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderLayout} from 'sentry/components/searchQueryBuilder/context';
import {useKeyboardSelection} from 'sentry/components/searchQueryBuilder/hooks/useKeyboardSelection';
import {findNearestFreeTextKey} from 'sentry/components/searchQueryBuilder/utils';
import type {ParseResultToken} from 'sentry/components/searchSyntax/parser';
@@ -94,7 +94,7 @@ export function useQueryBuilderGridItem(
state: ListState,
ref: RefObject
) {
- const {wrapperRef} = useSearchQueryBuilder();
+ const {wrapperRef} = useSearchQueryBuilderLayout();
const {rowProps, gridCellProps} = useGridListItem({node: item}, state, ref);
const {selectInDirection} = useKeyboardSelection();
diff --git a/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx b/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
index 05c0416a7a25ac..84a65d33cb4d35 100644
--- a/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
+++ b/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
@@ -2,7 +2,7 @@ import {useCallback, useEffect, useRef} from 'react';
import type {ListState} from '@react-stately/list';
import type {Key} from '@react-types/shared';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderLayout} from 'sentry/components/searchQueryBuilder/context';
import {Token, type ParseResultToken} from 'sentry/components/searchSyntax/parser';
type DraggingState = {
@@ -118,7 +118,7 @@ function getItemIndexAtPosition(
* and should behave similarly to selection within a textarea.
*/
export function useSelectOnDrag(state: ListState) {
- const {wrapperRef} = useSearchQueryBuilder();
+ const {wrapperRef} = useSearchQueryBuilderLayout();
const dragState = useRef(null);
const cachedTokenCoordinates = useRef(null);
// Mouse move events fire more than once per frame, so we use this ref to
diff --git a/static/app/components/searchQueryBuilder/hooks/useUndoStack.tsx b/static/app/components/searchQueryBuilder/hooks/useUndoStack.tsx
index 388edd7c3c77bc..2c7db1f1a95e0b 100644
--- a/static/app/components/searchQueryBuilder/hooks/useUndoStack.tsx
+++ b/static/app/components/searchQueryBuilder/hooks/useUndoStack.tsx
@@ -2,7 +2,7 @@ import {useCallback, useRef} from 'react';
import type {ListState} from '@react-stately/list';
import type {Key} from '@react-types/shared';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderState} from 'sentry/components/searchQueryBuilder/context';
import type {FocusOverride} from 'sentry/components/searchQueryBuilder/types';
import type {ParseResultToken} from 'sentry/components/searchSyntax/parser';
import {defined} from 'sentry/utils';
@@ -77,7 +77,7 @@ function updateUndoStack({
* Hook that manages the undo stack for the search query builder.
*/
export function useUndoStack(state: ListState) {
- const {query, focusOverride, dispatch} = useSearchQueryBuilder();
+ const {query, focusOverride, dispatch} = useSearchQueryBuilderState();
const undoStackRef = useRef([]);
const trimmedQuery = query.trim();
diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx
index ffdcb054166301..26e065245eb2f8 100644
--- a/static/app/components/searchQueryBuilder/index.spec.tsx
+++ b/static/app/components/searchQueryBuilder/index.spec.tsx
@@ -19,7 +19,8 @@ import {
import {AskSeerComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderState,
} from 'sentry/components/searchQueryBuilder/context';
import {
QueryInterfaceType,
@@ -5807,7 +5808,8 @@ describe('SearchQueryBuilder', () => {
});
function AskSeerTestComponent({children}: {children: React.ReactNode}) {
- const {displayAskSeer, query} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
+ const {query} = useSearchQueryBuilderState();
return displayAskSeer ? (
{
story('SearchQueryBuilderProvider', () => {
function OpenDropdownButton() {
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
return (
- The provider will give you access to the context values within the search bar.
- Access these values using the useSearchQueryBuilder hook within any
- of the provider's child components.
+ The provider will give you access to focused context values within the search
+ bar. Access query state using the useSearchQueryBuilderState hook
+ within any of the provider's child components.
Here is an example of a custom component that uses the provider. In this
@@ -899,7 +899,7 @@ export default Storybook.story('SearchQueryBuilder', story => {
{`
function OpenDropdownButton() {
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
const handleClick = () => {
dispatch({
diff --git a/static/app/components/searchQueryBuilder/index.tsx b/static/app/components/searchQueryBuilder/index.tsx
index dad371549b22d7..5474636b470765 100644
--- a/static/app/components/searchQueryBuilder/index.tsx
+++ b/static/app/components/searchQueryBuilder/index.tsx
@@ -1,4 +1,4 @@
-import {useContext, useLayoutEffect} from 'react';
+import {useLayoutEffect} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
@@ -7,9 +7,11 @@ import {Input} from '@sentry/scraps/input';
import {Tooltip} from '@sentry/scraps/tooltip';
import {
- SearchQueryBuilderContext,
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useHasSearchQueryBuilderProvider,
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
} from 'sentry/components/searchQueryBuilder/context';
import type {CaseInsensitive} from 'sentry/components/searchQueryBuilder/hooks';
import {useOnChange} from 'sentry/components/searchQueryBuilder/hooks/useOnChange';
@@ -217,8 +219,9 @@ function ActionButtons({
ref?: React.Ref;
trailingItems?: React.ReactNode;
}) {
- const {clearSearchQuery, disabled, query, caseInsensitive, onCaseInsensitiveClick} =
- useSearchQueryBuilder();
+ const {clearSearchQuery, query} = useSearchQueryBuilderState();
+ const {disabled, caseInsensitive, onCaseInsensitiveClick} =
+ useSearchQueryBuilderConfig();
if (disabled) {
return null;
@@ -269,8 +272,8 @@ function SearchQueryBuilderUI({
trailingItems,
onChange,
}: SearchQueryBuilderProps) {
- const {parsedQuery, query, dispatch, wrapperRef, actionBarRef, size} =
- useSearchQueryBuilder();
+ const {parsedQuery, query, dispatch} = useSearchQueryBuilderState();
+ const {wrapperRef, actionBarRef, size} = useSearchQueryBuilderLayout();
useOnChange({onChange});
useLayoutEffect(() => {
@@ -311,9 +314,9 @@ function SearchQueryBuilderUI({
}
export function SearchQueryBuilder({...props}: SearchQueryBuilderProps) {
- const contextValue = useContext(SearchQueryBuilderContext);
+ const hasProvider = useHasSearchQueryBuilderProvider();
- if (contextValue) {
+ if (hasProvider) {
return ;
}
return (
diff --git a/static/app/components/searchQueryBuilder/plainTextQueryInput.tsx b/static/app/components/searchQueryBuilder/plainTextQueryInput.tsx
index a597540787b931..c14133d4cc767d 100644
--- a/static/app/components/searchQueryBuilder/plainTextQueryInput.tsx
+++ b/static/app/components/searchQueryBuilder/plainTextQueryInput.tsx
@@ -8,7 +8,11 @@ import {
} from 'react';
import styled from '@emotion/styled';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {HighlightQuery} from 'sentry/components/searchSyntax/renderer';
interface PlainTextQueryInputProps {
@@ -17,8 +21,9 @@ interface PlainTextQueryInputProps {
export function PlainTextQueryInput({label}: PlainTextQueryInputProps) {
const inputRef = useRef(null);
- const {query, parsedQuery, dispatch, handleSearch, size, placeholder, disabled} =
- useSearchQueryBuilder();
+ const {query, parsedQuery, dispatch, handleSearch} = useSearchQueryBuilderState();
+ const {placeholder, disabled} = useSearchQueryBuilderConfig();
+ const {size} = useSearchQueryBuilderLayout();
const [cursorPosition, setCursorPosition] = useState(0);
const setCursorPositionOnEvent = (event: SyntheticEvent) => {
diff --git a/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx b/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx
index 89d24b09f2ab38..0fd75ea93e90f3 100644
--- a/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx
+++ b/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx
@@ -1,7 +1,11 @@
import {VisuallyHidden} from '@react-aria/visually-hidden';
import type {ListState} from '@react-stately/list';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useKeyboardSelection} from 'sentry/components/searchQueryBuilder/hooks/useKeyboardSelection';
import {findNearestFreeTextKey} from 'sentry/components/searchQueryBuilder/utils';
import type {ParseResultToken} from 'sentry/components/searchSyntax/parser';
@@ -29,7 +33,9 @@ export function SelectionKeyHandler({
undo,
gridRef,
}: SelectionKeyHandlerProps) {
- const {dispatch, disabled, currentInputValueRef} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
const {selectInDirection} = useKeyboardSelection();
const selectedTokens = [...state.collection.getKeys()]
diff --git a/static/app/components/searchQueryBuilder/tokenizedQueryGrid.tsx b/static/app/components/searchQueryBuilder/tokenizedQueryGrid.tsx
index d33a325d87a263..fae7113840dfea 100644
--- a/static/app/components/searchQueryBuilder/tokenizedQueryGrid.tsx
+++ b/static/app/components/searchQueryBuilder/tokenizedQueryGrid.tsx
@@ -6,7 +6,10 @@ import type {ListState} from '@react-stately/list';
import {useListState} from '@react-stately/list';
import type {CollectionChildren} from '@react-types/shared';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {KeyboardSelection} from 'sentry/components/searchQueryBuilder/hooks/useKeyboardSelection';
import {useQueryBuilderGrid} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGrid';
import {useSelectOnDrag} from 'sentry/components/searchQueryBuilder/hooks/useSelectOnDrag';
@@ -34,7 +37,7 @@ interface GridProps extends AriaGridListOptions {
}
function useAutoFocus(autoFocus: boolean, state: ListState) {
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
const autoFocused = useRef(!autoFocus);
useEffect(() => {
@@ -49,7 +52,7 @@ function useAutoFocus(autoFocus: boolean, state: ListState) {
}
function useApplyFocusOverride(state: ListState) {
- const {focusOverride, dispatch} = useSearchQueryBuilder();
+ const {focusOverride, dispatch} = useSearchQueryBuilderState();
useLayoutEffect(() => {
if (focusOverride && !focusOverride.part) {
@@ -68,7 +71,8 @@ function useApplyFocusOverride(state: ListState) {
function Grid(props: GridProps) {
const ref = useRef(null);
const selectionKeyHandlerRef = useRef(null);
- const {size, dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {size} = useSearchQueryBuilderLayout();
const state = useListState({
...props,
selectionBehavior: 'replace',
@@ -168,7 +172,7 @@ export function TokenizedQueryGrid({
label,
actionBarWidth,
}: TokenizedQueryGridProps) {
- const {parsedQuery} = useSearchQueryBuilder();
+ const {parsedQuery} = useSearchQueryBuilderState();
// Shouldn't ever get here since we will render the plain text input instead
if (!parsedQuery) {
diff --git a/static/app/components/searchQueryBuilder/tokens/boolean.tsx b/static/app/components/searchQueryBuilder/tokens/boolean.tsx
index 5cc8eeea18d6ea..9de3c11b9378ec 100644
--- a/static/app/components/searchQueryBuilder/tokens/boolean.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/boolean.tsx
@@ -9,7 +9,10 @@ import type {Node} from '@react-types/shared';
import {CompactSelect} from '@sentry/scraps/compactSelect';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGridItem';
import {
BaseGridCell,
@@ -34,7 +37,8 @@ type SearchQueryBuilderBooleanProps = {
};
function FilterDelete({token, state, item}: SearchQueryBuilderBooleanProps) {
- const {dispatch, disabled} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
const filterButtonProps = useFilterButtonProps({state, item});
return (
@@ -64,7 +68,8 @@ export function SearchQueryBuilderBoolean({
}: SearchQueryBuilderBooleanProps) {
const ref = useRef(null);
const [filterMenuOpen, setFilterMenuOpen] = useState(false);
- const {disabled, dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
const {rowProps, gridCellProps} = useQueryBuilderGridItem(item, state, ref);
const {focusWithinProps} = useFocusWithin({});
const filterButtonProps = useFilterButtonProps({state, item});
diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx
index 5d343c5edef841..18ba6f4a63ad1b 100644
--- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx
@@ -30,7 +30,12 @@ import {Overlay} from 'sentry/components/overlay';
import {AskSeer} from 'sentry/components/searchQueryBuilder/askSeer/askSeer';
import {ASK_SEER_CONSENT_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerConsentOption';
import {ASK_SEER_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerOption';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/tokens/useSearchTokenCombobox';
import {
findItemInSections,
@@ -295,7 +300,7 @@ function OverlayContent>({
isLoading?: boolean;
portalTarget?: HTMLElement | null;
}) {
- const {enableAISearch} = useSearchQueryBuilder();
+ const {enableAISearch} = useSearchQueryBuilderAI();
const anyItemsShowing = totalOptions > hiddenOptions.size;
if (customMenu) {
@@ -381,8 +386,10 @@ export function SearchQueryBuilderCombobox<
['data-test-id']: dataTestId,
ref,
}: SearchQueryBuilderComboboxProps) {
- const {clearSearchQuery, disabled, portalTarget, enableAISearch, wrapperRef} =
- useSearchQueryBuilder();
+ const {clearSearchQuery} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
+ const {portalTarget, wrapperRef} = useSearchQueryBuilderLayout();
+ const {enableAISearch} = useSearchQueryBuilderAI();
const listBoxRef = useRef(null);
const inputRef = useRef(null);
const popoverRef = useRef(null);
diff --git a/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx b/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx
index 53397a75fbffe6..8a4a804ed4af1a 100644
--- a/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/deletableToken.tsx
@@ -7,7 +7,7 @@ import type {Node} from '@react-types/shared';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderState} from 'sentry/components/searchQueryBuilder/context';
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGridItem';
import {InvalidTokenTooltip} from 'sentry/components/searchQueryBuilder/tokens/invalidTokenTooltip';
import {
@@ -39,7 +39,7 @@ export function DeletableToken({
invalid,
}: DeletableTokenProps) {
const ref = useRef(null);
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
const {rowProps, gridCellProps} = useQueryBuilderGridItem(item, state, ref);
const {shiftFocusProps} = useShiftFocusToChild(item, state);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx b/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx
index 3176fe4507a69b..bd86bb945acd7b 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/aggregateKey.tsx
@@ -7,7 +7,10 @@ import type {Node} from '@react-types/shared';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {SearchQueryBuilderParametersCombobox} from 'sentry/components/searchQueryBuilder/tokens/filter/parametersCombobox';
import {UnstyledButton} from 'sentry/components/searchQueryBuilder/tokens/filter/unstyledButton';
import {useAggregateParamVisual} from 'sentry/components/searchQueryBuilder/tokens/filter/useAggregateParamVisual';
@@ -49,7 +52,8 @@ export function AggregateKey({
filterRef,
}: AggregateKeyProps) {
const ref = useRef(null);
- const {dispatch, focusOverride, disabled} = useSearchQueryBuilder();
+ const {dispatch, focusOverride} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
const [isEditing, setIsEditing] = useState(false);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
index 7b8d2976cca52a..44319a016fd38a 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
@@ -9,7 +9,11 @@ import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
import {Flex} from '@sentry/scraps/layout';
import {DateTime} from 'sentry/components/dateTime';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGridItem';
import {
BaseGridCell,
@@ -51,7 +55,8 @@ interface FilterValueProps extends SearchQueryTokenProps {
}
export function FilterValueText({token}: {token: TokenResult}) {
- const {size, getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
+ const {size} = useSearchQueryBuilderLayout();
const valueType = getFilterValueType(token, getFieldDefinition(getKeyName(token.key)));
if (token.filter === FilterType.HAS) {
@@ -113,7 +118,8 @@ export function FilterValueText({token}: {token: TokenResult}) {
function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValueProps) {
const ref = useRef(null);
- const {dispatch, focusOverride, disabled} = useSearchQueryBuilder();
+ const {dispatch, focusOverride} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
const [isEditing, setIsEditing] = useState(false);
@@ -181,7 +187,8 @@ function FilterValue({token, state, item, filterRef, onActiveChange}: FilterValu
}
function FilterDelete({token, state, item}: SearchQueryTokenProps) {
- const {dispatch, disabled} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {disabled} = useSearchQueryBuilderConfig();
const filterButtonProps = useFilterButtonProps({state, item});
return (
@@ -205,7 +212,8 @@ export function SearchQueryBuilderFilter({item, state, token}: SearchQueryTokenP
const isFocused = item.key === state.selectionManager.focusedKey;
- const {dispatch, invalidFilterKeys} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {invalidFilterKeys} = useSearchQueryBuilderConfig();
const {rowProps, gridCellProps} = useQueryBuilderGridItem(item, state, ref);
const onKeyDown = (e: React.KeyboardEvent) => {
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx
index b997d07ad2e8a7..a02dc262407827 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/filterKey.tsx
@@ -8,7 +8,7 @@ import type {Node} from '@react-types/shared';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
import {Tooltip} from '@sentry/scraps/tooltip';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
import {FilterKeyCombobox} from 'sentry/components/searchQueryBuilder/tokens/filter/filterKeyCombobox';
import {UnstyledButton} from 'sentry/components/searchQueryBuilder/tokens/filter/unstyledButton';
import {useFilterButtonProps} from 'sentry/components/searchQueryBuilder/tokens/filter/useFilterButtonProps';
@@ -30,7 +30,7 @@ type FilterKeyProps = {
export function FilterKey({item, state, token, onActiveChange}: FilterKeyProps) {
const ref = useRef(null);
- const {disabled, getFieldDefinition} = useSearchQueryBuilder();
+ const {disabled, getFieldDefinition} = useSearchQueryBuilderConfig();
const fieldDefinition = getFieldDefinition(token.key.text);
const [isEditing, setIsEditing] = useState(false);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
index ecb0f8c077f88f..28328e65586d30 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
@@ -8,7 +8,12 @@ import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {useSeerAcknowledgeMutation} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation';
import {ASK_SEER_CONSENT_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerConsentOption';
import {ASK_SEER_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerOption';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {SearchQueryBuilderCombobox} from 'sentry/components/searchQueryBuilder/tokens/combobox';
import {getFilterValueType} from 'sentry/components/searchQueryBuilder/tokens/filter/utils';
import type {SearchKeyItem} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/types';
@@ -42,14 +47,10 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
inputValue,
includeSuggestions: false,
});
- const {
- dispatch,
- getFieldDefinition,
- getSuggestedFilterKey,
- setDisplayAskSeer,
- currentInputValueRef,
- setAutoSubmitSeer,
- } = useSearchQueryBuilder();
+ const {getFieldDefinition, getSuggestedFilterKey} = useSearchQueryBuilderConfig();
+ const {dispatch} = useSearchQueryBuilderState();
+ const {setDisplayAskSeer, setAutoSubmitSeer} = useSearchQueryBuilderAI();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
const analyticsArea = useAnalyticsArea();
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx
index cd30b54cb9fbe2..9ebc3221bfc31a 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/filterOperator.tsx
@@ -11,7 +11,10 @@ import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
import {Flex} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {UnstyledButton} from 'sentry/components/searchQueryBuilder/tokens/filter/unstyledButton';
import {useFilterButtonProps} from 'sentry/components/searchQueryBuilder/tokens/filter/useFilterButtonProps';
import {
@@ -72,7 +75,7 @@ function FilterKeyOperatorLabel({
includeKeyLabel?: boolean;
opLabel?: string;
}) {
- const {getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
const fieldDefinition = getFieldDefinition(keyValue);
if (!includeKeyLabel) {
@@ -219,15 +222,9 @@ export function getOperatorInfo({
export function FilterOperator({state, item, token, onOpenChange}: FilterOperatorProps) {
const organization = useOrganization();
- const {
- dispatch,
- searchSource,
- query,
- recentSearches,
- disabled,
- focusOverride,
- getFieldDefinition,
- } = useSearchQueryBuilder();
+ const {dispatch, query, focusOverride} = useSearchQueryBuilderState();
+ const {searchSource, recentSearches, disabled, getFieldDefinition} =
+ useSearchQueryBuilderConfig();
const filterButtonProps = useFilterButtonProps({state, item});
const {focusWithinProps} = useFocusWithin({});
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/functionDescription.tsx b/static/app/components/searchQueryBuilder/tokens/filter/functionDescription.tsx
index 40716420973f3b..f9ed9b8adac861 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/functionDescription.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/functionDescription.tsx
@@ -1,7 +1,7 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
import type {AggregateFilter} from 'sentry/components/searchSyntax/parser';
import {getKeyName} from 'sentry/components/searchSyntax/utils';
import type {AggregateParameter} from 'sentry/utils/fields';
@@ -24,7 +24,7 @@ function getParameterLabel(param: AggregateParameter) {
}
export function FunctionDescription({token, parameterIndex}: FunctionDescriptionProps) {
- const {getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
const fnName = getKeyName(token.key);
const fieldDefinition = getFieldDefinition(fnName);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
index b4e6b0c5638014..3e4963dfd7ac1b 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
@@ -7,7 +7,10 @@ import type {KeyboardEvent} from '@react-types/shared';
import type {SelectOptionWithKey} from '@sentry/scraps/compactSelect';
import {getEscapedKey} from '@sentry/scraps/compactSelect';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {SearchQueryBuilderCombobox} from 'sentry/components/searchQueryBuilder/tokens/combobox';
import {FunctionDescription} from 'sentry/components/searchQueryBuilder/tokens/filter/functionDescription';
import {replaceCommaSeparatedValue} from 'sentry/components/searchQueryBuilder/tokens/filter/replaceCommaSeparatedValue';
@@ -160,7 +163,7 @@ function useParameterSuggestions({
parameterIndex: number;
token: AggregateFilter;
}): Array> {
- const {getFieldDefinition, filterKeys} = useSearchQueryBuilder();
+ const {getFieldDefinition, filterKeys} = useSearchQueryBuilderConfig();
const fieldDefinition = getFieldDefinition(token.key.name.text);
const parameterDefinition = fieldDefinition?.parameters?.[parameterIndex];
@@ -240,10 +243,10 @@ export function SearchQueryBuilderParametersCombobox({
onDelete,
onKeyDown: passedOnKeyDown,
}: ParametersComboboxProps) {
- const {getFieldDefinition, getSuggestedFilterKey} = useSearchQueryBuilder();
+ const {getFieldDefinition, getSuggestedFilterKey} = useSearchQueryBuilderConfig();
const inputRef = useRef(null);
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
const initialValue = getInitialInputValue(token);
const [inputValue, setInputValue] = useState('');
const [inputChanged, setInputChanged] = useState(false);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/useAggregateParamVisual.tsx b/static/app/components/searchQueryBuilder/tokens/filter/useAggregateParamVisual.tsx
index c9dc17d763a4aa..b09288b32ba0fa 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/useAggregateParamVisual.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/useAggregateParamVisual.tsx
@@ -1,6 +1,6 @@
import {useMemo} from 'react';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
import {getKeyLabel} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/utils';
import type {AggregateFilter} from 'sentry/components/searchSyntax/parser';
@@ -9,7 +9,7 @@ interface UseAggregateParamVisualOptions {
}
export function useAggregateParamVisual({token}: UseAggregateParamVisualOptions) {
- const {filterKeys, getFieldDefinition} = useSearchQueryBuilder();
+ const {filterKeys, getFieldDefinition} = useSearchQueryBuilderConfig();
return useMemo(() => {
const aggregateDefinition = getFieldDefinition(token.key.name.text);
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
index 660c19633d2cbc..000ce055dcffc1 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx
@@ -17,7 +17,11 @@ import {
} from 'sentry/components/searchBar/types';
import {ASK_SEER_CONSENT_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerConsentOption';
import {ASK_SEER_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerOption';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {HighlightText} from 'sentry/components/searchQueryBuilder/highlightText';
import {
SearchQueryBuilderCombobox,
@@ -360,7 +364,7 @@ function useFilterSuggestions({
token: TokenResult;
}) {
const keyName = getKeyName(token.key);
- const {getFieldDefinition, getTagValues, filterKeys} = useSearchQueryBuilder();
+ const {getFieldDefinition, getTagValues, filterKeys} = useSearchQueryBuilderConfig();
const key = filterKeys[keyName];
const fieldDefinition = getFieldDefinition(keyName);
const valueType = getFilterValueType(token, fieldDefinition);
@@ -516,7 +520,7 @@ function ItemCheckbox({
value: string;
}) {
const {ctrlKeyPressed, selectedValueMap, token} = useValueComboboxContext();
- const {dispatch} = useSearchQueryBuilder();
+ const {dispatch} = useSearchQueryBuilderState();
const selected = selectedValueMap.get(value) ?? false;
return (
@@ -618,16 +622,16 @@ export function SearchQueryBuilderValueCombobox({
const ref = useRef(null);
const inputRef = useRef(null);
const organization = useOrganization();
+ const {dispatch} = useSearchQueryBuilderState();
const {
getFieldDefinition,
getSuggestedFilterKey,
filterKeys,
- dispatch,
searchSource,
recentSearches,
disallowWildcard,
- wrapperRef: topLevelWrapperRef,
- } = useSearchQueryBuilder();
+ } = useSearchQueryBuilderConfig();
+ const {wrapperRef: topLevelWrapperRef} = useSearchQueryBuilderLayout();
const keyName = getKeyName(token.key);
const fieldDefinition = getFieldDefinition(keyName);
const canSelectMultipleValues = tokenSupportsMultipleValues(
diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx
index cb5303289fd434..4f171aa9b55ea3 100644
--- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx
@@ -16,7 +16,12 @@ import {Overlay} from 'sentry/components/overlay';
import {AskSeer} from 'sentry/components/searchQueryBuilder/askSeer/askSeer';
import {ASK_SEER_CONSENT_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerConsentOption';
import {ASK_SEER_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer/askSeerOption';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import type {CustomComboboxMenuProps} from 'sentry/components/searchQueryBuilder/tokens/combobox';
import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription';
import type {Section} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/types';
@@ -74,7 +79,7 @@ function ListBoxSectionButton({
}
function FeedbackFooter() {
- const {searchSource} = useSearchQueryBuilder();
+ const {searchSource} = useSearchQueryBuilderConfig();
return (
@@ -204,7 +209,8 @@ function FilterKeyMenuContent>({
fullWidth,
sections,
}: FilterKeyMenuContentProps) {
- const {filterKeys, enableAISearch} = useSearchQueryBuilder();
+ const {filterKeys} = useSearchQueryBuilderConfig();
+ const {enableAISearch} = useSearchQueryBuilderAI();
const focusedItem = state.selectionManager.focusedKey
? (state.collection.getItem(state.selectionManager.focusedKey)?.props?.value as
| string
@@ -290,8 +296,10 @@ export function FilterKeyListBox>
setSelectedSection,
overlayProps,
}: FilterKeyListBoxProps) {
- const {filterKeyMenuWidth, wrapperRef, query, portalTarget, enableAISearch, size} =
- useSearchQueryBuilder();
+ const {query} = useSearchQueryBuilderState();
+ const {filterKeyMenuWidth, wrapperRef, portalTarget, size} =
+ useSearchQueryBuilderLayout();
+ const {enableAISearch} = useSearchQueryBuilderAI();
const hiddenOptionsWithRecentsAndAskSeerAdded = useMemo>(() => {
const baseHidden = [
diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription.tsx
index a82cbda53ceaa8..a169a46b6373e7 100644
--- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
import {getKeyLabel} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/utils';
import {t} from 'sentry/locale';
import type {Tag} from 'sentry/types/group';
@@ -33,7 +33,7 @@ export function ValueType({
}
export function KeyDescription({size = 'sm', tag}: KeyDescriptionProps) {
- const {getFieldDefinition} = useSearchQueryBuilder();
+ const {getFieldDefinition} = useSearchQueryBuilderConfig();
const fieldDefinition = getFieldDefinition(tag.key);
diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx
index 16ea2ecff84757..216c27326dc9b2 100644
--- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx
@@ -5,7 +5,12 @@ import type {Node} from '@react-types/shared';
import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {useSeerAcknowledgeMutation} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import type {CustomComboboxMenu} from 'sentry/components/searchQueryBuilder/tokens/combobox';
import {FilterKeyListBox} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox';
import type {
@@ -91,7 +96,8 @@ function findNextMatchingItem(
}
function useFilterKeyItems() {
- const {filterKeySections, getFieldDefinition, filterKeys} = useSearchQueryBuilder();
+ const {filterKeySections, getFieldDefinition, filterKeys} =
+ useSearchQueryBuilderConfig();
const sectionedItems = useMemo(() => {
const flatFilterKeys = Object.keys(filterKeys);
@@ -134,7 +140,8 @@ function useFilterKeySections({
}: {
recentSearches: RecentSearch[] | undefined;
}) {
- const {filterKeySections, query, disallowLogicalOperators} = useSearchQueryBuilder();
+ const {query} = useSearchQueryBuilderState();
+ const {filterKeySections, disallowLogicalOperators} = useSearchQueryBuilderConfig();
const sections = useMemo(() => {
const definedSections = filterKeySections.map(section => ({
value: section.value,
@@ -192,15 +199,11 @@ interface UseFilterKeyListBoxArgs {
}
export function useFilterKeyListBox({filterValue}: UseFilterKeyListBoxArgs) {
- const {
- filterKeys,
- getFieldDefinition,
- setAutoSubmitSeer,
- setDisplayAskSeer,
- enableAISearch,
- currentInputValueRef,
- disallowLogicalOperators,
- } = useSearchQueryBuilder();
+ const {filterKeys, getFieldDefinition, disallowLogicalOperators} =
+ useSearchQueryBuilderConfig();
+ const {setAutoSubmitSeer, setDisplayAskSeer, enableAISearch} =
+ useSearchQueryBuilderAI();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
const analyticsArea = useAnalyticsArea();
const {sectionedItems} = useFilterKeyItems();
const recentFilters = useRecentSearchFilters();
diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearchFilters.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearchFilters.tsx
index 8058c9877253ca..3a91d8c22ba1bb 100644
--- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearchFilters.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearchFilters.tsx
@@ -1,6 +1,9 @@
import {useMemo} from 'react';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useRecentSearches} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearches';
import type {FieldDefinitionGetter} from 'sentry/components/searchQueryBuilder/types';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
@@ -117,8 +120,9 @@ function getFiltersFromRecentSearches(
* Orders by highest count of filter key occurrences.
*/
export function useRecentSearchFilters() {
- const {parsedQuery, filterKeys, getFieldDefinition, filterKeyAliases} =
- useSearchQueryBuilder();
+ const {parsedQuery} = useSearchQueryBuilderState();
+ const {filterKeys, getFieldDefinition, filterKeyAliases} =
+ useSearchQueryBuilderConfig();
const {data: recentSearchesData} = useRecentSearches();
const filters = useMemo(
diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearches.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearches.tsx
index ff0784dc3af13b..8074f276894549 100644
--- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearches.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useRecentSearches.tsx
@@ -1,8 +1,8 @@
import {useFetchRecentSearches} from 'sentry/actionCreators/savedSearches';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {useSearchQueryBuilderConfig} from 'sentry/components/searchQueryBuilder/context';
export function useRecentSearches() {
- const {recentSearches, namespace} = useSearchQueryBuilder();
+ const {recentSearches, namespace} = useSearchQueryBuilderConfig();
return useFetchRecentSearches(
{
diff --git a/static/app/components/searchQueryBuilder/tokens/freeText.tsx b/static/app/components/searchQueryBuilder/tokens/freeText.tsx
index dc85843fea7c51..89fe79782f8599 100644
--- a/static/app/components/searchQueryBuilder/tokens/freeText.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/freeText.tsx
@@ -5,7 +5,12 @@ import {Item, Section} from '@react-stately/collections';
import type {ListState} from '@react-stately/list';
import type {KeyboardEvent, Node} from '@react-types/shared';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderConfig,
+ useSearchQueryBuilderInteraction,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {useQueryBuilderGridItem} from 'sentry/components/searchQueryBuilder/hooks/useQueryBuilderGridItem';
import {SearchQueryBuilderCombobox} from 'sentry/components/searchQueryBuilder/tokens/combobox';
import {useFilterKeyListBox} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox';
@@ -260,20 +265,18 @@ function SearchQueryBuilderInputInternal({
const filterValue = getWordAtCursorPosition(inputValue, selectionIndex);
+ const {query, dispatch, handleSearch} = useSearchQueryBuilderState();
const {
- query,
filterKeys,
- dispatch,
getFieldDefinition,
getSuggestedFilterKey,
- handleSearch,
placeholder,
searchSource,
recentSearches,
- currentInputValueRef,
- consumeReopenDropdownOnQueryClear,
- reopenDropdownOnQueryClear,
- } = useSearchQueryBuilder();
+ } = useSearchQueryBuilderConfig();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {consumeReopenDropdownOnQueryClear, reopenDropdownOnQueryClear} =
+ useSearchQueryBuilderInteraction();
const resetInputValue = useCallback(() => {
setInputValue(trimmedTokenValue);
diff --git a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
index 739c0d0340a7b2..3847cb86dd2c50 100644
--- a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx
@@ -2,7 +2,10 @@ import {useMemo, type ReactNode} from 'react';
import {useQuery} from '@tanstack/react-query';
import type Fuse from 'fuse.js';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderConfig,
+} from 'sentry/components/searchQueryBuilder/context';
import type {
KeySectionItem,
SearchKeyItem,
@@ -171,9 +174,9 @@ export function useSortedFilterKeyItems({
disallowLogicalOperators,
replaceRawSearchKeys,
matchKeySuggestions,
- enableAISearch,
getTagKeys,
- } = useSearchQueryBuilder();
+ } = useSearchQueryBuilderConfig();
+ const {enableAISearch} = useSearchQueryBuilderAI();
// Async key fetching with debounce when getTagKeys is provided
const shouldFetchAsync = !!getTagKeys;
diff --git a/static/app/views/discover/results/issueListSeerComboBox.tsx b/static/app/views/discover/results/issueListSeerComboBox.tsx
index e7ab8a7009d64d..62f3a3515030d3 100644
--- a/static/app/views/discover/results/issueListSeerComboBox.tsx
+++ b/static/app/views/discover/results/issueListSeerComboBox.tsx
@@ -6,7 +6,11 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useAiQueryContext} from 'sentry/components/searchQueryBuilder/askSeerCombobox/aiQueryContext';
import {AskSeerPollingComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox';
import type {AskSeerSearchQuery} from 'sentry/components/searchQueryBuilder/askSeerCombobox/types';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {Token} from 'sentry/components/searchSyntax/parser';
import {stringifyToken} from 'sentry/components/searchSyntax/utils';
import {ConfigStore} from 'sentry/stores/configStore';
@@ -46,14 +50,9 @@ export function IssueListSeerComboBox({onSearch}: IssueListSeerComboBoxProps) {
const organization = useOrganization();
const analyticsArea = useAnalyticsArea();
const {setRunId} = useAiQueryContext();
- const {
- currentInputValueRef,
- query,
- committedQuery,
- askSeerSuggestedQueryRef,
- enableAISearch,
- parseQuery,
- } = useSearchQueryBuilder();
+ const {query, committedQuery, parseQuery} = useSearchQueryBuilderState();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {askSeerSuggestedQueryRef, enableAISearch} = useSearchQueryBuilderAI();
let initialSeerQuery = '';
const queryDetails = useMemo(() => {
diff --git a/static/app/views/discover/results/resultsSearchQueryBuilder.tsx b/static/app/views/discover/results/resultsSearchQueryBuilder.tsx
index c13dc5390a6b7f..689ff76368b8bf 100644
--- a/static/app/views/discover/results/resultsSearchQueryBuilder.tsx
+++ b/static/app/views/discover/results/resultsSearchQueryBuilder.tsx
@@ -20,7 +20,7 @@ import {
} from 'sentry/components/searchQueryBuilder';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import type {
CallbackSearchState,
@@ -109,7 +109,7 @@ function ErrorsSearchBar({
recentSearches,
searchSource,
}: ErrorsSearchBarProps) {
- const {displayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
if (displayAskSeer && onSearch) {
return ;
diff --git a/static/app/views/explore/components/schemaHints/schemaHintsList.spec.tsx b/static/app/views/explore/components/schemaHints/schemaHintsList.spec.tsx
index 19d616f352c55a..d6a494cbe70294 100644
--- a/static/app/views/explore/components/schemaHints/schemaHintsList.spec.tsx
+++ b/static/app/views/explore/components/schemaHints/schemaHintsList.spec.tsx
@@ -27,12 +27,13 @@ const mockBooleanTags: TagCollection = {
const mockDispatch = jest.fn();
-// Add mock for useSearchQueryBuilder
+// Add mock for search query builder contexts
jest.mock('sentry/components/searchQueryBuilder/context', () => ({
- useSearchQueryBuilder: () => ({
+ useSearchQueryBuilderState: () => ({
query: '',
- getTagValues: () => Promise.resolve(['tagValue1', 'tagValue2']),
dispatch: mockDispatch,
+ }),
+ useSearchQueryBuilderLayout: () => ({
wrapperRef: {current: null},
}),
SearchQueryBuilderProvider: ({children}: {children: React.ReactNode}) => children,
@@ -275,16 +276,14 @@ describe('SchemaHintsList', () => {
});
it('should remove hint from query when checkbox is unchecked on drawer', async () => {
- const mockUseSearchQueryBuilder = jest
+ const mockUseSearchQueryBuilderState = jest
.spyOn(
require('sentry/components/searchQueryBuilder/context'),
- 'useSearchQueryBuilder'
+ 'useSearchQueryBuilderState'
)
.mockImplementation(() => ({
query: '!stringTag1:"" numberTag1:>0',
- getTagValues: () => Promise.resolve(['tagValue1', 'tagValue2']),
dispatch: mockDispatch,
- wrapperRef: {current: null},
}));
render(
@@ -313,20 +312,18 @@ describe('SchemaHintsList', () => {
shouldCommitQuery: false,
});
- mockUseSearchQueryBuilder.mockRestore();
+ mockUseSearchQueryBuilderState.mockRestore();
});
it('should remove aggregate hint from query when checkbox is unchecked on drawer', async () => {
- const mockUseSearchQueryBuilder = jest
+ const mockUseSearchQueryBuilderState = jest
.spyOn(
require('sentry/components/searchQueryBuilder/context'),
- 'useSearchQueryBuilder'
+ 'useSearchQueryBuilderState'
)
.mockImplementation(() => ({
query: 'stringTag1:"" numberTag1:>0 count_unique(user):>0',
- getTagValues: () => Promise.resolve(['tagValue1', 'tagValue2']),
dispatch: mockDispatch,
- wrapperRef: {current: null},
}));
render(
@@ -355,7 +352,7 @@ describe('SchemaHintsList', () => {
shouldCommitQuery: false,
});
- mockUseSearchQueryBuilder.mockRestore();
+ mockUseSearchQueryBuilderState.mockRestore();
});
it('should keep drawer open when query is updated', async () => {
@@ -415,16 +412,14 @@ describe('SchemaHintsList', () => {
});
it('should set focus override propely on duplicate filters', async () => {
- const mockUseSearchQueryBuilder = jest
+ const mockUseSearchQueryBuilderState = jest
.spyOn(
require('sentry/components/searchQueryBuilder/context'),
- 'useSearchQueryBuilder'
+ 'useSearchQueryBuilderState'
)
.mockImplementation(() => ({
query: 'stringTag1:"something"',
- getTagValues: () => Promise.resolve(['tagValue1', 'tagValue2']),
dispatch: mockDispatch,
- wrapperRef: {current: null},
}));
render(
@@ -449,7 +444,7 @@ describe('SchemaHintsList', () => {
shouldCommitQuery: false,
});
- mockUseSearchQueryBuilder.mockRestore();
+ mockUseSearchQueryBuilderState.mockRestore();
});
it('should filter schema hints in bar but show all in drawer for logs source', async () => {
diff --git a/static/app/views/explore/components/schemaHints/schemaHintsList.tsx b/static/app/views/explore/components/schemaHints/schemaHintsList.tsx
index 9915146786f005..252f0e2fc9fb67 100644
--- a/static/app/views/explore/components/schemaHints/schemaHintsList.tsx
+++ b/static/app/views/explore/components/schemaHints/schemaHintsList.tsx
@@ -11,7 +11,10 @@ import {Text} from '@sentry/scraps/text';
import {getFunctionTags} from 'sentry/components/performance/spanSearchQueryBuilder';
import {Placeholder} from 'sentry/components/placeholder';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types';
import {t} from 'sentry/locale';
import type {Tag, TagCollection} from 'sentry/types/group';
@@ -171,7 +174,8 @@ export function SchemaHintsList({
const organization = useOrganization();
const {openDrawer, panelRef} = useDrawer();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
- const {dispatch, query, wrapperRef: searchBarWrapperRef} = useSearchQueryBuilder();
+ const {dispatch, query} = useSearchQueryBuilderState();
+ const {wrapperRef: searchBarWrapperRef} = useSearchQueryBuilderLayout();
// Create a ref to hold the latest query for the drawer
const queryRef = useRef(query);
diff --git a/static/app/views/explore/logs/logsTab.tsx b/static/app/views/explore/logs/logsTab.tsx
index 71e5c5b7b16d45..3f6507bba4d216 100644
--- a/static/app/views/explore/logs/logsTab.tsx
+++ b/static/app/views/explore/logs/logsTab.tsx
@@ -16,7 +16,7 @@ import {ProjectPageFilter} from 'sentry/components/pageFilters/project/projectPa
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import {IconChevron, IconEdit, IconRefresh} from 'sentry/icons';
import {t} from 'sentry/locale';
@@ -110,7 +110,7 @@ interface LogsSearchBarProps {
}
function LogsSearchBar({tracesItemSearchQueryBuilderProps}: LogsSearchBarProps) {
- const {displayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
if (displayAskSeer) {
return ;
diff --git a/static/app/views/explore/logs/logsTabSeerComboBox.tsx b/static/app/views/explore/logs/logsTabSeerComboBox.tsx
index 012b4e2f4f3039..a1e0772835b929 100644
--- a/static/app/views/explore/logs/logsTabSeerComboBox.tsx
+++ b/static/app/views/explore/logs/logsTabSeerComboBox.tsx
@@ -5,7 +5,11 @@ import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useAiQueryContext} from 'sentry/components/searchQueryBuilder/askSeerCombobox/aiQueryContext';
import {AskSeerPollingComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
import {Token} from 'sentry/components/searchSyntax/parser';
import {stringifyToken} from 'sentry/components/searchSyntax/utils';
@@ -58,13 +62,9 @@ export function LogsTabSeerComboBox() {
const queryParams = useQueryParams();
const analyticsArea = useAnalyticsArea();
const {setRunId} = useAiQueryContext();
- const {
- currentInputValueRef,
- query,
- committedQuery,
- askSeerSuggestedQueryRef,
- enableAISearch,
- } = useSearchQueryBuilder();
+ const {query, committedQuery} = useSearchQueryBuilderState();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {askSeerSuggestedQueryRef, enableAISearch} = useSearchQueryBuilderAI();
let initialSeerQuery = '';
const queryDetails = useMemo(() => {
diff --git a/static/app/views/explore/metrics/metricToolbar/filter.tsx b/static/app/views/explore/metrics/metricToolbar/filter.tsx
index 0b05a579ca6f60..56583d0f19b5bc 100644
--- a/static/app/views/explore/metrics/metricToolbar/filter.tsx
+++ b/static/app/views/explore/metrics/metricToolbar/filter.tsx
@@ -4,7 +4,7 @@ import {useQuery} from '@tanstack/react-query';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import type {TagCollection} from 'sentry/types/group';
import {FieldKind} from 'sentry/utils/fields';
@@ -52,7 +52,7 @@ function MetricsSearchBar({
tracesItemSearchQueryBuilderProps,
traceMetric,
}: MetricsSearchBarProps) {
- const {displayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
if (displayAskSeer) {
return ;
diff --git a/static/app/views/explore/metrics/metricsTabSeerComboBox.tsx b/static/app/views/explore/metrics/metricsTabSeerComboBox.tsx
index 1c9132810284c6..71d2fe1c51a385 100644
--- a/static/app/views/explore/metrics/metricsTabSeerComboBox.tsx
+++ b/static/app/views/explore/metrics/metricsTabSeerComboBox.tsx
@@ -6,7 +6,11 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useAiQueryContext} from 'sentry/components/searchQueryBuilder/askSeerCombobox/aiQueryContext';
import {AskSeerComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox';
import {AskSeerPollingComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
import {Token} from 'sentry/components/searchSyntax/parser';
import {stringifyToken} from 'sentry/components/searchSyntax/utils';
@@ -81,13 +85,9 @@ export function MetricsTabSeerComboBox({traceMetric}: MetricsTabSeerComboBoxProp
const queryParams = useQueryParams();
const metricQueries = useMultiMetricsQueryParams();
const analyticsArea = useAnalyticsArea();
- const {
- currentInputValueRef,
- query,
- committedQuery,
- askSeerSuggestedQueryRef,
- enableAISearch,
- } = useSearchQueryBuilder();
+ const {query, committedQuery} = useSearchQueryBuilderState();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {askSeerSuggestedQueryRef, enableAISearch} = useSearchQueryBuilderAI();
let initialSeerQuery = '';
const queryDetails = useMemo(() => {
diff --git a/static/app/views/explore/spans/spansTabSearchSection.tsx b/static/app/views/explore/spans/spansTabSearchSection.tsx
index 480c47c2bc684f..871e8ea92e4ff8 100644
--- a/static/app/views/explore/spans/spansTabSearchSection.tsx
+++ b/static/app/views/explore/spans/spansTabSearchSection.tsx
@@ -13,7 +13,7 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useSpanSearchQueryBuilderProps} from 'sentry/components/performance/spanSearchQueryBuilder';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import {useCaseInsensitivity} from 'sentry/components/searchQueryBuilder/hooks';
import {TourElement} from 'sentry/components/tours/components';
@@ -54,7 +54,7 @@ function SpansSearchBar({
}: {
spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps;
}) {
- const {displayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
if (displayAskSeer) {
return ;
diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx
index 4689a37557c9bb..73ae680e35eab9 100644
--- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx
+++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx
@@ -6,7 +6,11 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useAiQueryContext} from 'sentry/components/searchQueryBuilder/askSeerCombobox/aiQueryContext';
import {AskSeerComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerComboBox';
import {AskSeerPollingComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
import {Token} from 'sentry/components/searchSyntax/parser';
import {stringifyToken} from 'sentry/components/searchSyntax/utils';
@@ -78,13 +82,9 @@ export function SpansTabSeerComboBox() {
const organization = useOrganization();
const analyticsArea = useAnalyticsArea();
const {setRunId} = useAiQueryContext();
- const {
- currentInputValueRef,
- query,
- committedQuery,
- askSeerSuggestedQueryRef,
- enableAISearch,
- } = useSearchQueryBuilder();
+ const {query, committedQuery} = useSearchQueryBuilderState();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {askSeerSuggestedQueryRef, enableAISearch} = useSearchQueryBuilderAI();
const useTranslateEndpoint = organization.features.includes(
'gen-ai-search-agent-translate'
diff --git a/static/app/views/issueList/issueListSeerComboBox.tsx b/static/app/views/issueList/issueListSeerComboBox.tsx
index cea0daf72507fd..e3e1645097f2b2 100644
--- a/static/app/views/issueList/issueListSeerComboBox.tsx
+++ b/static/app/views/issueList/issueListSeerComboBox.tsx
@@ -6,7 +6,11 @@ import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {useAiQueryContext} from 'sentry/components/searchQueryBuilder/askSeerCombobox/aiQueryContext';
import {AskSeerPollingComboBox} from 'sentry/components/searchQueryBuilder/askSeerCombobox/askSeerPollingComboBox';
-import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
+import {
+ useSearchQueryBuilderAI,
+ useSearchQueryBuilderLayout,
+ useSearchQueryBuilderState,
+} from 'sentry/components/searchQueryBuilder/context';
import {Token} from 'sentry/components/searchSyntax/parser';
import {stringifyToken} from 'sentry/components/searchSyntax/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
@@ -50,14 +54,9 @@ export function IssueListSeerComboBox() {
const navigate = useNavigate();
const {setRunId} = useAiQueryContext();
const analyticsArea = useAnalyticsArea();
- const {
- currentInputValueRef,
- query,
- committedQuery,
- enableAISearch,
- askSeerSuggestedQueryRef,
- parseQuery,
- } = useSearchQueryBuilder();
+ const {query, committedQuery, parseQuery} = useSearchQueryBuilderState();
+ const {currentInputValueRef} = useSearchQueryBuilderLayout();
+ const {enableAISearch, askSeerSuggestedQueryRef} = useSearchQueryBuilderAI();
let initialSeerQuery = '';
const queryDetails = useMemo(() => {
diff --git a/static/app/views/issueList/issueSearch.tsx b/static/app/views/issueList/issueSearch.tsx
index 3aa79758f9d765..2b2b93427e5934 100644
--- a/static/app/views/issueList/issueSearch.tsx
+++ b/static/app/views/issueList/issueSearch.tsx
@@ -1,7 +1,7 @@
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {
SearchQueryBuilderProvider,
- useSearchQueryBuilder,
+ useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import {t} from 'sentry/locale';
import {SavedSearchType} from 'sentry/types/group';
@@ -18,7 +18,7 @@ type IssueSearchProps = {
function IssueSearchBar({query, onSearch, className}: IssueSearchProps) {
const organization = useOrganization();
- const {displayAskSeer} = useSearchQueryBuilder();
+ const {displayAskSeer} = useSearchQueryBuilderAI();
if (displayAskSeer) {
return ;
From a26a0bef04d21e6ee7b6a55d7324ce01f7e3c9d3 Mon Sep 17 00:00:00 2001
From: Max Topolsky <30879163+mtopo27@users.noreply.github.com>
Date: Thu, 28 May 2026 11:15:29 -0400
Subject: [PATCH 12/14] fix(snapshots): Increase snapshot test timeout to 30s
(#116378)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Snapshot tests launch Chromium via Playwright, render components to
HTML, and capture PNG screenshots. Jest's default 5s test timeout is too
tight for browser automation — especially when multiple workers run in
parallel and compete for CPU/IO — causing spurious timeouts on tests
that are otherwise correct.
Sets `testTimeout: 30_000` in `jest.config.snapshots.ts` to match
Playwright Test's own default for browser-driven tests. This only
affects snapshot tests; the main Jest config is unchanged.
---
jest.config.snapshots.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/jest.config.snapshots.ts b/jest.config.snapshots.ts
index 9175ca209126b5..e2d6cca557fe3e 100644
--- a/jest.config.snapshots.ts
+++ b/jest.config.snapshots.ts
@@ -41,6 +41,7 @@ const swcConfig: SwcOptions = {
const ESM_NODE_MODULES = ['screenfull', 'cbor2', 'nuqs', 'color'];
const config: Config.InitialOptions = {
+ testTimeout: 30_000,
cacheDirectory: '.cache/jest-snapshots',
// testEnvironment and testMatch are the core differences between this and the main config
testEnvironment: 'node',
From 15ebc79cb8c98dde509c61792642cd4683241406 Mon Sep 17 00:00:00 2001
From: Abdullah Khan <60121741+Abdkhan14@users.noreply.github.com>
Date: Thu, 28 May 2026 11:35:02 -0400
Subject: [PATCH 13/14] chore(mcp-adoption-value-discovery): Adding utm source
to mcp docs link (#116202)
Paired with: https://github.com/getsentry/sentry-mcp/pull/1002
---
static/app/views/settings/organizationMcpCli/index.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/static/app/views/settings/organizationMcpCli/index.tsx b/static/app/views/settings/organizationMcpCli/index.tsx
index 3117f8f00eca3b..5bb112d9ff0578 100644
--- a/static/app/views/settings/organizationMcpCli/index.tsx
+++ b/static/app/views/settings/organizationMcpCli/index.tsx
@@ -43,7 +43,11 @@ export default function OrganizationMcpCli() {
https://mcp.sentry.dev/mcp/your-org/your-project