diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx index 6b6c990856aebb..2ca53c4b0361c3 100644 --- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx +++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx @@ -255,4 +255,116 @@ describe('useTraceItemAttributeKeys', () => { expect(result.current.attributes).toEqual(expectedAttributes); }); + + it('keeps previous attributes without re-entering loading when search changes', async () => { + const defaultAttributeKeys: Tag[] = [ + { + key: 'default.attribute', + name: 'Default Attribute', + kind: FieldKind.TAG, + }, + ]; + const searchedAttributeKeys: Tag[] = [ + { + key: 'searched.attribute', + name: 'Searched Attribute', + kind: FieldKind.TAG, + }, + ]; + + const defaultMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/trace-items/attributes/`, + body: defaultAttributeKeys, + match: [ + (_url: string, options: {query?: Record}) => { + const query = options?.query || {}; + return ( + query.itemType === TraceItemDataset.LOGS && + query.attributeType === 'string' && + query.substringMatch === undefined + ); + }, + ], + }); + const searchedMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/trace-items/attributes/`, + body: searchedAttributeKeys, + match: [ + (_url: string, options: {query?: Record}) => { + const query = options?.query || {}; + return ( + query.itemType === TraceItemDataset.LOGS && + query.attributeType === 'string' && + query.substringMatch === 'searched' + ); + }, + ], + }); + + const defaultExpectedAttributes = { + 'default.attribute': { + key: 'default.attribute', + name: 'Default Attribute', + kind: FieldKind.TAG, + secondaryAliases: [], + }, + }; + const searchedExpectedAttributes = { + 'searched.attribute': { + key: 'searched.attribute', + name: 'Searched Attribute', + kind: FieldKind.TAG, + secondaryAliases: [], + }, + }; + + const {result, rerender} = renderHookWithProviders( + ({search}: {search?: string}) => + useTraceItemAttributeKeys({ + search, + traceItemType: TraceItemDataset.LOGS, + type: 'string', + }), + { + initialProps: {search: undefined as string | undefined}, + organization, + } + ); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(result.current.attributes).toEqual(defaultExpectedAttributes); + expect(defaultMock).toHaveBeenCalledTimes(1); + + rerender({search: 'searched'}); + await waitFor(() => expect(result.current.isLoading).toBe(true)); + expect(result.current.attributes).toEqual(defaultExpectedAttributes); + + await waitFor(() => expect(searchedMock).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(result.current.attributes).toEqual(searchedExpectedAttributes); + + rerender({search: ''}); + expect(result.current.isLoading).toBe(false); + expect(result.current.attributes).toEqual(defaultExpectedAttributes); + + await waitFor(() => expect(defaultMock).toHaveBeenCalledTimes(2)); + await waitFor(() => + expect(result.current.attributes).toEqual(defaultExpectedAttributes) + ); + }); + + it('does not stay loading when the query is disabled', () => { + const {result} = renderHookWithProviders( + () => + useTraceItemAttributeKeys({ + enabled: false, + traceItemType: TraceItemDataset.LOGS, + type: 'string', + }), + {organization} + ); + + expect(result.current.isLoading).toBe(false); + expect(result.current.attributes).toBeUndefined(); + }); }); diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx index 23918fcec0e200..a7ab200e92a5ce 100644 --- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx +++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx @@ -3,8 +3,7 @@ import {useMemo} from 'react'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import type {TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils'; -import {useQuery} from 'sentry/utils/queryClient'; -import {usePrevious} from 'sentry/utils/usePrevious'; +import {keepPreviousData, useQuery} from 'sentry/utils/queryClient'; import { makeTraceItemAttributeKeysQueryOptions, useGetTraceItemAttributeKeys, @@ -28,6 +27,7 @@ export function useTraceItemAttributeKeys({ search, }: UseTraceItemAttributeKeysProps) { const {selection} = usePageFilters(); + const normalizedSearch = search || undefined; const projectIds = explicitProjectIds ?? @@ -56,18 +56,17 @@ export function useTraceItemAttributeKeys({ }); // eslint-disable-next-line @tanstack/query/exhaustive-deps - const {data, isFetching, error} = useQuery({ + const {data, isFetching, isPending, error} = useQuery({ enabled, - queryKey: [...queryKey, search], - queryFn: () => getTraceItemAttributeKeys(search), + placeholderData: keepPreviousData, + queryKey: [...queryKey, normalizedSearch], + queryFn: () => getTraceItemAttributeKeys(normalizedSearch), }); - const previous = usePrevious(data, isFetching); - return { - attributes: isFetching ? previous : data, + attributes: data, error, - isLoading: isFetching, + isLoading: isFetching && (isPending || normalizedSearch !== undefined), }; }