fix: handle default columns in logs and traces explorer (#9722)
* fix: handle default columns in logs and traces explorer * fix: filter out selected columns based on signal in logs and traces explorer
This commit is contained in:
@@ -393,15 +393,21 @@ function ExplorerOptions({
|
|||||||
backwardCompatibleOptions = omit(options, 'version');
|
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) {
|
if (extraData.selectColumns?.length) {
|
||||||
handleOptionsChange({
|
handleOptionsChange({
|
||||||
...backwardCompatibleOptions,
|
...backwardCompatibleOptions,
|
||||||
selectColumns: extraData.selectColumns,
|
selectColumns: extraData.selectColumns,
|
||||||
});
|
});
|
||||||
} else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) {
|
} else if (!isEqual(defaultColumns, options.selectColumns)) {
|
||||||
handleOptionsChange({
|
handleOptionsChange({
|
||||||
...backwardCompatibleOptions,
|
...backwardCompatibleOptions,
|
||||||
selectColumns: defaultTraceSelectedColumns,
|
selectColumns: defaultColumns,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ jest.mock('api/browser/localstorage/get', () => ({
|
|||||||
default: jest.fn((key: string) => mockLocalStorage[key] || null),
|
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', () => {
|
describe('logsLoaderConfig', () => {
|
||||||
// Save original location object
|
// Save original location object
|
||||||
const originalWindowLocation = window.location;
|
const originalWindowLocation = window.location;
|
||||||
@@ -157,4 +167,83 @@ describe('logsLoaderConfig', () => {
|
|||||||
} as FormattingOptions,
|
} 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import {
|
import {
|
||||||
@@ -126,4 +127,112 @@ describe('tracesLoaderConfig', () => {
|
|||||||
columns: defaultTraceSelectedColumns as TelemetryFieldKey[],
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
|
|
||||||
import { FormattingOptions } from '../types';
|
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 ---
|
// --- LOGS preferences loader config ---
|
||||||
const logsLoaders = {
|
const logsLoaders = {
|
||||||
local: (): {
|
local: (): {
|
||||||
@@ -18,8 +30,14 @@ const logsLoaders = {
|
|||||||
if (local) {
|
if (local) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(local);
|
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 {
|
return {
|
||||||
columns: parsed.selectColumns || [],
|
columns: validLogColumns.length > 0 ? validLogColumns : [],
|
||||||
formatting: {
|
formatting: {
|
||||||
maxLines: parsed.maxLines ?? 2,
|
maxLines: parsed.maxLines ?? 2,
|
||||||
format: parsed.format ?? 'table',
|
format: parsed.format ?? 'table',
|
||||||
@@ -38,8 +56,14 @@ const logsLoaders = {
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
try {
|
try {
|
||||||
const options = JSON.parse(urlParams.get('options') || '{}');
|
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 {
|
return {
|
||||||
columns: options.selectColumns || [],
|
columns: validLogColumns.length > 0 ? validLogColumns : [],
|
||||||
formatting: {
|
formatting: {
|
||||||
maxLines: options.maxLines ?? 2,
|
maxLines: options.maxLines ?? 2,
|
||||||
format: options.format ?? 'table',
|
format: options.format ?? 'table',
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
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 ---
|
// --- TRACES preferences loader config ---
|
||||||
const tracesLoaders = {
|
const tracesLoaders = {
|
||||||
local: (): {
|
local: (): {
|
||||||
@@ -14,8 +26,13 @@ const tracesLoaders = {
|
|||||||
if (local) {
|
if (local) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(local);
|
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 {
|
return {
|
||||||
columns: parsed.selectColumns || [],
|
columns: validTraceColumns.length > 0 ? validTraceColumns : [],
|
||||||
};
|
};
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -27,8 +44,15 @@ const tracesLoaders = {
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
try {
|
try {
|
||||||
const options = JSON.parse(urlParams.get('options') || '{}');
|
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 {
|
return {
|
||||||
columns: options.selectColumns || [],
|
columns: validTraceColumns.length > 0 ? validTraceColumns : [],
|
||||||
};
|
};
|
||||||
} catch {}
|
} catch {}
|
||||||
return { columns: [] };
|
return { columns: [] };
|
||||||
|
|||||||
Reference in New Issue
Block a user