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/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 c483b4c24786ce..2475a4f0286ecc 100644 --- a/static/app/components/searchQueryBuilder/askSeer.tsx +++ b/static/app/components/searchQueryBuilder/askSeer.tsx @@ -1,10 +1,159 @@ +import {useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {useOption} from '@react-aria/listbox'; +import type {ComboBoxState} from '@react-stately/combobox'; -import {space} from 'sentry/styles/space'; +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} 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 {trackAnalytics} from 'sentry/utils/analytics'; +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'; -export const AskSeerPane = styled('div')` +function AskSeerConsentOption({state}: {state: ComboBoxState}) { + const itemRef = useRef(null); + const [optionDisableOverride, setOptionDisableOverride] = useState(false); + + 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 + ); + + return ( + + + + + + {t('Enable Gen AI')} + + + + {tct( + 'Query assistant requires Generative AI which is subject to our [dataProcessingPolicy:data processing policy].', + { + dataProcessingPolicy: ( + setOptionDisableOverride(true)} + onMouseOut={() => setOptionDisableOverride(false)} + href="https://docs.sentry.io/product/security/ai-ml-policy/#use-of-identifying-data-for-generative-ai-features" + /> + ), + } + )} + + + ); +} + +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, + isDisabled: false, + }, + 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}) { + const organization = useOrganization(); + const {gaveSeerConsent} = useSearchQueryBuilder(); + const isMutating = useIsMutating({ + mutationKey: [setupCheckQueryKey(organization.slug)], + }); + + const isPendingSetupCheck = + useIsFetching({ + queryKey: makeOrganizationSeerSetupQueryKey(organization.slug), + }) > 0; + + if (isPendingSetupCheck || isMutating) { + return ( + + + {t('Loading Seer')} + + + + ); + } + + if (gaveSeerConsent) { + 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; align-items: center; @@ -15,12 +164,12 @@ export const AskSeerPane = styled('div')` width: 100%; `; -export const AskSeerListItem = styled('div')` +const AskSeerListItem = styled('div')<{justifyContent?: 'flex-start' | 'space-between'}>` position: relative; 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; @@ -29,8 +178,8 @@ export 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; - gap: ${space(1)}; + justify-content: ${p => p.justifyContent ?? 'flex-start'}; + gap: ${p => p.theme.space.md}; list-style: none; margin: 0; @@ -45,12 +194,19 @@ export const AskSeerListItem = styled('div')` } `; -export 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}; 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}; `; diff --git a/static/app/components/searchQueryBuilder/context.tsx b/static/app/components/searchQueryBuilder/context.tsx index 72510609fa6c1e..d9db39fa687166 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; - genAIConsent: boolean; + gaveSeerConsent: boolean; getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null; getSuggestedFilterKey: (key: string) => string | null; getTagValues: (tag: Tag, query: string) => Promise; @@ -81,7 +82,7 @@ export function SearchQueryBuilderProvider({ disallowFreeText, disallowUnsupportedFilters, disallowWildcard, - enableAISearch, + enableAISearch: enableAISearchProp, invalidMessages, initialQuery, fieldDefinitionGetter = getFieldDefinition, @@ -101,9 +102,12 @@ export function SearchQueryBuilderProvider({ }: SearchQueryBuilderProps & {children: React.ReactNode}) { const wrapperRef = useRef(null); const actionBarRef = useRef(null); - const [displaySeerResults, setDisplaySeerResults] = useState(false); const organization = useOrganization(); - const genAIConsent = organization?.genAIConsent ?? false; + + const enableAISearch = Boolean(enableAISearchProp) && !organization.hideAiFeatures; + const {setupAcknowledgement} = useOrganizationSeerSetup({enabled: enableAISearch}); + + const [displaySeerResults, setDisplaySeerResults] = useState(false); const {state, dispatch} = useQueryBuilderState({ initialQuery, @@ -167,8 +171,7 @@ export function SearchQueryBuilderProvider({ disabled, disallowFreeText: Boolean(disallowFreeText), disallowWildcard: Boolean(disallowWildcard), - enableAISearch: Boolean(enableAISearch), - genAIConsent, + enableAISearch, parseQuery, parsedQuery, filterKeySections: filterKeySections ?? [], @@ -190,6 +193,7 @@ export function SearchQueryBuilderProvider({ setDisplaySeerResults, replaceRawSearchKeys, filterKeyAliases, + gaveSeerConsent: setupAcknowledgement.orgHasAcknowledged, }; }, [ disabled, @@ -201,7 +205,6 @@ export function SearchQueryBuilderProvider({ filterKeyAliases, filterKeyMenuWidth, filterKeySections, - genAIConsent, getTagValues, handleSearch, parseQuery, @@ -211,6 +214,7 @@ export function SearchQueryBuilderProvider({ recentSearches, replaceRawSearchKeys, searchSource, + setupAcknowledgement.orgHasAcknowledged, size, stableFieldDefinitionGetter, stableFilterKeys, diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 790b9e7c56a815..fcc4d19c6e5d8d 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -1,5 +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, @@ -947,19 +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(); - 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('can add parens by typing', async function () { const mockOnChange = jest.fn(); render(); @@ -4443,4 +4432,142 @@ 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({ + 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/${organization.slug}/seer/setup-check/`, + body: AutofixSetupFixture({ + setupAcknowledgement: { + orgHasAcknowledged: false, + userHasAcknowledged: false, + }, + }), + }); + + render(, {organization}); + + await userEvent.click(getLastInput()); + + const enableAi = await screen.findByRole('option', {name: /Enable Gen AI/}); + expect(enableAi).toBeInTheDocument(); + + await userEvent.hover(enableAi); + await userEvent.keyboard('{enter}'); + + 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(); + }); + }); }); diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index 71dc9d6bf2451e..6ae45baec674f4 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,13 +28,11 @@ 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_CONSENT_ITEM_KEY, ASK_SEER_ITEM_KEY, - AskSeerLabel, - AskSeerListItem, - AskSeerPane, + AskSeer, } from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/tokens/useSearchTokenCombobox'; @@ -44,12 +41,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'; @@ -190,19 +183,38 @@ function useHiddenItems>({ maxOptions?: number; shouldFilterResults?: boolean; }) { + const {gaveSeerConsent} = useSearchQueryBuilder(); const hiddenOptions: Set = useMemo(() => { const options = getHiddenOptions( items, shouldFilterResults ? filterValue : '', maxOptions ); - return showAskSeerOption ? options.add(ASK_SEER_ITEM_KEY) : options; - }, [items, shouldFilterResults, filterValue, maxOptions, showAskSeerOption]); + + if (showAskSeerOption) { + if (gaveSeerConsent) { + options.add(ASK_SEER_ITEM_KEY); + } else { + options.add(ASK_SEER_CONSENT_ITEM_KEY); + } + } + + return options; + }, [ + filterValue, + gaveSeerConsent, + items, + maxOptions, + shouldFilterResults, + 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]); @@ -260,41 +272,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 +326,7 @@ function OverlayContent>({ /> {enableAISearch ? ( - - - + ) : null} @@ -669,7 +644,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 e2919e55306410..21837755466144 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 { @@ -17,10 +16,9 @@ import type { import InteractionStateLayer from 'sentry/components/core/interactionStateLayer'; import {Overlay} from 'sentry/components/overlay'; import { + ASK_SEER_CONSENT_ITEM_KEY, ASK_SEER_ITEM_KEY, - AskSeerLabel, - AskSeerListItem, - AskSeerPane, + AskSeer, } from 'sentry/components/searchQueryBuilder/askSeer'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import type {CustomComboboxMenuProps} from 'sentry/components/searchQueryBuilder/tokens/combobox'; @@ -32,12 +30,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 +140,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 +231,7 @@ function FilterKeyMenuContent>({ {enableAISearch ? ( - - - + ) : null} {showRecentFilters ? ( @@ -361,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/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/useFilterKeyListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx index f0b46e0e847da0..9fffc720daeaec 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/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'; @@ -16,6 +17,7 @@ import {useRecentSearchFilters} from 'sentry/components/searchQueryBuilder/token import { ALL_CATEGORY, ALL_CATEGORY_VALUE, + createAskSeerConsentItem, createAskSeerItem, createRecentFilterItem, createRecentFilterOptionKey, @@ -162,8 +164,13 @@ function useFilterKeySections({ return {sections, selectedSection, setSelectedSection}; } export function useFilterKeyListBox({filterValue}: {filterValue: string}) { - const {filterKeys, getFieldDefinition, setDisplaySeerResults, enableAISearch} = - useSearchQueryBuilder(); + const { + filterKeys, + getFieldDefinition, + setDisplaySeerResults, + enableAISearch, + gaveSeerConsent, + } = useSearchQueryBuilder(); const {sectionedItems} = useFilterKeyItems(); const recentFilters = useRecentSearchFilters(); const {data: recentSearches} = useRecentSearches(); @@ -176,7 +183,12 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { const filterKeyMenuItems = useMemo(() => { const recentFilterItems = makeRecentFilterItems({recentFilters}); - const askSeerItem = enableAISearch ? [createAskSeerItem()] : []; + const askSeerItem = []; + if (enableAISearch) { + askSeerItem.push( + gaveSeerConsent ? createAskSeerItem() : createAskSeerConsentItem() + ); + } if (selectedSection === RECENT_SEARCH_CATEGORY_VALUE) { return [ @@ -205,6 +217,7 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { }, [ enableAISearch, filterKeys, + gaveSeerConsent, getFieldDefinition, recentFilters, recentSearches, @@ -368,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') { @@ -378,8 +393,17 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) { setDisplaySeerResults(true); return; } + + if (option.type === 'ask-seer-consent') { + trackAnalytics('trace.explorer.ai_query_interface', { + organization, + action: 'consent_accepted', + }); + seerAcknowledgeMutate(); + return; + } }, - [organization, setDisplaySeerResults] + [organization, seerAcknowledgeMutate, setDisplaySeerResults] ); return { 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; 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; } } diff --git a/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx b/static/app/components/searchQueryBuilder/tokens/useSortedFilterKeyItems.tsx index 254f1acf5eb8a1..4e56aa3bedf631 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, + gaveSeerConsent, } = useSearchQueryBuilder(); const organization = useOrganization(); @@ -245,6 +247,13 @@ export function useSortedFilterKeyItems({ type: 'section', }; + const askSeerItem = []; + if (enableAISearch) { + askSeerItem.push( + gaveSeerConsent ? createAskSeerItem() : createAskSeerConsentItem() + ); + } + const {shouldShowAtTop, suggestedFiltersSection} = getValueSuggestionsFromSearchResult(searched); @@ -254,7 +263,7 @@ export function useSortedFilterKeyItems({ ...(shouldIncludeRawSearch ? [rawSearchSection] : []), keyItemsSection, ...(!shouldShowAtTop && suggestedFiltersSection ? [suggestedFiltersSection] : []), - ...(enableAISearch ? [createAskSeerItem()] : []), + ...askSeerItem, ]; } @@ -266,6 +275,7 @@ export function useSortedFilterKeyItems({ filterKeys, filterValue, flatKeys, + gaveSeerConsent, getFieldDefinition, hasRawSearchReplacement, includeSuggestions, 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/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; 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`); 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,