Compare commits

...

1 Commits

Author SHA1 Message Date
aks07
b470421df6 feat(explorer): disambiguate columns by dataType in the composite key
Same-name fields can ship as both number and string (e.g. http.status_code),
which collided on the 2-part composite id. buildCompositeKey now takes an
optional dataType appended when truthy, and the traces + logs column factories,
the options-menu add/remove/reorder path, and the field picker all pass it.
Fields with no dataType (timestamp, body, and the like) keep their 2-part id,
so existing preferences are unaffected.
2026-08-25 14:40:04 +05:30
9 changed files with 50 additions and 23 deletions

View File

@@ -197,7 +197,7 @@ function FieldsSelector({
() =>
fields.map((f) => ({
...f,
key: buildCompositeKey(f.name, f.fieldContext),
key: buildCompositeKey(f.name, f.fieldContext, f.fieldDataType),
})),
[fields],
);

View File

@@ -52,13 +52,15 @@ function OtherFields({
// Normalize: synthesize `key` once so downstream reads can trust it.
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
...attr,
key: buildCompositeKey(attr.name, attr.fieldContext as string),
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType,
}));
const addedIds = new Set(
addedFields.map((f) => buildCompositeKey(f.name, f.fieldContext)),
addedFields.map((f) =>
buildCompositeKey(f.name, f.fieldContext, f.fieldDataType),
),
);
const available = suggestions.filter(
(attr) => !addedIds.has(attr.key as string),

View File

@@ -14,10 +14,10 @@ jest.mock('providers/App/App', () => ({
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
}));
const field = (name: string, type = ''): IField => ({
const field = (name: string, type = '', dataType = ''): IField => ({
name,
type,
dataType: 'string',
dataType,
});
describe('useLogsTableColumns — selectColumns-order respected', () => {
@@ -136,6 +136,24 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
expect(byId.get('user_field')?.enableRemove).toBe(true);
});
it('disambiguates same-name/same-context fields by dataType (3-part id)', () => {
const { result } = renderHook(() =>
useLogsTableColumns({
fields: [
field('http.status_code', 'attribute', 'int64'),
field('http.status_code', 'attribute', 'string'),
],
fontSize: FontSize.SMALL,
}),
);
expect(result.current.map((c) => c.id)).toStrictEqual([
'state-indicator',
'attribute:http.status_code:int64',
'attribute:http.status_code:string',
]);
});
it('renders only the stateIndicator when fields is empty', () => {
const { result } = renderHook(() =>
useLogsTableColumns({

View File

@@ -92,7 +92,7 @@ export function useLogsTableColumns({
};
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
id: buildCompositeKey(f.name, f.type),
id: buildCompositeKey(f.name, f.type, f.dataType),
header: f.name,
accessorFn: (log): unknown =>
getLogFieldValue(log, f.name, isBodyJsonEnabled),

View File

@@ -298,9 +298,9 @@ describe('useOptionsMenu', () => {
// New order: [attribute:service.name, log:body, resource:service.name, log:timestamp]
result.current.config.addColumn?.onReorder([
'attribute:service.name',
'log:body',
'resource:service.name',
'attribute:service.name:string',
'log:body:string',
'resource:service.name:string',
'log:timestamp',
]);
@@ -331,9 +331,9 @@ describe('useOptionsMenu', () => {
'state-indicator',
'log:timestamp',
'unknown.composite',
'log:body',
'resource:service.name',
'attribute:service.name',
'log:body:string',
'resource:service.name:string',
'attribute:service.name:string',
]);
const reordered = mockUpdateColumns.mock.calls[0][0];
@@ -360,7 +360,7 @@ describe('useOptionsMenu', () => {
);
// Removing 'resource:service.name' should drop ONLY the resource variant.
result.current.config.addColumn?.onRemove('resource:service.name');
result.current.config.addColumn?.onRemove('resource:service.name:string');
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
const remaining = mockUpdateColumns.mock.calls[0][0];

View File

@@ -56,7 +56,7 @@ export function dedupeColumnsByCompositeKey(
const seen = new Set<string>();
let hasDuplicate = false;
const deduped = columns.filter((c) => {
const key = buildCompositeKey(c.name, c.fieldContext);
const key = buildCompositeKey(c.name, c.fieldContext, c.fieldDataType);
if (seen.has(key)) {
hasDuplicate = true;
return false;

View File

@@ -281,7 +281,8 @@ const useOptionsMenu = ({
const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => {
const newSelectedColumns = preferences?.columns?.filter(
(f) => buildCompositeKey(f.name, f.fieldContext) !== columnKey,
(f) =>
buildCompositeKey(f.name, f.fieldContext, f.fieldDataType) !== columnKey,
);
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
@@ -364,7 +365,10 @@ const useOptionsMenu = ({
(orderedIds: string[]): void => {
const current = preferences?.columns ?? [];
const byCompositeKey = new Map(
current.map((f) => [buildCompositeKey(f.name, f.fieldContext), f]),
current.map((f) => [
buildCompositeKey(f.name, f.fieldContext, f.fieldDataType),
f,
]),
);
const reordered = orderedIds
.map((id) => byCompositeKey.get(id))

View File

@@ -15,8 +15,11 @@ export const getOptionsFromKeys = (
);
};
// Composite identity for a column. Disambiguates same-name fields across
// different fieldContexts (e.g. resource:service.name vs attribute:service.name).
// Falls back to bare name when context is missing.
export const buildCompositeKey = (name: string, context?: string): string =>
context ? `${context}:${name}` : name;
export const buildCompositeKey = (
name: string,
context?: string,
dataType?: string,
): string => {
const withContext = context ? `${context}:${name}` : name;
return dataType ? `${withContext}:${dataType}` : withContext;
};

View File

@@ -10,11 +10,11 @@ export type TracesTableRow = { id: string } & Record<string, unknown>;
export function getFieldColumn(
field: TelemetryFieldKey,
): TableColumnDef<TracesTableRow> {
const { name, fieldContext } = field;
const { name, fieldContext, fieldDataType } = field;
const isTimestamp = TIMESTAMP_FIELD_NAMES.has(name);
return {
id: buildCompositeKey(name, fieldContext),
id: buildCompositeKey(name, fieldContext, fieldDataType),
header: name,
accessorFn: (row): unknown => row[name],
enableMove: !isTimestamp,