diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index 9b59f5fad1..115de3ab40 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -393,15 +393,21 @@ function ExplorerOptions({ backwardCompatibleOptions = omit(options, 'version'); } + // Use the correct default columns based on the current data source + const defaultColumns = + sourcepage === DataSource.TRACES + ? defaultTraceSelectedColumns + : defaultLogsSelectedColumns; + if (extraData.selectColumns?.length) { handleOptionsChange({ ...backwardCompatibleOptions, selectColumns: extraData.selectColumns, }); - } else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) { + } else if (!isEqual(defaultColumns, options.selectColumns)) { handleOptionsChange({ ...backwardCompatibleOptions, - selectColumns: defaultTraceSelectedColumns, + selectColumns: defaultColumns, }); } }; diff --git a/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts b/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts index c8d32a4c20..78ce41a532 100644 --- a/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/logsLoaderConfig.test.ts @@ -18,6 +18,16 @@ jest.mock('api/browser/localstorage/get', () => ({ default: jest.fn((key: string) => mockLocalStorage[key] || null), })); +const mockLogsColumns = [ + { name: 'timestamp', signal: 'logs', fieldContext: 'log' }, + { name: 'body', signal: 'logs', fieldContext: 'log' }, +]; + +const mockTracesColumns = [ + { name: 'service.name', signal: 'traces', fieldContext: 'resource' }, + { name: 'name', signal: 'traces', fieldContext: 'span' }, +]; + describe('logsLoaderConfig', () => { // Save original location object const originalWindowLocation = window.location; @@ -157,4 +167,83 @@ describe('logsLoaderConfig', () => { } as FormattingOptions, }); }); + + describe('Column validation - filtering Traces columns', () => { + it('should filter out Traces columns (name with traces signal) from URL', async () => { + const mixedColumns = [...mockLogsColumns, ...mockTracesColumns]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: mixedColumns, + }), + )}`; + + const result = await logsLoaderConfig.url(); + + // Should only keep logs columns + expect(result.columns).toEqual(mockLogsColumns); + }); + + it('should filter out Traces columns from localStorage', async () => { + const tracesColumns = [...mockTracesColumns]; + + mockLocalStorage[LOCALSTORAGE.LOGS_LIST_OPTIONS] = JSON.stringify({ + selectColumns: tracesColumns, + }); + + const result = await logsLoaderConfig.local(); + + // Should filter out all Traces columns + expect(result.columns).toEqual([]); + }); + + it('should accept valid Logs columns from URL', async () => { + const logsColumns = [...mockLogsColumns]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: logsColumns, + }), + )}`; + + const result = await logsLoaderConfig.url(); + + expect(result.columns).toEqual(logsColumns); + }); + + it('should fall back to defaults when all columns are filtered out from URL', async () => { + const tracesColumns = [...mockTracesColumns]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: tracesColumns, + }), + )}`; + + const result = await logsLoaderConfig.url(); + + // Should return empty array, which triggers fallback to defaults in preferencesLoader + expect(result.columns).toEqual([]); + }); + + it('should handle columns without signal field (legacy data)', async () => { + const columnsWithoutSignal = [ + { name: 'body', fieldContext: 'log' }, + { name: 'service.name', fieldContext: 'resource' }, + ]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: columnsWithoutSignal, + }), + )}`; + + const result = await logsLoaderConfig.url(); + + // Without signal field, columns pass through validation + // This matches the current implementation behavior where only columns + // with signal !== 'logs' are filtered out + expect(result.columns).toEqual(columnsWithoutSignal); + }); + }); }); diff --git a/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts b/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts index 6487a6e5b8..10ca011cdc 100644 --- a/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts +++ b/frontend/src/providers/preferences/__tests__/tracesLoaderConfig.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants'; import { @@ -126,4 +127,112 @@ describe('tracesLoaderConfig', () => { columns: defaultTraceSelectedColumns as TelemetryFieldKey[], }); }); + + describe('Column validation - filtering Logs columns', () => { + it('should filter out Logs columns (body) from URL', async () => { + const logsColumns = [ + { name: 'timestamp', signal: 'logs', fieldContext: 'log' }, + { name: 'body', signal: 'logs', fieldContext: 'log' }, + ]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: logsColumns, + }), + )}`; + + const result = await tracesLoaderConfig.url(); + + // Should filter out all Logs columns + expect(result.columns).toEqual([]); + }); + + it('should filter out Logs columns (timestamp with logs signal) from URL', async () => { + const mixedColumns = [ + { name: 'timestamp', signal: 'logs', fieldContext: 'log' }, + { name: 'service.name', signal: 'traces', fieldContext: 'resource' }, + ]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: mixedColumns, + }), + )}`; + + const result = await tracesLoaderConfig.url(); + + // Should only keep trace columns + expect(result.columns).toEqual([ + { name: 'service.name', signal: 'traces', fieldContext: 'resource' }, + ]); + }); + + it('should filter out Logs columns from localStorage', async () => { + const logsColumns = [ + { name: 'body', signal: 'logs', fieldContext: 'log' }, + { name: 'timestamp', signal: 'logs', fieldContext: 'log' }, + ]; + + mockLocalStorage[LOCALSTORAGE.TRACES_LIST_OPTIONS] = JSON.stringify({ + selectColumns: logsColumns, + }); + + const result = await tracesLoaderConfig.local(); + + // Should filter out all Logs columns + expect(result.columns).toEqual([]); + }); + + it('should accept valid Trace columns from URL', async () => { + const traceColumns = [ + { name: 'service.name', signal: 'traces', fieldContext: 'resource' }, + { name: 'name', signal: 'traces', fieldContext: 'span' }, + ]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: traceColumns, + }), + )}`; + + const result = await tracesLoaderConfig.url(); + + expect(result.columns).toEqual(traceColumns); + }); + + it('should fall back to defaults when all columns are filtered out from URL', async () => { + const logsColumns = [{ name: 'body', signal: 'logs' }]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: logsColumns, + }), + )}`; + + const result = await tracesLoaderConfig.url(); + + // Should return empty array, which triggers fallback to defaults in preferencesLoader + expect(result.columns).toEqual([]); + }); + + it('should handle columns without signal field (legacy data)', async () => { + const columnsWithoutSignal = [ + { name: 'service.name', fieldContext: 'resource' }, + { name: 'body', fieldContext: 'log' }, + ]; + + mockedLocation.search = `?options=${encodeURIComponent( + JSON.stringify({ + selectColumns: columnsWithoutSignal, + }), + )}`; + + const result = await tracesLoaderConfig.url(); + + // Without signal field, columns pass through validation + // This matches the current implementation behavior where only columns + // with signal !== 'traces' are filtered out + expect(result.columns).toEqual(columnsWithoutSignal); + }); + }); }); diff --git a/frontend/src/providers/preferences/configs/logsLoaderConfig.ts b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts index 85e72f0301..f9f717ad3b 100644 --- a/frontend/src/providers/preferences/configs/logsLoaderConfig.ts +++ b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts @@ -8,6 +8,18 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe import { FormattingOptions } from '../types'; +/** + * Validates if a column is valid for Logs Explorer + * Filters out Traces-specific columns that would cause query failures + */ +const isValidLogColumn = (col: { + name?: string; + signal?: string; + [key: string]: unknown; +}): boolean => + // If column has signal field, it must be 'logs' + !(col?.signal && col.signal !== 'logs'); + // --- LOGS preferences loader config --- const logsLoaders = { local: (): { @@ -18,8 +30,14 @@ const logsLoaders = { if (local) { try { const parsed = JSON.parse(local); + + const localColumns = parsed.selectColumns || []; + + // Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored) + const validLogColumns = localColumns.filter(isValidLogColumn); + return { - columns: parsed.selectColumns || [], + columns: validLogColumns.length > 0 ? validLogColumns : [], formatting: { maxLines: parsed.maxLines ?? 2, format: parsed.format ?? 'table', @@ -38,8 +56,14 @@ const logsLoaders = { const urlParams = new URLSearchParams(window.location.search); try { const options = JSON.parse(urlParams.get('options') || '{}'); + + const urlColumns = options.selectColumns || []; + + // Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored) + const validLogColumns = urlColumns.filter(isValidLogColumn); + return { - columns: options.selectColumns || [], + columns: validLogColumns.length > 0 ? validLogColumns : [], formatting: { maxLines: options.maxLines ?? 2, format: options.format ?? 'table', diff --git a/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts index 858a53ab7f..da07fb0866 100644 --- a/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts +++ b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts @@ -5,6 +5,18 @@ import { LOCALSTORAGE } from 'constants/localStorage'; import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +/** + * Validates if a column is valid for Traces Explorer + * Filters out Logs-specific columns that would cause query failures + */ +const isValidTraceColumn = (col: { + name?: string; + signal?: string; + [key: string]: unknown; +}): boolean => + // If column has signal field, it must be 'traces' + !(col?.signal && col.signal !== 'traces'); + // --- TRACES preferences loader config --- const tracesLoaders = { local: (): { @@ -14,8 +26,13 @@ const tracesLoaders = { if (local) { try { const parsed = JSON.parse(local); + const localColumns = parsed.selectColumns || []; + + // Filter out invalid columns (e.g., Logs columns that might have been incorrectly stored) + const validTraceColumns = localColumns.filter(isValidTraceColumn); + return { - columns: parsed.selectColumns || [], + columns: validTraceColumns.length > 0 ? validTraceColumns : [], }; } catch {} } @@ -27,8 +44,15 @@ const tracesLoaders = { const urlParams = new URLSearchParams(window.location.search); try { const options = JSON.parse(urlParams.get('options') || '{}'); + const urlColumns = options.selectColumns || []; + + // Filter out invalid columns (e.g., Logs columns) + // Only accept columns that are valid for Traces (signal='traces' or columns without signal that aren't logs-specific) + const validTraceColumns = urlColumns.filter(isValidTraceColumn); + + // Only return columns if we have valid trace columns, otherwise return empty to fall back to defaults return { - columns: options.selectColumns || [], + columns: validTraceColumns.length > 0 ? validTraceColumns : [], }; } catch {} return { columns: [] };