Skip to content

feat(search-bar): Add consent flow to Ask Seer #95406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fdc20f3
:necktie: Update SpansTabWrapper to use new useTraceExploreAiQuerySet…
nsdeschenes Jul 3, 2025
9bde3de
:necktie: Update SpanTabSearchSection to conditionally enable AI sear…
nsdeschenes Jul 3, 2025
2261496
:necktie: Conditionally call trace explorer ai setup endpoint and mov…
nsdeschenes Jul 3, 2025
e671acb
:necktie: Add in new createAskSeerConsentItem function
nsdeschenes Jul 12, 2025
b16e566
:fire: Remove genAIConsent field as its not needed
nsdeschenes Jul 12, 2025
cc247ca
:necktie: Update enableAISearch logic in SearchQueryBuilderProvider t…
nsdeschenes Jul 12, 2025
6efa20c
:necktie: Move all AskSeer related items into askSeer.tsx
nsdeschenes Jul 12, 2025
884eb10
:necktie: Add consent_accepted action to ai_query_interface analytics
nsdeschenes Jul 13, 2025
dc1edcf
:necktie: Add gaveSeerConsentRef to SearchQueryBuilderContext
nsdeschenes Jul 13, 2025
cb8e640
:necktie: Ignore ask-seer-consent when freeText onOptionSelected call…
nsdeschenes Jul 13, 2025
02c78b8
:necktie: Add ask-seer-consent items to lists
nsdeschenes Jul 13, 2025
67ec4ac
:white_check_mark: Update search query builder tests
nsdeschenes Jul 13, 2025
bec4db5
:necktie: Add in AskSeerConsent flow to askSeer.tsx
nsdeschenes Jul 13, 2025
b80b69e
:white_check_mark: Add in tests for askSeer
nsdeschenes Jul 13, 2025
fbfcd52
:necktie: Move acknowledgement mutation into it's own custom hook
nsdeschenes Jul 14, 2025
ee0894d
:necktie: Conditionally add options to hidden items to resolve disall…
nsdeschenes Jul 14, 2025
f69c2ef
:necktie: Utilize acknowledgement hook when selecting ask seer option…
nsdeschenes Jul 14, 2025
27152e9
:necktie: Grab consent status directly in context and pass down
nsdeschenes Jul 14, 2025
b23f876
:necktie: Move disable option logic directly onto react event listeners
nsdeschenes Jul 14, 2025
631bc6f
:necktie: Check if query is fetching using query key
nsdeschenes Jul 14, 2025
d3013d0
:bug: Remove extra array layering
nsdeschenes Jul 14, 2025
d3e242f
:necktie: Enable query only if AI features are enabled
nsdeschenes Jul 14, 2025
3c6942f
:truck: Move seer acknowledgement mutation into it's own file
nsdeschenes Jul 15, 2025
32bcc5b
:bug: Fix import issue
nsdeschenes Jul 15, 2025
b5c2e48
:white_check_mark: Fix tests
nsdeschenes Jul 15, 2025
4eb8681
:white_check_mark: Move all tests to index.spec.tsx
nsdeschenes Jul 16, 2025
6cdaf88
:necktie: Remove onClick handler, and move to using theme spacing
nsdeschenes Jul 16, 2025
bb15ff3
:white_check_mark: Fix up broken test ... in an interesting way
nsdeschenes Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface OrganizationSeerSetupResponse {
};
}

