From fdc20f36157475c2132a249156dc42c0ea5300f3 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 3 Jul 2025 13:47:08 -0300 Subject: [PATCH 01/28] :necktie: Update SpansTabWrapper to use new useTraceExploreAiQuerySetup hook over provider setup --- static/app/views/explore/spans/content.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/app/views/explore/spans/content.tsx b/static/app/views/explore/spans/content.tsx index b8555c49c4fb9a..5083cd60875bfc 100644 --- a/static/app/views/explore/spans/content.tsx +++ b/static/app/views/explore/spans/content.tsx @@ -23,6 +23,7 @@ import { } from 'sentry/views/explore/contexts/pageParamsContext'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; +import {useTraceExploreAiQuerySetup} from 'sentry/views/explore/hooks/useTraceExploreAiQuerySetup'; import {SavedQueryEditMenu} from 'sentry/views/explore/savedQueryEditMenu'; import {SpansTabContent, SpansTabOnboarding} from 'sentry/views/explore/spans/spansTab'; import { @@ -69,6 +70,8 @@ export function ExploreContent() { } function SpansTabWrapper({children}: SpansTabContextProps) { + useTraceExploreAiQuerySetup(); + return ( From 9bde3de05da088b5f1329c2203f9bae86ef4a873 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 3 Jul 2025 13:47:51 -0300 Subject: [PATCH 02/28] :necktie: Update SpanTabSearchSection to conditionally enable AI search features --- static/app/views/explore/spans/spansTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/spans/spansTab.tsx b/static/app/views/explore/spans/spansTab.tsx index 4d0a39c8337487..dc846ed51b2b57 100644 --- a/static/app/views/explore/spans/spansTab.tsx +++ b/static/app/views/explore/spans/spansTab.tsx @@ -187,9 +187,9 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps) const mode = useExploreMode(); const fields = useExploreFields(); const query = useExploreQuery(); + const organization = useOrganization(); const setExplorePageParams = useSetExplorePageParams(); - const organization = useOrganization(); const areAiFeaturesAllowed = !organization?.hideAiFeatures && organization.features.includes('gen-ai-features'); From 226149699e620d2b6efbc0bbe8c246cce479d740 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 3 Jul 2025 14:35:24 -0300 Subject: [PATCH 03/28] :necktie: Conditionally call trace explorer ai setup endpoint and move calling of hook --- static/app/views/explore/spans/content.tsx | 3 --- static/app/views/explore/spans/spansTab.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/static/app/views/explore/spans/content.tsx b/static/app/views/explore/spans/content.tsx index 5083cd60875bfc..b8555c49c4fb9a 100644 --- a/static/app/views/explore/spans/content.tsx +++ b/static/app/views/explore/spans/content.tsx @@ -23,7 +23,6 @@ import { } from 'sentry/views/explore/contexts/pageParamsContext'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; -import {useTraceExploreAiQuerySetup} from 'sentry/views/explore/hooks/useTraceExploreAiQuerySetup'; import {SavedQueryEditMenu} from 'sentry/views/explore/savedQueryEditMenu'; import {SpansTabContent, SpansTabOnboarding} from 'sentry/views/explore/spans/spansTab'; import { @@ -70,8 +69,6 @@ export function ExploreContent() { } function SpansTabWrapper({children}: SpansTabContextProps) { - useTraceExploreAiQuerySetup(); - return ( diff --git a/static/app/views/explore/spans/spansTab.tsx b/static/app/views/explore/spans/spansTab.tsx index dc846ed51b2b57..4d0a39c8337487 100644 --- a/static/app/views/explore/spans/spansTab.tsx +++ b/static/app/views/explore/spans/spansTab.tsx @@ -187,9 +187,9 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps) const mode = useExploreMode(); const fields = useExploreFields(); const query = useExploreQuery(); - const organization = useOrganization(); const setExplorePageParams = useSetExplorePageParams(); + const organization = useOrganization(); const areAiFeaturesAllowed = !organization?.hideAiFeatures && organization.features.includes('gen-ai-features'); From e671acba93c22096cd977a83c642940056db9ed4 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sat, 12 Jul 2025 12:36:30 +0200 Subject: [PATCH 04/28] :necktie: Add in new createAskSeerConsentItem function --- .../components/searchQueryBuilder/askSeer.tsx | 1 + .../tokens/filterKeyListBox/types.tsx | 11 +++++++++-- .../tokens/filterKeyListBox/utils.tsx | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index c483b4c24786ce..8fa9fa5037e22d 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {space} from 'sentry/styles/space'; export const ASK_SEER_ITEM_KEY = 'ask_seer'; +export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; export const AskSeerPane = styled('div')` grid-area: seer; diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/types.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/types.tsx index e38aa788c72a68..52c47fabb9c504 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/types.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/types.tsx @@ -53,13 +53,19 @@ export interface AskSeerItem extends SelectOptionWithKey { value: string; } +export interface AskSeerConsentItem extends SelectOptionWithKey { + type: 'ask-seer-consent'; + value: string; +} + export type SearchKeyItem = | KeySectionItem | KeyItem | RawSearchItem | FilterValueItem | RawSearchFilterValueItem - | AskSeerItem; + | AskSeerItem + | AskSeerConsentItem; export type FilterKeyItem = | KeyItem @@ -69,7 +75,8 @@ export type FilterKeyItem = | RawSearchItem | FilterValueItem | RawSearchFilterValueItem - | AskSeerItem; + | AskSeerItem + | AskSeerConsentItem; export type Section = { label: ReactNode; diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/utils.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/utils.tsx index 33fd9d3257923b..6af9dc529a9fab 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/utils.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/utils.tsx @@ -1,10 +1,14 @@ import styled from '@emotion/styled'; import {getEscapedKey} from 'sentry/components/core/compactSelect/utils'; -import {ASK_SEER_ITEM_KEY} from 'sentry/components/searchQueryBuilder/askSeer'; +import { + ASK_SEER_CONSENT_ITEM_KEY, + ASK_SEER_ITEM_KEY, +} from 'sentry/components/searchQueryBuilder/askSeer'; import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription'; import type { + AskSeerConsentItem, AskSeerItem, FilterValueItem, KeyItem, @@ -211,6 +215,17 @@ export function createAskSeerItem(): AskSeerItem { }; } +export function createAskSeerConsentItem(): AskSeerConsentItem { + return { + key: getEscapedKey(ASK_SEER_CONSENT_ITEM_KEY), + value: ASK_SEER_CONSENT_ITEM_KEY, + textValue: 'Enable Gen AI', + type: 'ask-seer-consent' as const, + label: t('Enable Gen AI'), + hideCheck: true, + }; +} + const SearchItemLabel = styled('div')` color: ${p => p.theme.subText}; white-space: nowrap; From b16e5663afc1507e5770649927ffdfe81d1a580f Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sat, 12 Jul 2025 12:38:46 +0200 Subject: [PATCH 05/28] :fire: Remove genAIConsent field as its not needed --- static/app/components/searchQueryBuilder/context.tsx | 6 ------ static/app/types/organization.tsx | 1 - tests/js/fixtures/organization.ts | 1 - 3 files changed, 8 deletions(-) diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 72510609fa6c1e..7dce5d2ca7c9a2 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -24,7 +24,6 @@ import type {SavedSearchType, Tag, TagCollection} from 'sentry/types/group'; import type {FieldDefinition, FieldKind} from 'sentry/utils/fields'; import {getFieldDefinition} from 'sentry/utils/fields'; import {useDimensions} from 'sentry/utils/useDimensions'; -import useOrganization from 'sentry/utils/useOrganization'; interface SearchQueryBuilderContextData { actionBarRef: React.RefObject; @@ -39,7 +38,6 @@ interface SearchQueryBuilderContextData { filterKeySections: FilterKeySection[]; filterKeys: TagCollection; focusOverride: FocusOverride | null; - genAIConsent: boolean; getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null; getSuggestedFilterKey: (key: string) => string | null; getTagValues: (tag: Tag, query: string) => Promise; @@ -102,8 +100,6 @@ export function SearchQueryBuilderProvider({ const wrapperRef = useRef(null); const actionBarRef = useRef(null); const [displaySeerResults, setDisplaySeerResults] = useState(false); - const organization = useOrganization(); - const genAIConsent = organization?.genAIConsent ?? false; const {state, dispatch} = useQueryBuilderState({ initialQuery, @@ -168,7 +164,6 @@ export function SearchQueryBuilderProvider({ disallowFreeText: Boolean(disallowFreeText), disallowWildcard: Boolean(disallowWildcard), enableAISearch: Boolean(enableAISearch), - genAIConsent, parseQuery, parsedQuery, filterKeySections: filterKeySections ?? [], @@ -201,7 +196,6 @@ export function SearchQueryBuilderProvider({ filterKeyAliases, filterKeyMenuWidth, filterKeySections, - genAIConsent, getTagValues, handleSearch, parseQuery, diff --git a/static/app/types/organization.tsx b/static/app/types/organization.tsx index e99f550b25c580..e2c3cb9cde32c5 100644 --- a/static/app/types/organization.tsx +++ b/static/app/types/organization.tsx @@ -64,7 +64,6 @@ export interface Organization extends OrganizationSummary { defaultRole: string; enhancedPrivacy: boolean; eventsMemberAdmin: boolean; - genAIConsent: boolean; isDefault: boolean; isDynamicallySampled: boolean; onboardingTasks: OnboardingTaskStatus[]; diff --git a/tests/js/fixtures/organization.ts b/tests/js/fixtures/organization.ts index 5e22a7fbdd2210..1cd2c6f4a71e06 100644 --- a/tests/js/fixtures/organization.ts +++ b/tests/js/fixtures/organization.ts @@ -59,7 +59,6 @@ export function OrganizationFixture(params: Partial = {}): Organiz githubOpenPRBot: false, githubPRBot: false, gitlabPRBot: false, - genAIConsent: false, hideAiFeatures: false, isDefault: false, isDynamicallySampled: true, From cc247ca894eb88ed043aa213469af9a945060eeb Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sat, 12 Jul 2025 12:40:45 +0200 Subject: [PATCH 06/28] :necktie: Update enableAISearch logic in SearchQueryBuilderProvider to check if ai features are hidden --- static/app/components/searchQueryBuilder/context.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 7dce5d2ca7c9a2..b12d43554043a7 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -24,6 +24,7 @@ import type {SavedSearchType, Tag, TagCollection} from 'sentry/types/group'; import type {FieldDefinition, FieldKind} from 'sentry/utils/fields'; import {getFieldDefinition} from 'sentry/utils/fields'; import {useDimensions} from 'sentry/utils/useDimensions'; +import useOrganization from 'sentry/utils/useOrganization'; interface SearchQueryBuilderContextData { actionBarRef: React.RefObject; @@ -100,6 +101,7 @@ export function SearchQueryBuilderProvider({ const wrapperRef = useRef(null); const actionBarRef = useRef(null); const [displaySeerResults, setDisplaySeerResults] = useState(false); + const organization = useOrganization(); const {state, dispatch} = useQueryBuilderState({ initialQuery, @@ -163,7 +165,7 @@ export function SearchQueryBuilderProvider({ disabled, disallowFreeText: Boolean(disallowFreeText), disallowWildcard: Boolean(disallowWildcard), - enableAISearch: Boolean(enableAISearch), + enableAISearch: Boolean(enableAISearch) && !organization.hideAiFeatures, parseQuery, parsedQuery, filterKeySections: filterKeySections ?? [], @@ -198,6 +200,7 @@ export function SearchQueryBuilderProvider({ filterKeySections, getTagValues, handleSearch, + organization.hideAiFeatures, parseQuery, parsedQuery, placeholder, From 6efa20c4b4fb39a1cdd2e2dc78df1d32c0010003 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sat, 12 Jul 2025 13:05:23 +0200 Subject: [PATCH 07/28] :necktie: Move all AskSeer related items into askSeer.tsx --- .../components/searchQueryBuilder/askSeer.tsx | 59 ++++++++++++++++++- .../searchQueryBuilder/tokens/combobox.tsx | 54 +---------------- .../tokens/filterKeyListBox/index.tsx | 52 +--------------- 3 files changed, 62 insertions(+), 103 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 8fa9fa5037e22d..d9e453ba848d09 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -1,11 +1,64 @@ +import {useRef} from 'react'; import styled from '@emotion/styled'; +import {useOption} from '@react-aria/listbox'; +import type {ComboBoxState} from '@react-stately/combobox'; +import { FeatureBadge } from 'sentry/components/core/badge/featureBadge'; +import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; +import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; +import {IconSeer} from 'sentry/icons'; +import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import useOrganization from 'sentry/utils/useOrganization'; export const ASK_SEER_ITEM_KEY = 'ask_seer'; export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; -export const AskSeerPane = styled('div')` +function AskSeerOption({state}: {state: ComboBoxState}) { + const ref = useRef(null); + const {setDisplaySeerResults} = useSearchQueryBuilder(); + const organization = useOrganization(); + + const {optionProps, labelProps, isFocused, isPressed} = useOption( + { + key: ASK_SEER_ITEM_KEY, + 'aria-label': 'Ask Seer', + shouldFocusOnHover: true, + shouldSelectOnPressUp: true, + }, + state, + ref + ); + + const handleClick = () => { + trackAnalytics('trace.explorer.ai_query_interface', { + organization, + action: 'opened', + }); + setDisplaySeerResults(true); + }; + + return ( + + + + + {t('Ask Seer')} + + + ); +} + +export function AskSeer({state}: {state: ComboBoxState}) { + return ( + + + + ); +} + +const AskSeerPane = styled('div')` grid-area: seer; display: flex; align-items: center; @@ -16,7 +69,7 @@ export const AskSeerPane = styled('div')` width: 100%; `; -export const AskSeerListItem = styled('div')` +const AskSeerListItem = styled('div')` position: relative; display: flex; align-items: center; @@ -46,7 +99,7 @@ export const AskSeerListItem = styled('div')` } `; -export const AskSeerLabel = styled('span')` +const AskSeerLabel = styled('span')` ${p => p.theme.overflowEllipsis}; color: ${p => p.theme.purple400}; font-size: ${p => p.theme.fontSize.md}; diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index 71dc9d6bf2451e..f9d24c02b12925 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -10,14 +10,13 @@ import { import {usePopper} from 'react-popper'; import styled from '@emotion/styled'; import {type AriaComboBoxProps} from '@react-aria/combobox'; -import {type AriaListBoxOptions, useOption} from '@react-aria/listbox'; +import {type AriaListBoxOptions} from '@react-aria/listbox'; import {ariaHideOutside} from '@react-aria/overlays'; import {mergeRefs} from '@react-aria/utils'; import {type ComboBoxState, useComboBoxState} from '@react-stately/combobox'; import type {CollectionChildren, Key, KeyboardEvent} from '@react-types/shared'; import Feature from 'sentry/components/acl/feature'; -import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import {ListBox} from 'sentry/components/core/compactSelect/listBox'; import type { SelectKey, @@ -29,14 +28,8 @@ import { } from 'sentry/components/core/compactSelect/utils'; import {Input} from 'sentry/components/core/input'; import {useAutosizeInput} from 'sentry/components/core/input/useAutosizeInput'; -import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {Overlay} from 'sentry/components/overlay'; -import { - ASK_SEER_ITEM_KEY, - AskSeerLabel, - AskSeerListItem, - AskSeerPane, -} from 'sentry/components/searchQueryBuilder/askSeer'; +import {ASK_SEER_ITEM_KEY, AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/tokens/useSearchTokenCombobox'; import { @@ -44,12 +37,8 @@ import { itemIsSection, } from 'sentry/components/searchQueryBuilder/tokens/utils'; import type {Token, TokenResult} from 'sentry/components/searchSyntax/parser'; -import {IconSeer} from 'sentry/icons'; -import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import useOrganization from 'sentry/utils/useOrganization'; import useOverlay from 'sentry/utils/useOverlay'; import usePrevious from 'sentry/utils/usePrevious'; @@ -260,41 +249,6 @@ function useUpdateOverlayPositionOnContentChange({ }, [contentRef, isOpen, updateOverlayPosition]); } -function AskSeerOption({state}: {state: ComboBoxState}) { - const ref = useRef(null); - const {setDisplaySeerResults} = useSearchQueryBuilder(); - const organization = useOrganization(); - - const {optionProps, labelProps, isFocused, isPressed} = useOption( - { - key: ASK_SEER_ITEM_KEY, - 'aria-label': 'Ask Seer', - shouldFocusOnHover: true, - shouldSelectOnPressUp: true, - }, - state, - ref - ); - - const handleClick = () => { - trackAnalytics('trace.explorer.ai_query_interface', { - organization, - action: 'opened', - }); - setDisplaySeerResults(true); - }; - - return ( - - - - - {t('Ask Seer')} - - - ); -} - function OverlayContent>({ customMenu, filterValue, @@ -349,9 +303,7 @@ function OverlayContent>({ /> {enableAISearch ? ( - - - + ) : null} diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx index e2919e55306410..3eaafea04c8343 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx @@ -7,7 +7,6 @@ import type {ComboBoxState} from '@react-stately/combobox'; import type {Key} from '@react-types/shared'; import Feature from 'sentry/components/acl/feature'; -import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import {Button} from 'sentry/components/core/button'; import {ListBox} from 'sentry/components/core/compactSelect/listBox'; import type { @@ -16,12 +15,7 @@ import type { } from 'sentry/components/core/compactSelect/types'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {Overlay} from 'sentry/components/overlay'; -import { - ASK_SEER_ITEM_KEY, - AskSeerLabel, - AskSeerListItem, - AskSeerPane, -} from 'sentry/components/searchQueryBuilder/askSeer'; +import {ASK_SEER_ITEM_KEY, AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import type {CustomComboboxMenuProps} from 'sentry/components/searchQueryBuilder/tokens/combobox'; import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription'; @@ -32,12 +26,10 @@ import { } from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/utils'; import type {Token, TokenResult} from 'sentry/components/searchSyntax/parser'; import {getKeyLabel, getKeyName} from 'sentry/components/searchSyntax/utils'; -import {IconMegaphone, IconSeer} from 'sentry/icons'; +import {IconMegaphone} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {trackAnalytics} from 'sentry/utils/analytics'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; -import useOrganization from 'sentry/utils/useOrganization'; import usePrevious from 'sentry/utils/usePrevious'; interface FilterKeyListBoxProps extends CustomComboboxMenuProps { @@ -144,42 +136,6 @@ function RecentSearchFilterOption({ ); } -function AskSeerOption({state}: {state: ComboBoxState}) { - const ref = useRef(null); - const {setDisplaySeerResults} = useSearchQueryBuilder(); - const organization = useOrganization(); - - const {optionProps, labelProps, isFocused, isPressed} = useOption( - { - key: ASK_SEER_ITEM_KEY, - 'aria-label': 'Ask Seer', - shouldFocusOnHover: true, - shouldSelectOnPressUp: true, - }, - state, - ref - ); - - const handleClick = () => { - trackAnalytics('trace.explorer.ai_query_interface', { - organization, - action: 'opened', - }); - setDisplaySeerResults(true); - }; - - return ( - - - - - {t('Ask Seer')} - - - - ); -} - function useHighlightFirstOptionOnSectionChange({ state, selectedSection, @@ -271,9 +227,7 @@ function FilterKeyMenuContent>({ {enableAISearch ? ( - - - + ) : null} {showRecentFilters ? ( From 884eb102aa5456eb94cfa1171f58f1fa65a33de0 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 19:48:45 +0200 Subject: [PATCH 08/28] :necktie: Add consent_accepted action to ai_query_interface analytics --- static/app/utils/analytics/tracingEventMap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/analytics/tracingEventMap.tsx b/static/app/utils/analytics/tracingEventMap.tsx index b3d789e291af4c..631f64df23630d 100644 --- a/static/app/utils/analytics/tracingEventMap.tsx +++ b/static/app/utils/analytics/tracingEventMap.tsx @@ -16,7 +16,7 @@ export type TracingEventParameters = { visualize_count: number; }; 'trace.explorer.ai_query_interface': { - action: 'opened' | 'closed'; + action: 'opened' | 'closed' | 'consent_accepted'; }; 'trace.explorer.ai_query_rejected': { natural_language_query: string; From dc1edcf37e67f8c60b6594716da1c09996fec897 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 19:53:58 +0200 Subject: [PATCH 09/28] :necktie: Add gaveSeerConsentRef to SearchQueryBuilderContext --- static/app/components/searchQueryBuilder/context.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index b12d43554043a7..98085f65fe672c 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -39,6 +39,7 @@ interface SearchQueryBuilderContextData { filterKeySections: FilterKeySection[]; filterKeys: TagCollection; focusOverride: FocusOverride | null; + gaveSeerConsentRef: React.RefObject; getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null; getSuggestedFilterKey: (key: string) => string | null; getTagValues: (tag: Tag, query: string) => Promise; @@ -100,9 +101,11 @@ export function SearchQueryBuilderProvider({ }: SearchQueryBuilderProps & {children: React.ReactNode}) { const wrapperRef = useRef(null); const actionBarRef = useRef(null); - const [displaySeerResults, setDisplaySeerResults] = useState(false); const organization = useOrganization(); + const [displaySeerResults, setDisplaySeerResults] = useState(false); + const gaveSeerConsentRef = useRef(false); + const {state, dispatch} = useQueryBuilderState({ initialQuery, getFieldDefinition: fieldDefinitionGetter, @@ -187,6 +190,7 @@ export function SearchQueryBuilderProvider({ setDisplaySeerResults, replaceRawSearchKeys, filterKeyAliases, + gaveSeerConsentRef, }; }, [ disabled, From cb8e6402d28e45a63cbc69cc450fce50e57e3aec Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 19:55:23 +0200 Subject: [PATCH 10/28] :necktie: Ignore ask-seer-consent when freeText onOptionSelected callback is triggered --- static/app/components/searchQueryBuilder/tokens/freeText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/tokens/freeText.tsx b/static/app/components/searchQueryBuilder/tokens/freeText.tsx index d231e9b2161361..26d0c4e104b6fc 100644 --- a/static/app/components/searchQueryBuilder/tokens/freeText.tsx +++ b/static/app/components/searchQueryBuilder/tokens/freeText.tsx @@ -394,7 +394,7 @@ function SearchQueryBuilderInputInternal({ onOptionSelected={option => { if (handleOptionSelected) { handleOptionSelected(option); - if (option.type === 'ask-seer') { + if (option.type === 'ask-seer' || option.type === 'ask-seer-consent') { return; } } From 02c78b80c96822ddf6b7bb5454fba4fc6005397e Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 20:09:26 +0200 Subject: [PATCH 11/28] :necktie: Add ask-seer-consent items to lists --- .../searchQueryBuilder/tokens/combobox.tsx | 22 +++++++++++---- .../tokens/filterKeyListBox/index.tsx | 7 ++++- .../filterKeyListBox/useFilterKeyListBox.tsx | 27 ++++++++++++++++--- .../tokens/useSortedFilterKeyItems.tsx | 12 ++++++++- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index f9d24c02b12925..ab41f13094f97d 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -29,7 +29,11 @@ import { import {Input} from 'sentry/components/core/input'; import {useAutosizeInput} from 'sentry/components/core/input/useAutosizeInput'; import {Overlay} from 'sentry/components/overlay'; -import {ASK_SEER_ITEM_KEY, AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; +import { + ASK_SEER_CONSENT_ITEM_KEY, + ASK_SEER_ITEM_KEY, + AskSeer, +} from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/tokens/useSearchTokenCombobox'; import { @@ -163,7 +167,7 @@ function menuIsOpen({ // When a custom menu is not being displayed and we aren't loading anything, // only show when there is something to select from. - return openState && totalOptions > hiddenOptions.size; + return openState && totalOptions >= hiddenOptions.size; } function useHiddenItems>({ @@ -185,13 +189,21 @@ function useHiddenItems>({ shouldFilterResults ? filterValue : '', maxOptions ); - return showAskSeerOption ? options.add(ASK_SEER_ITEM_KEY) : options; + + if (showAskSeerOption) { + options.add(ASK_SEER_ITEM_KEY); + options.add(ASK_SEER_CONSENT_ITEM_KEY); + } + + return options; }, [items, shouldFilterResults, filterValue, maxOptions, showAskSeerOption]); const disabledKeys = useMemo(() => { const baseDisabledKeys = [...getDisabledOptions(items), ...hiddenOptions]; return showAskSeerOption - ? baseDisabledKeys.filter(key => key !== ASK_SEER_ITEM_KEY) + ? baseDisabledKeys.filter( + key => key !== ASK_SEER_ITEM_KEY && key !== ASK_SEER_CONSENT_ITEM_KEY + ) : baseDisabledKeys; }, [hiddenOptions, items, showAskSeerOption]); @@ -621,7 +633,7 @@ const ListBoxOverlay = styled(Overlay)` max-height: 400px; min-width: 200px; width: 600px; - max-width: min-content; + max-width: fit-content; overflow-y: auto; `; diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx index 3eaafea04c8343..21837755466144 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx @@ -15,7 +15,11 @@ import type { } from 'sentry/components/core/compactSelect/types'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {Overlay} from 'sentry/components/overlay'; -import {ASK_SEER_ITEM_KEY, AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; +import { + ASK_SEER_CONSENT_ITEM_KEY, + ASK_SEER_ITEM_KEY, + AskSeer, +} from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import type {CustomComboboxMenuProps} from 'sentry/components/searchQueryBuilder/tokens/combobox'; import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription'; @@ -315,6 +319,7 @@ export function FilterKeyListBox> if (enableAISearch) { baseHidden.push(ASK_SEER_ITEM_KEY); + baseHidden.push(ASK_SEER_CONSENT_ITEM_KEY); } return new Set(baseHidden); diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx index f0b46e0e847da0..78ea3faab51ad9 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx @@ -16,6 +16,7 @@ import {useRecentSearchFilters} from 'sentry/components/searchQueryBuilder/token import { ALL_CATEGORY, ALL_CATEGORY_VALUE, + createAskSeerConsentItem, createAskSeerItem, createRecentFilterItem, createRecentFilterOptionKey, @@ -162,8 +163,13 @@ function useFilterKeySections({ return {sections, selectedSection, setSelectedSection}; } export function useFilterKeyListBox({filterValue}: {filterValue: string}) { - const {filterKeys, getFieldDefinition, setDisplaySeerResults, enableAISearch} = - useSearchQueryBuilder(); + const { + filterKeys, + getFieldDefinition, + setDisplaySeerResults, + enableAISearch, + gaveSeerConsentRef, + } = useSearchQueryBuilder(); const {sectionedItems} = useFilterKeyItems(); const recentFilters = useRecentSearchFilters(); const {data: recentSearches} = useRecentSearches(); @@ -176,7 +182,12 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { const filterKeyMenuItems = useMemo(() => { const recentFilterItems = makeRecentFilterItems({recentFilters}); - const askSeerItem = enableAISearch ? [createAskSeerItem()] : []; + const askSeerItem = []; + if (enableAISearch) { + askSeerItem.push( + gaveSeerConsentRef.current ? createAskSeerItem() : createAskSeerConsentItem() + ); + } if (selectedSection === RECENT_SEARCH_CATEGORY_VALUE) { return [ @@ -206,6 +217,7 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { enableAISearch, filterKeys, getFieldDefinition, + gaveSeerConsentRef, recentFilters, recentSearches, sectionedItems, @@ -378,6 +390,15 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { setDisplaySeerResults(true); return; } + + if (option.type === 'ask-seer-consent') { + // TODO: Implement logic to handle consent + trackAnalytics('trace.explorer.ai_query_interface', { + organization, + action: 'consent_accepted', + }); + return; + } }, [organization, setDisplaySeerResults] ); diff --git a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx index 254f1acf5eb8a1..b87ae3bdf8cc56 100644 --- a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx +++ b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx @@ -7,6 +7,7 @@ import type { SearchKeyItem, } from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/types'; import { + createAskSeerConsentItem, createAskSeerItem, createFilterValueItem, createItem, @@ -137,6 +138,7 @@ export function useSortedFilterKeyItems({ disallowFreeText, replaceRawSearchKeys, enableAISearch, + gaveSeerConsentRef, } = useSearchQueryBuilder(); const organization = useOrganization(); @@ -245,6 +247,13 @@ export function useSortedFilterKeyItems({ type: 'section', }; + const askSeerItem = []; + if (enableAISearch) { + askSeerItem.push( + gaveSeerConsentRef.current ? createAskSeerItem() : createAskSeerConsentItem() + ); + } + const {shouldShowAtTop, suggestedFiltersSection} = getValueSuggestionsFromSearchResult(searched); @@ -254,7 +263,7 @@ export function useSortedFilterKeyItems({ ...(shouldIncludeRawSearch ? [rawSearchSection] : []), keyItemsSection, ...(!shouldShowAtTop && suggestedFiltersSection ? [suggestedFiltersSection] : []), - ...(enableAISearch ? [createAskSeerItem()] : []), + ...askSeerItem, ]; } @@ -267,6 +276,7 @@ export function useSortedFilterKeyItems({ filterValue, flatKeys, getFieldDefinition, + gaveSeerConsentRef, hasRawSearchReplacement, includeSuggestions, inputValue, From 67ec4ac578a950ca75c47f044cb6a8e95dd9ce48 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 20:10:05 +0200 Subject: [PATCH 12/28] :white_check_mark: Update search query builder tests --- .../searchQueryBuilder/index.spec.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 790b9e7c56a815..297d8a18950438 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -1,5 +1,6 @@ import type {ComponentProps} from 'react'; import {destroyAnnouncer} from '@react-aria/live-announcer'; +import {AutofixSetupFixture} from 'sentry-fixture/autofixSetupFixture'; import { act, @@ -949,6 +950,16 @@ describe('SearchQueryBuilder', function () { it('displays ask seer button when searching free text', async function () { const mockOnSearch = jest.fn(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: true, + userHasAcknowledged: true, + }, + }), + }); + render( , {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} @@ -960,6 +971,29 @@ describe('SearchQueryBuilder', function () { expect(screen.getByRole('option', {name: /Ask Seer/i})).toBeInTheDocument(); }); + it('displays enable ai button when searching free text and user has not given consent', async function () { + const mockOnSearch = jest.fn(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + render( + , + {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} + ); + + await userEvent.click(getLastInput()); + await userEvent.type(screen.getByRole('combobox'), 'some free text'); + + expect(screen.getByRole('option', {name: 'Enable Gen AI'})).toBeInTheDocument(); + }); + it('can add parens by typing', async function () { const mockOnChange = jest.fn(); render(); From bec4db59193c5b3ebfc287dff0deeead8c5d2f26 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 20:35:29 +0200 Subject: [PATCH 13/28] :necktie: Add in AskSeerConsent flow to askSeer.tsx --- .../components/searchQueryBuilder/askSeer.tsx | 166 +++++++++++++++++- 1 file changed, 159 insertions(+), 7 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index d9e453ba848d09..b6c8b8c0085f29 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -1,20 +1,120 @@ -import {useRef} from 'react'; +import {useEffect, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {useOption} from '@react-aria/listbox'; import type {ComboBoxState} from '@react-stately/combobox'; -import { FeatureBadge } from 'sentry/components/core/badge/featureBadge'; +import {promptsUpdate} from 'sentry/actionCreators/prompts'; +import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; +import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; +import ExternalLink from 'sentry/components/links/externalLink'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {IconSeer} from 'sentry/icons'; -import {t} from 'sentry/locale'; +import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {useIsMutating, useMutation, useQueryClient} from 'sentry/utils/queryClient'; +import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; export const ASK_SEER_ITEM_KEY = 'ask_seer'; export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; +const setupCheckQueryKey = (orgSlug: string) => + `/organizations/${orgSlug}/seer/setup-check/`; + +function AskSeerConsentOption({state}: {state: ComboBoxState}) { + const api = useApi(); + const queryClient = useQueryClient(); + const organization = useOrganization(); + const itemRef = useRef(null); + const linkRef = useRef(null); + const [optionDisableOverride, setOptionDisableOverride] = useState(false); + + useEffect(() => { + const link = linkRef.current; + if (!link) return undefined; + + const disableOption = () => setOptionDisableOverride(true); + const enableOption = () => setOptionDisableOverride(false); + + link.addEventListener('mouseover', disableOption); + link.addEventListener('mouseout', enableOption); + + return () => { + link.removeEventListener('mouseover', disableOption); + link.removeEventListener('mouseout', enableOption); + }; + }, []); + + const seerAcknowledgeMutation = useMutation({ + mutationKey: [setupCheckQueryKey(organization.slug)], + mutationFn: () => { + return promptsUpdate(api, { + organization, + feature: 'seer_autofix_setup_acknowledged', + status: 'dismissed', + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [setupCheckQueryKey(organization.slug)], + }); + }, + }); + + const {optionProps, labelProps, isFocused, isPressed} = useOption( + { + key: ASK_SEER_CONSENT_ITEM_KEY, + 'aria-label': 'Enable Gen AI', + shouldFocusOnHover: true, + shouldSelectOnPressUp: true, + isDisabled: optionDisableOverride, + }, + state, + itemRef + ); + + const handleClick = () => { + trackAnalytics('trace.explorer.ai_query_interface', { + organization, + action: 'consent_accepted', + }); + seerAcknowledgeMutation.mutate(); + }; + + return ( + + +
+ + + {t('Enable Gen AI')} + +
+ + {tct( + 'Query assistant requires Generative AI which is subject to our [dataProcessingPolicy:data processing policy].', + { + dataProcessingPolicy: ( + + ), + } + )} + +
+ ); +} + function AskSeerOption({state}: {state: ComboBoxState}) { const ref = useRef(null); const {setDisplaySeerResults} = useSearchQueryBuilder(); @@ -26,6 +126,7 @@ function AskSeerOption({state}: {state: ComboBoxState}) { 'aria-label': 'Ask Seer', shouldFocusOnHover: true, shouldSelectOnPressUp: true, + isDisabled: false, }, state, ref @@ -51,13 +152,63 @@ function AskSeerOption({state}: {state: ComboBoxState}) { } export function AskSeer({state}: {state: ComboBoxState}) { + const organization = useOrganization(); + const {gaveSeerConsentRef} = useSearchQueryBuilder(); + const isMutating = useIsMutating({ + mutationKey: [setupCheckQueryKey(organization.slug)], + }); + + const {setupAcknowledgement, isPending: isPendingSetupCheck} = + useOrganizationSeerSetup(); + const orgHasAcknowledged = setupAcknowledgement.orgHasAcknowledged; + + if (!gaveSeerConsentRef.current && orgHasAcknowledged && !isPendingSetupCheck) { + gaveSeerConsentRef.current = true; + } + + if (isPendingSetupCheck || isMutating) { + return ( + + + {t('Loading Seer')} + + + + ); + } + + if (orgHasAcknowledged) { + return ( + + + + ); + } + return ( - + ); } +const TooltipSubExternalLink = styled(ExternalLink)` + color: ${p => p.theme.purple400}; + + :hover { + color: ${p => p.theme.purple400}; + text-decoration: underline; + } +`; + +const SeerConsentText = styled('p')` + color: ${p => p.theme.subText}; + font-size: ${p => p.theme.fontSize.xs}; + font-weight: ${p => p.theme.fontWeight.normal}; + margin: 0; + background-color: none; +`; + const AskSeerPane = styled('div')` grid-area: seer; display: flex; @@ -69,7 +220,7 @@ const AskSeerPane = styled('div')` width: 100%; `; -const AskSeerListItem = styled('div')` +const AskSeerListItem = styled('div')<{justifyContent?: 'flex-start' | 'space-between'}>` position: relative; display: flex; align-items: center; @@ -83,7 +234,7 @@ const AskSeerListItem = styled('div')` font-size: ${p => p.theme.fontSize.md}; font-weight: ${p => p.theme.fontWeight.bold}; text-align: left; - justify-content: flex-start; + justify-content: ${p => p.justifyContent ?? 'flex-start'}; gap: ${space(1)}; list-style: none; margin: 0; @@ -99,7 +250,7 @@ const AskSeerListItem = styled('div')` } `; -const AskSeerLabel = styled('span')` +const AskSeerLabel = styled('span')<{width?: 'auto'}>` ${p => p.theme.overflowEllipsis}; color: ${p => p.theme.purple400}; font-size: ${p => p.theme.fontSize.md}; @@ -107,4 +258,5 @@ const AskSeerLabel = styled('span')` display: flex; align-items: center; gap: ${space(1)}; + width: ${p => p.width}; `; From b80b69eeae2c722d15f3890e3eae729d1e59ffa7 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Sun, 13 Jul 2025 20:37:22 +0200 Subject: [PATCH 14/28] :white_check_mark: Add in tests for askSeer --- .../searchQueryBuilder/askSeer.spec.tsx | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 static/app/components/searchQueryBuilder/askSeer.spec.tsx diff --git a/static/app/components/searchQueryBuilder/askSeer.spec.tsx b/static/app/components/searchQueryBuilder/askSeer.spec.tsx new file mode 100644 index 00000000000000..43956e4513d4c3 --- /dev/null +++ b/static/app/components/searchQueryBuilder/askSeer.spec.tsx @@ -0,0 +1,126 @@ +import {useComboBoxState} from '@react-stately/combobox'; +import {AutofixSetupFixture} from 'sentry-fixture/autofixSetupFixture'; +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; + +import {AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; +import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; +import {FieldKind} from 'sentry/utils/fields'; + +// Mock the useOption hook to avoid the "Unknown list" error +jest.mock('@react-aria/listbox', () => ({ + ...jest.requireActual('@react-aria/listbox'), + useOption: jest.fn(() => ({ + optionProps: { + role: 'option', + 'aria-selected': false, + 'aria-disabled': false, + }, + labelProps: {}, + isFocused: false, + isPressed: false, + })), +})); + +function MockComboBoxComponent() { + const comboBoxState = useComboBoxState({ + items: [{id: 1, name: 'one'}], + disabledKeys: [], + }); + + return ( + Promise.resolve([])} + initialQuery="" + searchSource="" + enableAISearch + > + + + ); +} + +describe('AskSeer', () => { + it('renders ask seer button when user has given consent', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: true, + userHasAcknowledged: true, + }, + }), + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + const askSeer = await screen.findByRole('option', {name: 'Ask Seer'}); + expect(askSeer).toBeInTheDocument(); + }); + + it('renders enable ai button when user has not given consent', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + const enableAi = await screen.findByText('Enable Gen AI'); + expect(enableAi).toBeInTheDocument(); + }); + + describe('user clicks on enable gen ai button', () => { + it('calls promptsUpdate', async () => { + const organization = OrganizationFixture(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + const promptsUpdateMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/prompts-activity/`, + method: 'PUT', + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + const enableAi = await screen.findByText('Enable Gen AI'); + expect(enableAi).toBeInTheDocument(); + + await userEvent.click(enableAi); + + await waitFor(() => { + expect(promptsUpdateMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + data: { + feature: 'seer_autofix_setup_acknowledged', + organization_id: organization.id, + project_id: undefined, + status: 'dismissed', + }, + }) + ); + }); + }); + }); +}); From fbfcd5275317a5075235252ad1094bc39d159d14 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 08:23:00 +0200 Subject: [PATCH 15/28] :necktie: Move acknowledgement mutation into it's own custom hook --- .../components/searchQueryBuilder/askSeer.tsx | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index b6c8b8c0085f29..5f8f97424bbb02 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -24,13 +24,36 @@ export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; const setupCheckQueryKey = (orgSlug: string) => `/organizations/${orgSlug}/seer/setup-check/`; -function AskSeerConsentOption({state}: {state: ComboBoxState}) { +export function useSeerAcknowledgeMutation() { const api = useApi(); const queryClient = useQueryClient(); const organization = useOrganization(); + + const {mutate} = useMutation({ + mutationKey: [setupCheckQueryKey(organization.slug)], + mutationFn: () => { + return promptsUpdate(api, { + organization, + feature: 'seer_autofix_setup_acknowledged', + status: 'dismissed', + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [setupCheckQueryKey(organization.slug)], + }); + }, + }); + + return {mutate}; +} + +function AskSeerConsentOption({state}: {state: ComboBoxState}) { + const organization = useOrganization(); const itemRef = useRef(null); const linkRef = useRef(null); const [optionDisableOverride, setOptionDisableOverride] = useState(false); + const {mutate: seerAcknowledgeMutate} = useSeerAcknowledgeMutation(); useEffect(() => { const link = linkRef.current; @@ -48,22 +71,6 @@ function AskSeerConsentOption({state}: {state: ComboBoxState}) { }; }, []); - const seerAcknowledgeMutation = useMutation({ - mutationKey: [setupCheckQueryKey(organization.slug)], - mutationFn: () => { - return promptsUpdate(api, { - organization, - feature: 'seer_autofix_setup_acknowledged', - status: 'dismissed', - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [setupCheckQueryKey(organization.slug)], - }); - }, - }); - const {optionProps, labelProps, isFocused, isPressed} = useOption( { key: ASK_SEER_CONSENT_ITEM_KEY, @@ -81,7 +88,7 @@ function AskSeerConsentOption({state}: {state: ComboBoxState}) { organization, action: 'consent_accepted', }); - seerAcknowledgeMutation.mutate(); + seerAcknowledgeMutate(); }; return ( From ee0894dd49555bba6fc622c268a571c39d297076 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 08:23:38 +0200 Subject: [PATCH 16/28] :necktie: Conditionally add options to hidden items to resolve disallowFreeText issue --- .../searchQueryBuilder/tokens/combobox.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index ab41f13094f97d..a1e51c5d4a5c20 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -167,7 +167,7 @@ function menuIsOpen({ // When a custom menu is not being displayed and we aren't loading anything, // only show when there is something to select from. - return openState && totalOptions >= hiddenOptions.size; + return openState && totalOptions > hiddenOptions.size; } function useHiddenItems>({ @@ -183,6 +183,7 @@ function useHiddenItems>({ maxOptions?: number; shouldFilterResults?: boolean; }) { + const {gaveSeerConsentRef} = useSearchQueryBuilder(); const hiddenOptions: Set = useMemo(() => { const options = getHiddenOptions( items, @@ -191,12 +192,22 @@ function useHiddenItems>({ ); if (showAskSeerOption) { - options.add(ASK_SEER_ITEM_KEY); - options.add(ASK_SEER_CONSENT_ITEM_KEY); + if (gaveSeerConsentRef.current) { + options.add(ASK_SEER_ITEM_KEY); + } else { + options.add(ASK_SEER_CONSENT_ITEM_KEY); + } } return options; - }, [items, shouldFilterResults, filterValue, maxOptions, showAskSeerOption]); + }, [ + filterValue, + gaveSeerConsentRef, + items, + maxOptions, + shouldFilterResults, + showAskSeerOption, + ]); const disabledKeys = useMemo(() => { const baseDisabledKeys = [...getDisabledOptions(items), ...hiddenOptions]; From f69c2ef7da53476bf268e518aaa1254f76680b45 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 08:24:15 +0200 Subject: [PATCH 17/28] :necktie: Utilize acknowledgement hook when selecting ask seer option from key selection dropdown --- .../tokens/filterKeyListBox/useFilterKeyListBox.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx index 78ea3faab51ad9..4262d9b9e1d998 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx @@ -3,6 +3,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; import type {ComboBoxState} from '@react-stately/combobox'; import type {Node} from '@react-types/shared'; +import {useSeerAcknowledgeMutation} from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import type {CustomComboboxMenu} from 'sentry/components/searchQueryBuilder/tokens/combobox'; import {FilterKeyListBox} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox'; @@ -380,6 +381,8 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { [handleArrowUpDown, handleCycleRecentFilterKeys, handleCycleSections] ); + const {mutate: seerAcknowledgeMutate} = useSeerAcknowledgeMutation(); + const handleOptionSelected = useCallback( (option: FilterKeyItem) => { if (option.type === 'ask-seer') { @@ -392,15 +395,15 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { } if (option.type === 'ask-seer-consent') { - // TODO: Implement logic to handle consent trackAnalytics('trace.explorer.ai_query_interface', { organization, action: 'consent_accepted', }); + seerAcknowledgeMutate(); return; } }, - [organization, setDisplaySeerResults] + [organization, seerAcknowledgeMutate, setDisplaySeerResults] ); return { From 27152e9abf13806f20ef83e367858d760bfa1f70 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 09:26:48 +0200 Subject: [PATCH 18/28] :necktie: Grab consent status directly in context and pass down --- static/app/components/searchQueryBuilder/askSeer.tsx | 12 +++--------- static/app/components/searchQueryBuilder/context.tsx | 8 +++++--- .../searchQueryBuilder/tokens/combobox.tsx | 6 +++--- .../tokens/filterKeyListBox/useFilterKeyListBox.tsx | 6 +++--- .../tokens/useSortedFilterKeyItems.tsx | 6 +++--- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 5f8f97424bbb02..4c4b8e31c7d836 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -160,18 +160,12 @@ function AskSeerOption({state}: {state: ComboBoxState}) { export function AskSeer({state}: {state: ComboBoxState}) { const organization = useOrganization(); - const {gaveSeerConsentRef} = useSearchQueryBuilder(); + const {gaveSeerConsent} = useSearchQueryBuilder(); const isMutating = useIsMutating({ mutationKey: [setupCheckQueryKey(organization.slug)], }); - const {setupAcknowledgement, isPending: isPendingSetupCheck} = - useOrganizationSeerSetup(); - const orgHasAcknowledged = setupAcknowledgement.orgHasAcknowledged; - - if (!gaveSeerConsentRef.current && orgHasAcknowledged && !isPendingSetupCheck) { - gaveSeerConsentRef.current = true; - } + const {isPending: isPendingSetupCheck} = useOrganizationSeerSetup(); if (isPendingSetupCheck || isMutating) { return ( @@ -184,7 +178,7 @@ export function AskSeer({state}: {state: ComboBoxState}) { ); } - if (orgHasAcknowledged) { + if (gaveSeerConsent) { return ( diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 98085f65fe672c..96f2007714409c 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -8,6 +8,7 @@ import { useState, } from 'react'; +import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; import type {SearchQueryBuilderProps} from 'sentry/components/searchQueryBuilder'; import {useHandleSearch} from 'sentry/components/searchQueryBuilder/hooks/useHandleSearch'; import { @@ -39,7 +40,7 @@ interface SearchQueryBuilderContextData { filterKeySections: FilterKeySection[]; filterKeys: TagCollection; focusOverride: FocusOverride | null; - gaveSeerConsentRef: React.RefObject; + gaveSeerConsent: boolean; getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null; getSuggestedFilterKey: (key: string) => string | null; getTagValues: (tag: Tag, query: string) => Promise; @@ -102,9 +103,9 @@ export function SearchQueryBuilderProvider({ const wrapperRef = useRef(null); const actionBarRef = useRef(null); const organization = useOrganization(); + const {setupAcknowledgement} = useOrganizationSeerSetup(); const [displaySeerResults, setDisplaySeerResults] = useState(false); - const gaveSeerConsentRef = useRef(false); const {state, dispatch} = useQueryBuilderState({ initialQuery, @@ -190,7 +191,7 @@ export function SearchQueryBuilderProvider({ setDisplaySeerResults, replaceRawSearchKeys, filterKeyAliases, - gaveSeerConsentRef, + gaveSeerConsent: setupAcknowledgement.orgHasAcknowledged, }; }, [ disabled, @@ -212,6 +213,7 @@ export function SearchQueryBuilderProvider({ recentSearches, replaceRawSearchKeys, searchSource, + setupAcknowledgement.orgHasAcknowledged, size, stableFieldDefinitionGetter, stableFilterKeys, diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index a1e51c5d4a5c20..6ae45baec674f4 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -183,7 +183,7 @@ function useHiddenItems>({ maxOptions?: number; shouldFilterResults?: boolean; }) { - const {gaveSeerConsentRef} = useSearchQueryBuilder(); + const {gaveSeerConsent} = useSearchQueryBuilder(); const hiddenOptions: Set = useMemo(() => { const options = getHiddenOptions( items, @@ -192,7 +192,7 @@ function useHiddenItems>({ ); if (showAskSeerOption) { - if (gaveSeerConsentRef.current) { + if (gaveSeerConsent) { options.add(ASK_SEER_ITEM_KEY); } else { options.add(ASK_SEER_CONSENT_ITEM_KEY); @@ -202,7 +202,7 @@ function useHiddenItems>({ return options; }, [ filterValue, - gaveSeerConsentRef, + gaveSeerConsent, items, maxOptions, shouldFilterResults, diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx index 4262d9b9e1d998..d9b1a609a1a606 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx @@ -169,7 +169,7 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { getFieldDefinition, setDisplaySeerResults, enableAISearch, - gaveSeerConsentRef, + gaveSeerConsent, } = useSearchQueryBuilder(); const {sectionedItems} = useFilterKeyItems(); const recentFilters = useRecentSearchFilters(); @@ -186,7 +186,7 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { const askSeerItem = []; if (enableAISearch) { askSeerItem.push( - gaveSeerConsentRef.current ? createAskSeerItem() : createAskSeerConsentItem() + gaveSeerConsent ? createAskSeerItem() : createAskSeerConsentItem() ); } @@ -217,8 +217,8 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { }, [ enableAISearch, filterKeys, + gaveSeerConsent, getFieldDefinition, - gaveSeerConsentRef, recentFilters, recentSearches, sectionedItems, diff --git a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx index b87ae3bdf8cc56..4e56aa3bedf631 100644 --- a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx +++ b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx @@ -138,7 +138,7 @@ export function useSortedFilterKeyItems({ disallowFreeText, replaceRawSearchKeys, enableAISearch, - gaveSeerConsentRef, + gaveSeerConsent, } = useSearchQueryBuilder(); const organization = useOrganization(); @@ -250,7 +250,7 @@ export function useSortedFilterKeyItems({ const askSeerItem = []; if (enableAISearch) { askSeerItem.push( - gaveSeerConsentRef.current ? createAskSeerItem() : createAskSeerConsentItem() + gaveSeerConsent ? createAskSeerItem() : createAskSeerConsentItem() ); } @@ -275,8 +275,8 @@ export function useSortedFilterKeyItems({ filterKeys, filterValue, flatKeys, + gaveSeerConsent, getFieldDefinition, - gaveSeerConsentRef, hasRawSearchReplacement, includeSuggestions, inputValue, From b23f876e53eac01fc2c6727c7b7600d770e22e34 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 09:27:40 +0200 Subject: [PATCH 19/28] :necktie: Move disable option logic directly onto react event listeners --- .../components/searchQueryBuilder/askSeer.tsx | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 4c4b8e31c7d836..8b2b991b0debd1 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState} from 'react'; +import {useRef, useState} from 'react'; import styled from '@emotion/styled'; import {useOption} from '@react-aria/listbox'; import type {ComboBoxState} from '@react-stately/combobox'; @@ -51,26 +51,9 @@ export function useSeerAcknowledgeMutation() { function AskSeerConsentOption({state}: {state: ComboBoxState}) { const organization = useOrganization(); const itemRef = useRef(null); - const linkRef = useRef(null); const [optionDisableOverride, setOptionDisableOverride] = useState(false); const {mutate: seerAcknowledgeMutate} = useSeerAcknowledgeMutation(); - useEffect(() => { - const link = linkRef.current; - if (!link) return undefined; - - const disableOption = () => setOptionDisableOverride(true); - const enableOption = () => setOptionDisableOverride(false); - - link.addEventListener('mouseover', disableOption); - link.addEventListener('mouseout', enableOption); - - return () => { - link.removeEventListener('mouseover', disableOption); - link.removeEventListener('mouseout', enableOption); - }; - }, []); - const {optionProps, labelProps, isFocused, isPressed} = useOption( { key: ASK_SEER_CONSENT_ITEM_KEY, @@ -111,7 +94,8 @@ function AskSeerConsentOption({state}: {state: ComboBoxState}) { { dataProcessingPolicy: ( setOptionDisableOverride(true)} + onMouseOut={() => setOptionDisableOverride(false)} href="https://docs.sentry.io/product/security/ai-ml-policy/#use-of-identifying-data-for-generative-ai-features" /> ), From 631bc6fcdbc6f662e22827e325c3a23abdc49604 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 09:30:59 +0200 Subject: [PATCH 20/28] :necktie: Check if query is fetching using query key --- .../events/autofix/useOrganizationSeerSetup.tsx | 2 +- .../app/components/searchQueryBuilder/askSeer.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/static/app/components/events/autofix/useOrganizationSeerSetup.tsx b/static/app/components/events/autofix/useOrganizationSeerSetup.tsx index e31ad69e26d77b..697ca8c196712d 100644 --- a/static/app/components/events/autofix/useOrganizationSeerSetup.tsx +++ b/static/app/components/events/autofix/useOrganizationSeerSetup.tsx @@ -17,7 +17,7 @@ interface OrganizationSeerSetupResponse { }; } -function makeOrganizationSeerSetupQueryKey(orgSlug: string): ApiQueryKey { +export function makeOrganizationSeerSetupQueryKey(orgSlug: string): ApiQueryKey { return [`/organizations/${orgSlug}/seer/setup-check/`]; } diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 8b2b991b0debd1..ffe3551a3dbe7c 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -6,7 +6,7 @@ import type {ComboBoxState} from '@react-stately/combobox'; import {promptsUpdate} from 'sentry/actionCreators/prompts'; import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; -import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; +import {makeOrganizationSeerSetupQueryKey} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; @@ -14,7 +14,12 @@ import {IconSeer} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {useIsMutating, useMutation, useQueryClient} from 'sentry/utils/queryClient'; +import { + useIsFetching, + useIsMutating, + useMutation, + useQueryClient, +} from 'sentry/utils/queryClient'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; @@ -149,7 +154,10 @@ export function AskSeer({state}: {state: ComboBoxState}) { mutationKey: [setupCheckQueryKey(organization.slug)], }); - const {isPending: isPendingSetupCheck} = useOrganizationSeerSetup(); + const isPendingSetupCheck = + useIsFetching({ + queryKey: [makeOrganizationSeerSetupQueryKey(organization.slug)], + }) > 0; if (isPendingSetupCheck || isMutating) { return ( From d3013d01c8f2415d6e34e260cee07722e76b5963 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 09:47:08 +0200 Subject: [PATCH 21/28] :bug: Remove extra array layering --- static/app/components/searchQueryBuilder/askSeer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index ffe3551a3dbe7c..0b85a89c84b7d8 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -156,7 +156,7 @@ export function AskSeer({state}: {state: ComboBoxState}) { const isPendingSetupCheck = useIsFetching({ - queryKey: [makeOrganizationSeerSetupQueryKey(organization.slug)], + queryKey: makeOrganizationSeerSetupQueryKey(organization.slug), }) > 0; if (isPendingSetupCheck || isMutating) { From d3e242f5a1a90cf3a3739a2d8cebfa443d27e0b1 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 14 Jul 2025 09:59:39 +0200 Subject: [PATCH 22/28] :necktie: Enable query only if AI features are enabled --- static/app/components/searchQueryBuilder/context.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 96f2007714409c..d9db39fa687166 100644 --- a/static/app/components/searchQueryBuilder/context.tsx +++ b/static/app/components/searchQueryBuilder/context.tsx @@ -82,7 +82,7 @@ export function SearchQueryBuilderProvider({ disallowFreeText, disallowUnsupportedFilters, disallowWildcard, - enableAISearch, + enableAISearch: enableAISearchProp, invalidMessages, initialQuery, fieldDefinitionGetter = getFieldDefinition, @@ -103,7 +103,9 @@ export function SearchQueryBuilderProvider({ const wrapperRef = useRef(null); const actionBarRef = useRef(null); const organization = useOrganization(); - const {setupAcknowledgement} = useOrganizationSeerSetup(); + + const enableAISearch = Boolean(enableAISearchProp) && !organization.hideAiFeatures; + const {setupAcknowledgement} = useOrganizationSeerSetup({enabled: enableAISearch}); const [displaySeerResults, setDisplaySeerResults] = useState(false); @@ -169,7 +171,7 @@ export function SearchQueryBuilderProvider({ disabled, disallowFreeText: Boolean(disallowFreeText), disallowWildcard: Boolean(disallowWildcard), - enableAISearch: Boolean(enableAISearch) && !organization.hideAiFeatures, + enableAISearch, parseQuery, parsedQuery, filterKeySections: filterKeySections ?? [], @@ -205,7 +207,6 @@ export function SearchQueryBuilderProvider({ filterKeySections, getTagValues, handleSearch, - organization.hideAiFeatures, parseQuery, parsedQuery, placeholder, From 3c6942f7d4b1c9a777a5069a1b4273c114e91594 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 15 Jul 2025 09:22:21 +0200 Subject: [PATCH 23/28] :truck: Move seer acknowledgement mutation into it's own file --- .../autofix/useSeerAcknowledgeMutation.tsx | 31 ++++++++++++++ .../components/searchQueryBuilder/askSeer.tsx | 40 +++---------------- .../components/ai/AiSetupDataConsent.tsx | 28 +------------ 3 files changed, 38 insertions(+), 61 deletions(-) create mode 100644 static/app/components/events/autofix/useSeerAcknowledgeMutation.tsx diff --git a/static/app/components/events/autofix/useSeerAcknowledgeMutation.tsx b/static/app/components/events/autofix/useSeerAcknowledgeMutation.tsx new file mode 100644 index 00000000000000..2f61430ba8d271 --- /dev/null +++ b/static/app/components/events/autofix/useSeerAcknowledgeMutation.tsx @@ -0,0 +1,31 @@ +import {promptsUpdate} from 'sentry/actionCreators/prompts'; +import {useMutation, useQueryClient} from 'sentry/utils/queryClient'; +import useApi from 'sentry/utils/useApi'; +import useOrganization from 'sentry/utils/useOrganization'; + +export const setupCheckQueryKey = (orgSlug: string) => + `/organizations/${orgSlug}/seer/setup-check/`; + +export function useSeerAcknowledgeMutation() { + const api = useApi(); + const queryClient = useQueryClient(); + const organization = useOrganization(); + + const {mutate, isPending, isError} = useMutation({ + mutationKey: [setupCheckQueryKey(organization.slug)], + mutationFn: () => { + return promptsUpdate(api, { + organization, + feature: 'seer_autofix_setup_acknowledged', + status: 'dismissed', + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [setupCheckQueryKey(organization.slug)], + }); + }, + }); + + return {mutate, isPending, isError}; +} diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 0b85a89c84b7d8..54bd184e1be113 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -3,10 +3,13 @@ import styled from '@emotion/styled'; import {useOption} from '@react-aria/listbox'; import type {ComboBoxState} from '@react-stately/combobox'; -import {promptsUpdate} from 'sentry/actionCreators/prompts'; import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {makeOrganizationSeerSetupQueryKey} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; +import { + setupCheckQueryKey, + useSeerAcknowledgeMutation, +} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; @@ -14,45 +17,12 @@ import {IconSeer} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; -import { - useIsFetching, - useIsMutating, - useMutation, - useQueryClient, -} from 'sentry/utils/queryClient'; -import useApi from 'sentry/utils/useApi'; +import {useIsFetching, useIsMutating} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; export const ASK_SEER_ITEM_KEY = 'ask_seer'; export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; -const setupCheckQueryKey = (orgSlug: string) => - `/organizations/${orgSlug}/seer/setup-check/`; - -export function useSeerAcknowledgeMutation() { - const api = useApi(); - const queryClient = useQueryClient(); - const organization = useOrganization(); - - const {mutate} = useMutation({ - mutationKey: [setupCheckQueryKey(organization.slug)], - mutationFn: () => { - return promptsUpdate(api, { - organization, - feature: 'seer_autofix_setup_acknowledged', - status: 'dismissed', - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [setupCheckQueryKey(organization.slug)], - }); - }, - }); - - return {mutate}; -} - function AskSeerConsentOption({state}: {state: ComboBoxState}) { const organization = useOrganization(); const itemRef = useRef(null); diff --git a/static/gsApp/components/ai/AiSetupDataConsent.tsx b/static/gsApp/components/ai/AiSetupDataConsent.tsx index b7928ebf0062db..379ccd5d670918 100644 --- a/static/gsApp/components/ai/AiSetupDataConsent.tsx +++ b/static/gsApp/components/ai/AiSetupDataConsent.tsx @@ -4,12 +4,12 @@ import styled from '@emotion/styled'; import autofixSetupImg from 'sentry-images/features/autofix-setup.svg'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; -import {promptsUpdate} from 'sentry/actionCreators/prompts'; import {Alert} from 'sentry/components/core/alert'; import {Button} from 'sentry/components/core/button'; import {Flex} from 'sentry/components/core/layout'; import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup'; import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; +import {useSeerAcknowledgeMutation} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DATA_CATEGORY_INFO} from 'sentry/constants'; @@ -17,7 +17,6 @@ import {IconRefresh, IconSeer} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {DataCategory} from 'sentry/types/core'; -import {useMutation, useQueryClient} from 'sentry/utils/queryClient'; import useApi from 'sentry/utils/useApi'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; @@ -37,7 +36,6 @@ type AiSetupDataConsentProps = { function AiSetupDataConsent({groupId}: AiSetupDataConsentProps) { const api = useApi({persistInFlight: true}); const organization = useOrganization(); - const queryClient = useQueryClient(); const navigate = useNavigate(); const subscription = useSubscription(); @@ -82,29 +80,7 @@ function AiSetupDataConsent({groupId}: AiSetupDataConsentProps) { !isTouchCustomer && !hasSeerButNeedsPayg; - const autofixAcknowledgeMutation = useMutation({ - mutationFn: () => { - return promptsUpdate(api, { - organization, - feature: 'seer_autofix_setup_acknowledged', - status: 'dismissed', - }); - }, - onSuccess: () => { - // Invalidate the appropriate query based on mode - if (isGroupMode && groupId) { - queryClient.invalidateQueries({ - queryKey: [ - `/organizations/${organization.slug}/issues/${groupId}/autofix/setup/`, - ], - }); - } else { - queryClient.invalidateQueries({ - queryKey: [`/organizations/${organization.slug}/seer/setup-check/`], - }); - } - }, - }); + const autofixAcknowledgeMutation = useSeerAcknowledgeMutation(); function handlePurchaseSeer() { navigate(`/settings/billing/checkout/?referrer=ai_setup_data_consent`); From 32bcc5bdbd5b74c41c939cad9ff7ed60d32fead8 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 15 Jul 2025 09:37:15 +0200 Subject: [PATCH 24/28] :bug: Fix import issue --- .../tokens/filterKeyListBox/useFilterKeyListBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx index d9b1a609a1a606..9fffc720daeaec 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx @@ -3,7 +3,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; import type {ComboBoxState} from '@react-stately/combobox'; import type {Node} from '@react-types/shared'; -import {useSeerAcknowledgeMutation} from 'sentry/components/searchQueryBuilder/askSeer'; +import {useSeerAcknowledgeMutation} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import type {CustomComboboxMenu} from 'sentry/components/searchQueryBuilder/tokens/combobox'; import {FilterKeyListBox} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox'; From b5c2e487fe75fcde91b663927e1e2dfb4ac7d745 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 15 Jul 2025 09:44:42 +0200 Subject: [PATCH 25/28] :white_check_mark: Fix tests --- static/app/components/searchQueryBuilder/askSeer.spec.tsx | 6 +++--- static/app/components/searchQueryBuilder/index.spec.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.spec.tsx b/static/app/components/searchQueryBuilder/askSeer.spec.tsx index 43956e4513d4c3..b0ff27b30eb817 100644 --- a/static/app/components/searchQueryBuilder/askSeer.spec.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.spec.tsx @@ -58,7 +58,7 @@ describe('AskSeer', () => { organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, }); - const askSeer = await screen.findByRole('option', {name: 'Ask Seer'}); + const askSeer = await screen.findByRole('option', {name: /Ask Seer/}); expect(askSeer).toBeInTheDocument(); }); @@ -77,7 +77,7 @@ describe('AskSeer', () => { organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, }); - const enableAi = await screen.findByText('Enable Gen AI'); + const enableAi = await screen.findByText(/Enable Gen AI/); expect(enableAi).toBeInTheDocument(); }); @@ -103,7 +103,7 @@ describe('AskSeer', () => { organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, }); - const enableAi = await screen.findByText('Enable Gen AI'); + const enableAi = await screen.findByText(/Enable Gen AI/); expect(enableAi).toBeInTheDocument(); await userEvent.click(enableAi); diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 297d8a18950438..81a451454ebf6a 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -991,7 +991,7 @@ describe('SearchQueryBuilder', function () { await userEvent.click(getLastInput()); await userEvent.type(screen.getByRole('combobox'), 'some free text'); - expect(screen.getByRole('option', {name: 'Enable Gen AI'})).toBeInTheDocument(); + expect(screen.getByRole('option', {name: /Enable Gen AI/})).toBeInTheDocument(); }); it('can add parens by typing', async function () { From 4eb86818d701df371fa71921d5552f8c0cce15d1 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 16 Jul 2025 13:11:20 -0400 Subject: [PATCH 26/28] :white_check_mark: Move all tests to index.spec.tsx --- .../searchQueryBuilder/askSeer.spec.tsx | 126 ------------ .../searchQueryBuilder/index.spec.tsx | 184 +++++++++++++----- 2 files changed, 138 insertions(+), 172 deletions(-) delete mode 100644 static/app/components/searchQueryBuilder/askSeer.spec.tsx diff --git a/static/app/components/searchQueryBuilder/askSeer.spec.tsx b/static/app/components/searchQueryBuilder/askSeer.spec.tsx deleted file mode 100644 index b0ff27b30eb817..00000000000000 --- a/static/app/components/searchQueryBuilder/askSeer.spec.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import {useComboBoxState} from '@react-stately/combobox'; -import {AutofixSetupFixture} from 'sentry-fixture/autofixSetupFixture'; -import {OrganizationFixture} from 'sentry-fixture/organization'; - -import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; - -import {AskSeer} from 'sentry/components/searchQueryBuilder/askSeer'; -import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; -import {FieldKind} from 'sentry/utils/fields'; - -// Mock the useOption hook to avoid the "Unknown list" error -jest.mock('@react-aria/listbox', () => ({ - ...jest.requireActual('@react-aria/listbox'), - useOption: jest.fn(() => ({ - optionProps: { - role: 'option', - 'aria-selected': false, - 'aria-disabled': false, - }, - labelProps: {}, - isFocused: false, - isPressed: false, - })), -})); - -function MockComboBoxComponent() { - const comboBoxState = useComboBoxState({ - items: [{id: 1, name: 'one'}], - disabledKeys: [], - }); - - return ( - Promise.resolve([])} - initialQuery="" - searchSource="" - enableAISearch - > - - - ); -} - -describe('AskSeer', () => { - it('renders ask seer button when user has given consent', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', - body: AutofixSetupFixture({ - setupAcknowledgement: { - orgHasAcknowledged: true, - userHasAcknowledged: true, - }, - }), - }); - - render(, { - organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, - }); - - const askSeer = await screen.findByRole('option', {name: /Ask Seer/}); - expect(askSeer).toBeInTheDocument(); - }); - - it('renders enable ai button when user has not given consent', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', - body: AutofixSetupFixture({ - setupAcknowledgement: { - orgHasAcknowledged: false, - userHasAcknowledged: false, - }, - }), - }); - - render(, { - organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, - }); - - const enableAi = await screen.findByText(/Enable Gen AI/); - expect(enableAi).toBeInTheDocument(); - }); - - describe('user clicks on enable gen ai button', () => { - it('calls promptsUpdate', async () => { - const organization = OrganizationFixture(); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', - body: AutofixSetupFixture({ - setupAcknowledgement: { - orgHasAcknowledged: false, - userHasAcknowledged: false, - }, - }), - }); - - const promptsUpdateMock = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - method: 'PUT', - }); - - render(, { - organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, - }); - - const enableAi = await screen.findByText(/Enable Gen AI/); - expect(enableAi).toBeInTheDocument(); - - await userEvent.click(enableAi); - - await waitFor(() => { - expect(promptsUpdateMock).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - data: { - feature: 'seer_autofix_setup_acknowledged', - organization_id: organization.id, - project_id: undefined, - status: 'dismissed', - }, - }) - ); - }); - }); - }); -}); diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 81a451454ebf6a..50b2e2e35cf40a 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -1,6 +1,7 @@ import type {ComponentProps} from 'react'; import {destroyAnnouncer} from '@react-aria/live-announcer'; import {AutofixSetupFixture} from 'sentry-fixture/autofixSetupFixture'; +import {OrganizationFixture} from 'sentry-fixture/organization'; import { act, @@ -948,52 +949,6 @@ describe('SearchQueryBuilder', function () { expect(screen.getByRole('row', {name: 'browser.name:foo'})).toBeInTheDocument(); }); - it('displays ask seer button when searching free text', async function () { - const mockOnSearch = jest.fn(); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', - body: AutofixSetupFixture({ - setupAcknowledgement: { - orgHasAcknowledged: true, - userHasAcknowledged: true, - }, - }), - }); - - render( - , - {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} - ); - - await userEvent.click(getLastInput()); - await userEvent.type(screen.getByRole('combobox'), 'some free text'); - - expect(screen.getByRole('option', {name: /Ask Seer/i})).toBeInTheDocument(); - }); - - it('displays enable ai button when searching free text and user has not given consent', async function () { - const mockOnSearch = jest.fn(); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', - body: AutofixSetupFixture({ - setupAcknowledgement: { - orgHasAcknowledged: false, - userHasAcknowledged: false, - }, - }), - }); - - render( - , - {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} - ); - - await userEvent.click(getLastInput()); - await userEvent.type(screen.getByRole('combobox'), 'some free text'); - - expect(screen.getByRole('option', {name: /Enable Gen AI/})).toBeInTheDocument(); - }); - it('can add parens by typing', async function () { const mockOnChange = jest.fn(); render(); @@ -4477,4 +4432,141 @@ describe('SearchQueryBuilder', function () { ).toBeInTheDocument(); }); }); + + describe('ask seer', function () { + it('renders ask seer button when user has given consent', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: true, + userHasAcknowledged: true, + }, + }), + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + await userEvent.click(getLastInput()); + + const askSeer = await screen.findByRole('option', {name: /Ask Seer/}); + expect(askSeer).toBeInTheDocument(); + }); + + it('renders enable ai button when user has not given consent', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + await userEvent.click(getLastInput()); + + const enableAi = await screen.findByText(/Enable Gen AI/); + expect(enableAi).toBeInTheDocument(); + }); + + describe('user clicks on enable gen ai button', () => { + it('calls promptsUpdate', async () => { + const organization = OrganizationFixture(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + const promptsUpdateMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/prompts-activity/`, + method: 'PUT', + }); + + render(, { + organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, + }); + + await userEvent.click(getLastInput()); + + const enableAi = await screen.findByText(/Enable Gen AI/); + expect(enableAi).toBeInTheDocument(); + + await userEvent.click(enableAi); + + await waitFor(() => { + expect(promptsUpdateMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + data: { + feature: 'seer_autofix_setup_acknowledged', + organization_id: organization.id, + project_id: undefined, + status: 'dismissed', + }, + }) + ); + }); + }); + }); + + describe('free text', function () { + it('displays ask seer button when searching free text', async function () { + const mockOnSearch = jest.fn(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: true, + userHasAcknowledged: true, + }, + }), + }); + + render( + , + {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} + ); + + await userEvent.click(getLastInput()); + await userEvent.type(screen.getByRole('combobox'), 'some free text'); + + expect(screen.getByRole('option', {name: /Ask Seer/i})).toBeInTheDocument(); + }); + }); + + it('displays enable ai button when searching free text and user has not given consent', async function () { + const mockOnSearch = jest.fn(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + render( + , + {organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}} + ); + + await userEvent.click(getLastInput()); + await userEvent.type(screen.getByRole('combobox'), 'some free text'); + + expect(screen.getByRole('option', {name: /Enable Gen AI/})).toBeInTheDocument(); + }); + }); }); From 6cdaf88ead86fe2f7db1a4dc1ab766c145737e3b Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 16 Jul 2025 14:10:05 -0400 Subject: [PATCH 27/28] :necktie: Remove onClick handler, and move to using theme spacing --- .../components/searchQueryBuilder/askSeer.tsx | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/static/app/components/searchQueryBuilder/askSeer.tsx b/static/app/components/searchQueryBuilder/askSeer.tsx index 54bd184e1be113..2475a4f0286ecc 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -6,16 +6,12 @@ import type {ComboBoxState} from '@react-stately/combobox'; import {FeatureBadge} from 'sentry/components/core/badge/featureBadge'; import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {makeOrganizationSeerSetupQueryKey} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; -import { - setupCheckQueryKey, - useSeerAcknowledgeMutation, -} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation'; +import {setupCheckQueryKey} from 'sentry/components/events/autofix/useSeerAcknowledgeMutation'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {IconSeer} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useIsFetching, useIsMutating} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; @@ -24,10 +20,8 @@ export const ASK_SEER_ITEM_KEY = 'ask_seer'; export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent'; function AskSeerConsentOption({state}: {state: ComboBoxState}) { - const organization = useOrganization(); const itemRef = useRef(null); const [optionDisableOverride, setOptionDisableOverride] = useState(false); - const {mutate: seerAcknowledgeMutate} = useSeerAcknowledgeMutation(); const {optionProps, labelProps, isFocused, isPressed} = useOption( { @@ -41,28 +35,15 @@ function AskSeerConsentOption({state}: {state: ComboBoxState}) { itemRef ); - const handleClick = () => { - trackAnalytics('trace.explorer.ai_query_interface', { - organization, - action: 'consent_accepted', - }); - seerAcknowledgeMutate(); - }; - return ( - + -
+ {t('Enable Gen AI')} -
+ {tct( 'Query assistant requires Generative AI which is subject to our [dataProcessingPolicy:data processing policy].', @@ -188,7 +169,7 @@ const AskSeerListItem = styled('div')<{justifyContent?: 'flex-start' | 'space-be display: flex; align-items: center; width: 100%; - padding: ${space(1)} ${space(1.5)}; + padding: ${p => p.theme.space.md} ${p => p.theme.space.lg}; background: transparent; border-radius: 0; background-color: none; @@ -198,7 +179,7 @@ const AskSeerListItem = styled('div')<{justifyContent?: 'flex-start' | 'space-be font-weight: ${p => p.theme.fontWeight.bold}; text-align: left; justify-content: ${p => p.justifyContent ?? 'flex-start'}; - gap: ${space(1)}; + gap: ${p => p.theme.space.md}; list-style: none; margin: 0; @@ -220,6 +201,12 @@ const AskSeerLabel = styled('span')<{width?: 'auto'}>` font-weight: ${p => p.theme.fontWeight.bold}; display: flex; align-items: center; - gap: ${space(1)}; + gap: ${p => p.theme.space.md}; width: ${p => p.width}; `; + +const AskSeerConsentLabelWrapper = styled('div')` + display: flex; + align-items: center; + gap: ${p => p.theme.space.md}; +`; From bb15ff302df85171decdbe1bfb86459e586d24ef Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 16 Jul 2025 15:06:51 -0400 Subject: [PATCH 28/28] :white_check_mark: Fix up broken test ... in an interesting way --- .../searchQueryBuilder/index.spec.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 50b2e2e35cf40a..fcc4d19c6e5d8d 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -4478,9 +4478,16 @@ describe('SearchQueryBuilder', function () { describe('user clicks on enable gen ai button', () => { it('calls promptsUpdate', async () => { - const organization = OrganizationFixture(); + const organization = OrganizationFixture({ + slug: 'org-slug', + features: ['gen-ai-features', 'gen-ai-explore-traces'], + }); + const promptsUpdateMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/prompts-activity/`, + method: 'PUT', + }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/seer/setup-check/', + url: `/organizations/${organization.slug}/seer/setup-check/`, body: AutofixSetupFixture({ setupAcknowledgement: { orgHasAcknowledged: false, @@ -4489,21 +4496,15 @@ describe('SearchQueryBuilder', function () { }), }); - const promptsUpdateMock = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/prompts-activity/`, - method: 'PUT', - }); - - render(, { - organization: {features: ['gen-ai-features', 'gen-ai-explore-traces']}, - }); + render(, {organization}); await userEvent.click(getLastInput()); - const enableAi = await screen.findByText(/Enable Gen AI/); + const enableAi = await screen.findByRole('option', {name: /Enable Gen AI/}); expect(enableAi).toBeInTheDocument(); - await userEvent.click(enableAi); + await userEvent.hover(enableAi); + await userEvent.keyboard('{enter}'); await waitFor(() => { expect(promptsUpdateMock).toHaveBeenCalledWith(