From ca287e918e913fd1ad0d6b55391079201c3c5ba3 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 11:50:44 -0300 Subject: [PATCH 1/3] fix(explore): Preserve attribute search loading state Keep previous attribute results in place while the dropdown refetches a server-filtered search, but only show loading for the initial load and active non-empty search requests. This keeps clear-to-empty transitions from flashing a misleading loading state. Refs EXP-797 Co-Authored-By: GPT-5.4 Made-with: Cursor --- .../explore/hooks/useTraceItemAttributeKeys.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx index 23918fcec0e200..72c07d76f13528 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: isPending || (isFetching && normalizedSearch !== undefined), }; } From 13bbc3f2ab9e339658b1b065183036c4e296b545 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 11:51:10 -0300 Subject: [PATCH 2/3] test(explore): Cover attribute search loading transitions Add hook coverage for default, searched, and cleared attribute searches so we keep server-side substring filtering while preserving the intended loading signal across transitions. Refs EXP-797 Co-Authored-By: GPT-5.4 Made-with: Cursor --- .../hooks/useTraceItemAttributeKeys.spec.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx index 6b6c990856aebb..f4a238f8f2a0ee 100644 --- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx +++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx @@ -255,4 +255,101 @@ 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) + ); + }); }); From fe34d3f1e0b56724b3aa0e54a37d884c6c550ac4 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 12:57:14 -0300 Subject: [PATCH 3/3] fix(explore): Avoid loading state for disabled attribute queries Only report attribute loading while a query is actively fetching so disabled queries do not stay stuck in a pending spinner state. Add a regression test for the TanStack Query v5 disabled-query case. Co-Authored-By: GPT-5.4 Made-with: Cursor --- .../hooks/useTraceItemAttributeKeys.spec.tsx | 15 +++++++++++++++ .../explore/hooks/useTraceItemAttributeKeys.tsx | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx index f4a238f8f2a0ee..2ca53c4b0361c3 100644 --- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx +++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.spec.tsx @@ -352,4 +352,19 @@ describe('useTraceItemAttributeKeys', () => { 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 72c07d76f13528..a7ab200e92a5ce 100644 --- a/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx +++ b/static/app/views/explore/hooks/useTraceItemAttributeKeys.tsx @@ -66,7 +66,7 @@ export function useTraceItemAttributeKeys({ return { attributes: data, error, - isLoading: isPending || (isFetching && normalizedSearch !== undefined), + isLoading: isFetching && (isPending || normalizedSearch !== undefined), }; }