From 6fd43b7d67c3047718181e956163cf211ca41326 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 24 Jun 2026 15:38:49 -0300 Subject: [PATCH 1/6] Validate explore column editor fields --- .../app/views/explore/tables/index.spec.tsx | 58 ++++++++++ static/app/views/explore/tables/index.tsx | 102 +++++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 static/app/views/explore/tables/index.spec.tsx diff --git a/static/app/views/explore/tables/index.spec.tsx b/static/app/views/explore/tables/index.spec.tsx new file mode 100644 index 000000000000..3aa9ffc331f2 --- /dev/null +++ b/static/app/views/explore/tables/index.spec.tsx @@ -0,0 +1,58 @@ +import type {TagCollection} from 'sentry/types/group'; +import {FieldKind} from 'sentry/utils/fields'; +import {getValidatedColumnEditorData} from 'sentry/views/explore/tables'; +import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions'; + +const stringTags: TagCollection = { + id: { + key: 'id', + name: 'id', + kind: FieldKind.TAG, + }, + 'missing.field': { + key: 'missing.field', + name: 'missing.field', + kind: FieldKind.TAG, + }, +}; + +const numberTags: TagCollection = {}; +const booleanTags: TagCollection = {}; + +const validatedColumnsData: EventValidationData = { + dataset: [], + environment: [], + field: [ + {attrType: 'number', error: null, name: 'custom.duration', valid: true}, + {attrType: null, error: 'unknown attribute', name: 'missing.field', valid: false}, + ], + orderby: [], + projects: [], + query: { + error: null, + fields: [], + valid: true, + }, + valid: false, +}; + +describe('getValidatedColumnEditorData', () => { + it('adds valid fields and removes invalid fields', () => { + const result = getValidatedColumnEditorData({ + booleanTags, + fields: ['id', 'custom.duration', 'missing.field'], + numberTags, + stringTags, + validatedColumnsData, + }); + + expect(result.validatedFields).toEqual(['id', 'custom.duration']); + expect(result.validatedNumberTags['custom.duration']).toEqual( + expect.objectContaining({ + key: 'custom.duration', + kind: FieldKind.MEASUREMENT, + }) + ); + expect(result.validatedStringTags['missing.field']).toBeUndefined(); + }); +}); diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 57ba58a40638..7aa1d057d0d8 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -1,4 +1,4 @@ -import {Fragment, useEffect} from 'react'; +import {Fragment, useEffect, useMemo} from 'react'; import {FeatureBadge} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; @@ -9,8 +9,11 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {IconEdit} from 'sentry/icons/iconEdit'; import {t} from 'sentry/locale'; +import type {TagCollection} from 'sentry/types/group'; import type {Confidence} from 'sentry/types/organization'; +import {FieldKind} from 'sentry/utils/fields'; import {AttributeBreakdownsContent} from 'sentry/views/explore/components/attributeBreakdowns/content'; +import {prettifyAttributeName} from 'sentry/views/explore/components/traceItemAttributes/utils'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable'; import type {SpansTableResult} from 'sentry/views/explore/hooks/useExploreSpansTable'; @@ -24,11 +27,13 @@ import { useSetQueryParamsAggregateFields, useSetQueryParamsFields, } from 'sentry/views/explore/queryParams/context'; +import {useValidateSpansTab} from 'sentry/views/explore/spans/hooks/useValidateSpansTab'; import {AggregateColumnEditorModal} from 'sentry/views/explore/tables/aggregateColumnEditorModal'; import {AggregatesTable} from 'sentry/views/explore/tables/aggregatesTable'; import {ColumnEditorModal} from 'sentry/views/explore/tables/columnEditorModal'; import {SpansTable} from 'sentry/views/explore/tables/spansTable'; import {TracesTable} from 'sentry/views/explore/tables/tracesTable/index'; +import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions'; interface BaseExploreTablesProps { confidences: Confidence[]; @@ -58,17 +63,40 @@ export function ExploreTables(props: ExploreTablesProps) { const {attributes: numberTags} = useSpanItemAttributes({}, 'number'); const {attributes: stringTags} = useSpanItemAttributes({}, 'string'); const {attributes: booleanTags} = useSpanItemAttributes({}, 'boolean'); + const {data: validatedColumnsData} = useValidateSpansTab({enabled: tab === Tab.SPAN}); + const { + validatedBooleanTags, + validatedFields, + validatedNumberTags, + validatedStringTags, + } = useMemo( + () => + getValidatedColumnEditorData({ + booleanTags, + fields, + numberTags, + stringTags, + validatedColumnsData, + }), + [booleanTags, fields, numberTags, stringTags, validatedColumnsData] + ); + + useEffect(() => { + if (validatedFields.length < fields.length) { + setFields([...validatedFields]); + } + }, [fields, setFields, validatedFields]); const openColumnEditor = () => { openModal( modalProps => ( ), {closeEvents: 'escape-key'} @@ -161,3 +189,67 @@ export function ExploreTables(props: ExploreTablesProps) { ); } + +export function getValidatedColumnEditorData({ + booleanTags, + fields, + numberTags, + stringTags, + validatedColumnsData, +}: { + booleanTags: TagCollection; + fields: readonly string[]; + numberTags: TagCollection; + stringTags: TagCollection; + validatedColumnsData?: EventValidationData; +}) { + const validatedBooleanTags = {...booleanTags}; + const validatedNumberTags = {...numberTags}; + const validatedStringTags = {...stringTags}; + const invalidFields = new Set(); + + for (const item of validatedColumnsData?.field ?? []) { + if (!item.name) { + continue; + } + + if (!item.valid) { + invalidFields.add(item.name); + delete validatedBooleanTags[item.name]; + delete validatedNumberTags[item.name]; + delete validatedStringTags[item.name]; + continue; + } + + if (item.attrType === 'boolean') { + validatedBooleanTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.BOOLEAN, + }; + } + + if (item.attrType === 'number') { + validatedNumberTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.MEASUREMENT, + }; + } + + if (item.attrType === 'string') { + validatedStringTags[item.name] ??= { + key: item.name, + name: prettifyAttributeName(item.name), + kind: FieldKind.TAG, + }; + } + } + + return { + validatedBooleanTags, + validatedFields: fields.filter(field => !invalidFields.has(field)), + validatedNumberTags, + validatedStringTags, + }; +} From 7bc6137b0769759f67341d4211ccf2599b4e13d1 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 24 Jun 2026 15:39:11 -0300 Subject: [PATCH 2/6] Cover typed explore column selections --- .../explore/tables/columnEditorModal.spec.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/static/app/views/explore/tables/columnEditorModal.spec.tsx b/static/app/views/explore/tables/columnEditorModal.spec.tsx index 89d9aca3204c..f02df2406c55 100644 --- a/static/app/views/explore/tables/columnEditorModal.spec.tsx +++ b/static/app/views/explore/tables/columnEditorModal.spec.tsx @@ -50,6 +50,24 @@ const booleanTags: TagCollection = { }, }; +const enrichedNumberTags: TagCollection = { + ...numberTags, + 'custom.duration': { + key: 'custom.duration', + name: 'custom.duration', + kind: FieldKind.MEASUREMENT, + }, +}; + +const enrichedBooleanTags: TagCollection = { + ...booleanTags, + 'custom.enabled': { + key: 'custom.enabled', + name: 'custom.enabled', + kind: FieldKind.BOOLEAN, + }, +}; + describe('ColumnEditorModal', () => { it('allows closes modal on apply', async () => { const onClose = jest.fn(); @@ -371,4 +389,32 @@ describe('ColumnEditorModal', () => { expect(columns[1]).toHaveTextContent('id'); expect(columns[1]).toHaveTextContent('string'); }); + + it('renders existing columns with types from supplied tags', async () => { + renderGlobalModal(); + + act(() => { + openModal( + modalProps => ( + {}} + stringTags={stringTags} + numberTags={enrichedNumberTags} + booleanTags={enrichedBooleanTags} + /> + ), + {onClose: jest.fn()} + ); + }); + + expect(await screen.findByRole('button', {name: 'Apply'})).toBeInTheDocument(); + + const columns = screen.getAllByTestId('editor-column'); + expect(columns[0]).toHaveTextContent('custom.duration'); + expect(columns[0]).toHaveTextContent('number'); + expect(columns[1]).toHaveTextContent('custom.enabled'); + expect(columns[1]).toHaveTextContent('boolean'); + }); }); From 3fe0e2f36a9b79b8e02dfbc7164965e58a21be09 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 25 Jun 2026 08:46:46 -0300 Subject: [PATCH 3/6] Sync changed validated explore fields --- static/app/views/explore/tables/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 7aa1d057d0d8..14f0633cd670 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -82,7 +82,11 @@ export function ExploreTables(props: ExploreTablesProps) { ); useEffect(() => { - if (validatedFields.length < fields.length) { + const fieldsChanged = + validatedFields.length !== fields.length || + validatedFields.some((field, index) => field !== fields[index]); + + if (fieldsChanged) { setFields([...validatedFields]); } }, [fields, setFields, validatedFields]); From 0bb534e3bca34dcdb0b47ab87ebc111d8c088968 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 25 Jun 2026 09:54:44 -0300 Subject: [PATCH 4/6] Remove delete calls, since all these attributes are valid --- static/app/views/explore/tables/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 14f0633cd670..862758b898c7 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -219,9 +219,6 @@ export function getValidatedColumnEditorData({ if (!item.valid) { invalidFields.add(item.name); - delete validatedBooleanTags[item.name]; - delete validatedNumberTags[item.name]; - delete validatedStringTags[item.name]; continue; } From dc3dafe63b7c1ef5780879de7a8d86ab6018a1f2 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 25 Jun 2026 09:56:16 -0300 Subject: [PATCH 5/6] Remove test file --- .../app/views/explore/tables/index.spec.tsx | 58 ------------------- static/app/views/explore/tables/index.tsx | 2 +- 2 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 static/app/views/explore/tables/index.spec.tsx diff --git a/static/app/views/explore/tables/index.spec.tsx b/static/app/views/explore/tables/index.spec.tsx deleted file mode 100644 index 3aa9ffc331f2..000000000000 --- a/static/app/views/explore/tables/index.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type {TagCollection} from 'sentry/types/group'; -import {FieldKind} from 'sentry/utils/fields'; -import {getValidatedColumnEditorData} from 'sentry/views/explore/tables'; -import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions'; - -const stringTags: TagCollection = { - id: { - key: 'id', - name: 'id', - kind: FieldKind.TAG, - }, - 'missing.field': { - key: 'missing.field', - name: 'missing.field', - kind: FieldKind.TAG, - }, -}; - -const numberTags: TagCollection = {}; -const booleanTags: TagCollection = {}; - -const validatedColumnsData: EventValidationData = { - dataset: [], - environment: [], - field: [ - {attrType: 'number', error: null, name: 'custom.duration', valid: true}, - {attrType: null, error: 'unknown attribute', name: 'missing.field', valid: false}, - ], - orderby: [], - projects: [], - query: { - error: null, - fields: [], - valid: true, - }, - valid: false, -}; - -describe('getValidatedColumnEditorData', () => { - it('adds valid fields and removes invalid fields', () => { - const result = getValidatedColumnEditorData({ - booleanTags, - fields: ['id', 'custom.duration', 'missing.field'], - numberTags, - stringTags, - validatedColumnsData, - }); - - expect(result.validatedFields).toEqual(['id', 'custom.duration']); - expect(result.validatedNumberTags['custom.duration']).toEqual( - expect.objectContaining({ - key: 'custom.duration', - kind: FieldKind.MEASUREMENT, - }) - ); - expect(result.validatedStringTags['missing.field']).toBeUndefined(); - }); -}); diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 862758b898c7..012f70e90555 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -194,7 +194,7 @@ export function ExploreTables(props: ExploreTablesProps) { ); } -export function getValidatedColumnEditorData({ +function getValidatedColumnEditorData({ booleanTags, fields, numberTags, From c3ad65e86c77bbb2419d0d20267cbc31993d6773 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Thu, 25 Jun 2026 10:25:01 -0300 Subject: [PATCH 6/6] Wait for current explore field validation --- .../views/explore/spans/hooks/useValidateSpansTab.tsx | 3 ++- static/app/views/explore/tables/index.tsx | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx b/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx index 26b22e52e3b6..08d1a1fa0e1a 100644 --- a/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx +++ b/static/app/views/explore/spans/hooks/useValidateSpansTab.tsx @@ -26,7 +26,7 @@ export function useValidateSpansTab({enabled = true}: UseValidateSpansTabArgs = const groupBys = useQueryParamsGroupBys(); const visualizes = useQueryParamsVisualizes(); - const {data, isLoading} = useQuery({ + const {data, isFetching, isLoading} = useQuery({ ...validateEventParamsOptions({ organization, selection, @@ -49,6 +49,7 @@ export function useValidateSpansTab({enabled = true}: UseValidateSpansTabArgs = return { data, + isFetching, isLoading, }; } diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx index 012f70e90555..e2f02969028f 100644 --- a/static/app/views/explore/tables/index.tsx +++ b/static/app/views/explore/tables/index.tsx @@ -63,7 +63,10 @@ export function ExploreTables(props: ExploreTablesProps) { const {attributes: numberTags} = useSpanItemAttributes({}, 'number'); const {attributes: stringTags} = useSpanItemAttributes({}, 'string'); const {attributes: booleanTags} = useSpanItemAttributes({}, 'boolean'); - const {data: validatedColumnsData} = useValidateSpansTab({enabled: tab === Tab.SPAN}); + const {data: validatedColumnsData, isFetching: isValidatingColumns} = + useValidateSpansTab({ + enabled: tab === Tab.SPAN, + }); const { validatedBooleanTags, validatedFields, @@ -82,6 +85,10 @@ export function ExploreTables(props: ExploreTablesProps) { ); useEffect(() => { + if (tab !== Tab.SPAN || isValidatingColumns) { + return; + } + const fieldsChanged = validatedFields.length !== fields.length || validatedFields.some((field, index) => field !== fields[index]); @@ -89,7 +96,7 @@ export function ExploreTables(props: ExploreTablesProps) { if (fieldsChanged) { setFields([...validatedFields]); } - }, [fields, setFields, validatedFields]); + }, [fields, isValidatingColumns, setFields, tab, validatedFields]); const openColumnEditor = () => { openModal(