Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
135 changes: 97 additions & 38 deletions static/app/components/performance/spanSearchQueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import type {CaseInsensitive} from 'sentry/components/searchQueryBuilder/hooks';
import type {CallbackSearchState} from 'sentry/components/searchQueryBuilder/types';
import type {PageFilters} from 'sentry/types/core';
import type {TagCollection} from 'sentry/types/group';
import {type AggregationKey} from 'sentry/utils/fields';
import {FieldKind, type AggregationKey} from 'sentry/utils/fields';
import {prettifyAttributeName} from 'sentry/views/explore/components/traceItemAttributes/utils';
import {
useTraceItemSearchQueryBuilderProps,
type TraceItemSearchQueryBuilderProps,
} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
import {useSpanItemAttributes} from 'sentry/views/explore/hooks/useTraceItemAttributes';
import {TraceItemDataset} from 'sentry/views/explore/types';
import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions';
import {SpanFields} from 'sentry/views/insights/types';

export interface UseSpanSearchQueryBuilderProps {
Expand All @@ -32,6 +34,7 @@ export interface UseSpanSearchQueryBuilderProps {
projects?: PageFilters['projects'];
supportedAggregates?: AggregationKey[];
useEap?: boolean;
validatedSearchQueryData?: EventValidationData;
}
export interface SpanSearchQueryBuilderProps extends UseSpanSearchQueryBuilderProps {
booleanAttributes: TagCollection;
Expand All @@ -51,64 +54,120 @@ export function useSpanSearchQueryBuilderProps(props: UseSpanSearchQueryBuilderP
spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps;
spanSearchQueryBuilderProviderProps: UseTraceItemSearchQueryBuilderPropsReturnType;
} {
const {attributes: numberAttributes, secondaryAliases: numberSecondaryAliases} =
const {attributes: spanBooleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useSpanItemAttributes({}, 'boolean');
const {attributes: spanNumberAttributes, secondaryAliases: numberSecondaryAliases} =
useSpanItemAttributes({}, 'number');
const {attributes: stringAttributes, secondaryAliases: stringSecondaryAliases} =
const {attributes: spanStringAttributes, secondaryAliases: stringSecondaryAliases} =
useSpanItemAttributes({}, 'string');
const {attributes: booleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useSpanItemAttributes({}, 'boolean');

const stringAttributesWithSemver = useMemo(() => {
if (SpanFields.RELEASE in stringAttributes) {
const spanStringAttributesWithSemver = useMemo(() => {
if (SpanFields.RELEASE in spanStringAttributes) {
return {
...stringAttributes,
...spanStringAttributes,
...STATIC_SEMVER_TAGS,
};
}
return stringAttributes;
}, [stringAttributes]);
return spanStringAttributes;
}, [spanStringAttributes]);

const spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps = useMemo(
() => ({
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes: stringAttributesWithSemver,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
}),
[
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
numberSecondaryAliases,
props,
stringAttributesWithSemver,
stringSecondaryAliases,
]
);
const {booleanAttributes, numberAttributes, stringAttributes, invalidFilterKeys} =
useMemo(() => {
const localInvalidFilterKeys: string[] = [];
const localBooleanAttributes = {...spanBooleanAttributes};
const localNumberAttributes = {...spanNumberAttributes};
const localStringAttributes = {...spanStringAttributesWithSemver};

if (props.validatedSearchQueryData?.query.fields.length) {
for (const item of props.validatedSearchQueryData.query.fields) {
if (item.valid) {
if (item.attrType === 'boolean' && item.name) {
localBooleanAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.BOOLEAN,
};
}

if (item.attrType === 'number' && item.name) {
localNumberAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.MEASUREMENT,
};
}

if (item.attrType === 'string' && item.name) {
localStringAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.TAG,
};
}

continue;
}

if (item.name) {
localInvalidFilterKeys.push(item.name);
}
}
}

return {
booleanAttributes: localBooleanAttributes,
numberAttributes: localNumberAttributes,
stringAttributes: localStringAttributes,
invalidFilterKeys: localInvalidFilterKeys,
};
}, [
props.validatedSearchQueryData?.query.fields,
spanBooleanAttributes,
spanNumberAttributes,
spanStringAttributesWithSemver,
]);

const spanSearchQueryBuilderProviderProps = useTraceItemSearchQueryBuilderProps({
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes: stringAttributesWithSemver,
stringAttributes,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
onCaseInsensitiveClick: props.onCaseInsensitiveClick,
invalidFilterKeys,
});

return useMemo(
() => ({
return useMemo(() => {
const spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps = {
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
invalidFilterKeys,
};

return {
spanSearchQueryBuilderProps,
spanSearchQueryBuilderProviderProps,
}),
[spanSearchQueryBuilderProps, spanSearchQueryBuilderProviderProps]
);
};
}, [
booleanAttributes,
booleanSecondaryAliases,
invalidFilterKeys,
numberAttributes,
numberSecondaryAliases,
props,
spanSearchQueryBuilderProviderProps,
stringAttributes,
stringSecondaryAliases,
]);
}
89 changes: 14 additions & 75 deletions static/app/components/searchQueryBuilder/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,21 @@ import {
useCallback,
useContext,
useEffect,
useId,
useMemo,
useRef,
useState,
type Dispatch,
} from 'react';
import * as Sentry from '@sentry/react';
import {
queryOptions,
useQuery,
useQueryClient,
type QueryKey,
} from '@tanstack/react-query';
import {useQuery, type QueryKey} from '@tanstack/react-query';

import type {
GetTagKeys,
GetTagValues,
SearchQueryBuilderProps,
} from 'sentry/components/searchQueryBuilder';
import type {CaseInsensitive} from 'sentry/components/searchQueryBuilder/hooks';
import {useFilterKeyRegistry} from 'sentry/components/searchQueryBuilder/hooks/useFilterKeyRegistry';
import {useHandleSearch} from 'sentry/components/searchQueryBuilder/hooks/useHandleSearch';
import {
useQueryBuilderState,
Expand All @@ -35,7 +30,7 @@ import type {
} from 'sentry/components/searchQueryBuilder/types';
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
import type {ParseResult} from 'sentry/components/searchSyntax/parser';
import type {SavedSearchType, Tag, TagCollection} from 'sentry/types/group';
import type {SavedSearchType, TagCollection} from 'sentry/types/group';
import {defined} from 'sentry/utils/defined';
import {getFieldDefinition as defaultGetFieldDefinition} from 'sentry/utils/fields';
import {isEmptyObject} from 'sentry/utils/object/isEmptyObject';
Expand Down Expand Up @@ -149,18 +144,6 @@ export function useHasSearchQueryBuilderProvider() {
const defaultFieldDefinitionGetter: FieldDefinitionGetter = key =>
defaultGetFieldDefinition(key);

function getEmptyFilterKeyRegistry(): TagCollection {
return {};
}

function filterKeyRegistryOptions(queryKey: QueryKey) {
return queryOptions({
queryKey,
queryFn: getEmptyFilterKeyRegistry,
staleTime: Infinity,
});
}

const SearchQueryBuilderStateContext =
createContext<SearchQueryBuilderStateContextData | null>(null);
const SearchQueryBuilderConfigContext =
Expand Down Expand Up @@ -209,8 +192,6 @@ export function SearchQueryBuilderProvider({
}: SearchQueryBuilderProps & {children: React.ReactNode}) {
const wrapperRef = useRef<HTMLDivElement>(null);
const actionBarRef = useRef<HTMLDivElement>(null);
const fallbackRegistryId = useId();
const queryClient = useQueryClient();

const [autoSubmitSeer, setAutoSubmitSeer] = useState(false);
const [displayAskSeerFeedback, setDisplayAskSeerFeedback] = useState(false);
Expand All @@ -228,55 +209,11 @@ export function SearchQueryBuilderProvider({
const [displayAskSeerState, setDisplayAskSeerState] = useState(false);
const displayAskSeer = enableAISearch ? displayAskSeerState : false;

const filterKeyRegistryQueryKey = useMemo<QueryKey>(
() =>
asyncFilterKeyRegistryQueryKey ?? [
'search-query-builder-filter-key-registry',
fallbackRegistryId,
],
[asyncFilterKeyRegistryQueryKey, fallbackRegistryId]
);

const filterKeyRegistryQueryOptions = useMemo(
() => filterKeyRegistryOptions(filterKeyRegistryQueryKey),
[filterKeyRegistryQueryKey]
);
const {data: asyncFilterKeys = getEmptyFilterKeyRegistry()} = useQuery(
filterKeyRegistryQueryOptions
);
const {filterKeyRegistryQueryOptions, registerFilterKeys} = useFilterKeyRegistry({
asyncFilterKeyRegistryQueryKey,
});

const registerFilterKeys = useCallback(
(tags: Tag[], registryQueryKey: QueryKey) => {
if (!tags.length) {
return;
}

queryClient.setQueryData(
registryQueryKey,
(current: TagCollection | undefined): TagCollection => {
const next = {...current};
let changed = false;

for (const tag of tags) {
const currentTag = current?.[tag.key];
if (
currentTag?.name === tag.name &&
currentTag?.kind === tag.kind &&
currentTag?.predefined === tag.predefined
) {
continue;
}

next[tag.key] = tag;
changed = true;
}

return changed ? next : (current ?? {});
}
);
},
[queryClient]
);
const {data: asyncFilterKeys = {}} = useQuery(filterKeyRegistryQueryOptions);

const registeredGetTagKeys = useCallback<GetTagKeys>(
async searchQuery => {
Expand Down Expand Up @@ -316,6 +253,11 @@ export function SearchQueryBuilderProvider({
[getSuggestedFilterKey]
);

const stableInvalidFilterKeys = useMemo(
() => invalidFilterKeys ?? [],
[invalidFilterKeys]
);

const parseQuery = useCallback(
(query: string) =>
parseQueryBuilderValue(query, getFieldDefinitionWithTagMetadata, {
Expand All @@ -326,6 +268,7 @@ export function SearchQueryBuilderProvider({
disallowWildcard,
filterKeys: mergedFilterKeys,
invalidMessages,
invalidFilterKeys: stableInvalidFilterKeys,
filterKeyAliases,
}),
[
Expand All @@ -337,6 +280,7 @@ export function SearchQueryBuilderProvider({
mergedFilterKeys,
getFilterTokenWarning,
invalidMessages,
stableInvalidFilterKeys,
filterKeyAliases,
]
);
Expand All @@ -354,11 +298,6 @@ export function SearchQueryBuilderProvider({

const parsedQuery = useMemo(() => parseQuery(state.query), [parseQuery, state.query]);

const stableInvalidFilterKeys = useMemo(
() => invalidFilterKeys ?? [],
[invalidFilterKeys]
);

const previousQuery = usePrevious(state.query);
const firstRender = useRef(true);
useEffect(() => {
Expand Down
Loading
Loading