function makeOrganizationSeerSetupQueryKey(orgSlug: string): ApiQueryKey {
export function makeOrganizationSeerSetupQueryKey(orgSlug: string): ApiQueryKey {
return [`/organizations/${orgSlug}/seer/setup-check/`];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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};
}
172 changes: 164 additions & 8 deletions static/app/components/searchQueryBuilder/askSeer.tsx
Original file line number Diff line number Diff line change
@@ -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<T>({state}: {state: ComboBoxState<T>}) {
const itemRef = useRef<HTMLDivElement>(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 (
<AskSeerListItem ref={itemRef} {...optionProps} justifyContent="space-between">
<InteractionStateLayer isHovered={isFocused} isPressed={isPressed} />
<AskSeerConsentLabelWrapper>
<IconSeer />
<AskSeerLabel {...labelProps}>
{t('Enable Gen AI')} <FeatureBadge type="beta" />
</AskSeerLabel>
</AskSeerConsentLabelWrapper>
<SeerConsentText>
{tct(
'Query assistant requires Generative AI which is subject to our [dataProcessingPolicy:data processing policy].',
{
dataProcessingPolicy: (
<TooltipSubExternalLink
onMouseOver={() => setOptionDisableOverride(true)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you disabling to prevent the click here from propagating to the option?

In other areas, I've added stopPropagation to the necessary handlers to prevent the click from going through:

<TrailingWrap
onPointerUp={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>

Copy link
Contributor Author

@nsdeschenes nsdeschenes Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. The reason why this is needed is because of the logic that lives inside these lines: https://github.com/getsentry/sentry/blob/nd/AIML-704/feat-add-consent-flow-to-trace-ask-seer/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx#L388-L404, there doesn't seem to be any way to access the event in this handler, so can't call stopPropagation, there it would seem

As the onClick handler in askSeer.tsx isn't used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can still add onMouseUp={e => e.stopPropagation()} (and others if necessary) to the <TooltipSubExternalLink> though right? That would prevent the option from being selected when you click the link

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that doesn't work annoyingly, it's still triggering the option via useFilterKeyListBox

onMouseOut={() => setOptionDisableOverride(false)}
href="https://docs.sentry.io/product/security/ai-ml-policy/#use-of-identifying-data-for-generative-ai-features"
/>
),
}
)}
</SeerConsentText>
</AskSeerListItem>
);
}

function AskSeerOption<T>({state}: {state: ComboBoxState<T>}) {
const ref = useRef<HTMLDivElement>(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 (
<AskSeerListItem ref={ref} onClick={handleClick} {...optionProps}>
<InteractionStateLayer isHovered={isFocused} isPressed={isPressed} />
<IconSeer />
<AskSeerLabel {...labelProps}>
{t('Ask Seer')} <FeatureBadge type="beta" />
</AskSeerLabel>
</AskSeerListItem>
);
}

export function AskSeer<T>({state}: {state: ComboBoxState<T>}) {
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 (
<AskSeerPane>
<AskSeerListItem>
<AskSeerLabel width="auto">{t('Loading Seer')}</AskSeerLabel>
<LoadingIndicator size={16} style={{margin: 0}} />
</AskSeerListItem>
</AskSeerPane>
);
}

if (gaveSeerConsent) {
return (
<AskSeerPane>
<AskSeerOption state={state} />
</AskSeerPane>
);
}

return (
<AskSeerPane>
<AskSeerConsentOption state={state} />
</AskSeerPane>
);
}

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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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};
`;
18 changes: 11 additions & 7 deletions static/app/components/searchQueryBuilder/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string[]>;
Expand Down Expand Up @@ -81,7 +82,7 @@ export function SearchQueryBuilderProvider({
disallowFreeText,
disallowUnsupportedFilters,
disallowWildcard,
enableAISearch,
enableAISearch: enableAISearchProp,
invalidMessages,
initialQuery,
fieldDefinitionGetter = getFieldDefinition,
Expand All @@ -101,9 +102,12 @@ export function SearchQueryBuilderProvider({
}: SearchQueryBuilderProps & {children: React.ReactNode}) {
const wrapperRef = useRef<HTMLDivElement>(null);
const actionBarRef = useRef<HTMLDivElement>(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,
Expand Down Expand Up @@ -167,8 +171,7 @@ export function SearchQueryBuilderProvider({
disabled,
disallowFreeText: Boolean(disallowFreeText),
disallowWildcard: Boolean(disallowWildcard),
enableAISearch: Boolean(enableAISearch),
genAIConsent,
enableAISearch,
parseQuery,
parsedQuery,
filterKeySections: filterKeySections ?? [],
Expand All @@ -190,6 +193,7 @@ export function SearchQueryBuilderProvider({
setDisplaySeerResults,
replaceRawSearchKeys,
filterKeyAliases,
gaveSeerConsent: setupAcknowledgement.orgHasAcknowledged,
};
}, [
disabled,
Expand All @@ -201,7 +205,6 @@ export function SearchQueryBuilderProvider({
filterKeyAliases,
filterKeyMenuWidth,
filterKeySections,
genAIConsent,
getTagValues,
handleSearch,
parseQuery,
Expand All @@ -211,6 +214,7 @@ export function SearchQueryBuilderProvider({
recentSearches,
replaceRawSearchKeys,
searchSource,
setupAcknowledgement.orgHasAcknowledged,
size,
stableFieldDefinitionGetter,
stableFilterKeys,
Expand Down
Loading
Loading