diff --git a/static/app/views/explore/components/typeBadge.tsx b/static/app/views/explore/components/typeBadge.tsx
new file mode 100644
index 00000000000000..d42d82fdc4344c
--- /dev/null
+++ b/static/app/views/explore/components/typeBadge.tsx
@@ -0,0 +1,18 @@
+import BadgeTag from 'sentry/components/badge/tag';
+import {t} from 'sentry/locale';
+import type {Tag} from 'sentry/types/group';
+import {FieldKind} from 'sentry/utils/fields';
+
+interface TypeBadgeProps {
+ tag?: Tag;
+}
+
+export function TypeBadge({tag}: TypeBadgeProps) {
+ if (tag?.kind === FieldKind.MEASUREMENT) {
+ return {t('number')};
+ }
+ if (tag?.kind === FieldKind.TAG) {
+ return {t('string')};
+ }
+ return null;
+}
diff --git a/static/app/views/explore/contexts/spanTagsContext.tsx b/static/app/views/explore/contexts/spanTagsContext.tsx
index 87392760546754..234d8d13c2b829 100644
--- a/static/app/views/explore/contexts/spanTagsContext.tsx
+++ b/static/app/views/explore/contexts/spanTagsContext.tsx
@@ -4,9 +4,11 @@ import {createContext, useContext, useMemo} from 'react';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import type {Tag, TagCollection} from 'sentry/types/group';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {FieldKind} from 'sentry/utils/fields';
import {useApiQuery} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
+import {SpanIndexedField} from 'sentry/views/insights/types';
import {
useSpanFieldStaticTags,
useSpanFieldSupportedTags,
@@ -25,6 +27,21 @@ interface SpanTagsProviderProps {
}
export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) {
+ const numericSpanFields: Set = useMemo(() => {
+ return new Set([
+ SpanIndexedField.SPAN_DURATION,
+ SpanIndexedField.SPAN_SELF_TIME,
+ SpanIndexedField.INP,
+ SpanIndexedField.INP_SCORE,
+ SpanIndexedField.INP_SCORE_WEIGHT,
+ SpanIndexedField.TOTAL_SCORE,
+ SpanIndexedField.CACHE_ITEM_SIZE,
+ SpanIndexedField.MESSAGING_MESSAGE_BODY_SIZE,
+ SpanIndexedField.MESSAGING_MESSAGE_RECEIVE_LATENCY,
+ SpanIndexedField.MESSAGING_MESSAGE_RETRY_COUNT,
+ ]);
+ }, []);
+
const supportedTags = useSpanFieldSupportedTags();
const numberTags: TagCollection = useTypedSpanTags({
@@ -44,8 +61,15 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) {
return {};
}
- return numberTags;
- }, [dataset, numberTags]);
+ return {
+ ...numberTags,
+ ...Object.fromEntries(
+ Object.entries(staticTags)
+ .filter(([key, _]) => numericSpanFields.has(key))
+ .map(([key, tag]) => [key, {...tag, kind: FieldKind.MEASUREMENT}])
+ ),
+ };
+ }, [dataset, numberTags, numericSpanFields, staticTags]);
const allStringTags = useMemo(() => {
if (dataset === DiscoverDatasets.SPANS_INDEXED) {
@@ -54,9 +78,13 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) {
return {
...stringTags,
- ...staticTags,
+ ...Object.fromEntries(
+ Object.entries(staticTags)
+ .filter(([key, _]) => !numericSpanFields.has(key))
+ .map(([key, tag]) => [key, {...tag, kind: FieldKind.TAG}])
+ ),
};
- }, [dataset, supportedTags, stringTags, staticTags]);
+ }, [dataset, supportedTags, stringTags, staticTags, numericSpanFields]);
const tags = {
number: allNumberTags,
@@ -66,7 +94,7 @@ export function SpanTagsProvider({children, dataset}: SpanTagsProviderProps) {
return {children};
}
-export const useSpanTags = (type?: 'number' | 'string') => {
+export function useSpanTags(type?: 'number' | 'string') {
const typedTags = useContext(SpanTagsContext);
if (typedTags === undefined) {
@@ -77,7 +105,14 @@ export const useSpanTags = (type?: 'number' | 'string') => {
return typedTags.number;
}
return typedTags.string;
-};
+}
+
+export function useSpanTag(key: string) {
+ const numberTags = useSpanTags('number');
+ const stringTags = useSpanTags('string');
+
+ return stringTags[key] ?? numberTags[key] ?? null;
+}
function useTypedSpanTags({
enabled,
@@ -123,14 +158,17 @@ function useTypedSpanTags({
continue;
}
- allTags[tag.key] = {
- key: tag.key,
- name: tag.name,
+ const key = type === 'number' ? `tags[${tag.key},number]` : tag.key;
+
+ allTags[key] = {
+ key,
+ name: tag.key,
+ kind: type === 'number' ? FieldKind.MEASUREMENT : FieldKind.TAG,
};
}
return allTags;
- }, [result]);
+ }, [result, type]);
return tags;
}
diff --git a/static/app/views/explore/hooks/useResultsMode.spec.tsx b/static/app/views/explore/hooks/useResultsMode.spec.tsx
index 40c05b69fec7bc..21e1019041131b 100644
--- a/static/app/views/explore/hooks/useResultsMode.spec.tsx
+++ b/static/app/views/explore/hooks/useResultsMode.spec.tsx
@@ -40,7 +40,7 @@ describe('useResultMode', function () {
expect(resultMode).toEqual('samples'); // default
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
@@ -57,7 +57,7 @@ describe('useResultMode', function () {
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
diff --git a/static/app/views/explore/hooks/useSampleFields.spec.tsx b/static/app/views/explore/hooks/useSampleFields.spec.tsx
index bfb1b47f24a833..20fdd9d3231894 100644
--- a/static/app/views/explore/hooks/useSampleFields.spec.tsx
+++ b/static/app/views/explore/hooks/useSampleFields.spec.tsx
@@ -15,7 +15,7 @@ describe('useSampleFields', function () {
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
@@ -28,7 +28,7 @@ describe('useSampleFields', function () {
act(() => setSampleFields([]));
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
diff --git a/static/app/views/explore/hooks/useSampleFields.tsx b/static/app/views/explore/hooks/useSampleFields.tsx
index b904b9cb67f59a..0f36e1c2d6fd87 100644
--- a/static/app/views/explore/hooks/useSampleFields.tsx
+++ b/static/app/views/explore/hooks/useSampleFields.tsx
@@ -31,7 +31,14 @@ function useSampleFieldsImpl({
return fields;
}
- return ['project', 'id', 'span.op', 'span.description', 'span.duration', 'timestamp'];
+ return [
+ 'project',
+ 'span_id',
+ 'span.op',
+ 'span.description',
+ 'span.duration',
+ 'timestamp',
+ ];
}, [location.query.field]);
const setSampleFields = useCallback(
diff --git a/static/app/views/explore/tables/columnEditorModal.spec.tsx b/static/app/views/explore/tables/columnEditorModal.spec.tsx
index 0a57350f14a0fd..976d33940b43f1 100644
--- a/static/app/views/explore/tables/columnEditorModal.spec.tsx
+++ b/static/app/views/explore/tables/columnEditorModal.spec.tsx
@@ -1,20 +1,28 @@
import {act, renderGlobalModal, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {openModal} from 'sentry/actionCreators/modal';
+import type {TagCollection} from 'sentry/types/group';
import {ColumnEditorModal} from 'sentry/views/explore/tables/columnEditorModal';
-const tagOptions = {
+const stringTags: TagCollection = {
id: {
key: 'id',
- name: 'ID',
+ name: 'id',
},
project: {
key: 'project',
- name: 'Project',
+ name: 'project',
},
'span.op': {
key: 'span.op',
- name: 'Span OP',
+ name: 'span.op',
+ },
+};
+
+const numberTags: TagCollection = {
+ 'span.duration': {
+ key: 'span.duration',
+ name: 'span.duration',
},
};
@@ -36,7 +44,8 @@ describe('ColumnEditorModal', function () {
{...modalProps}
columns={['id', 'project']}
onColumnsChange={() => {}}
- tags={tagOptions}
+ stringTags={stringTags}
+ numberTags={numberTags}
/>
),
{onClose}
@@ -60,7 +69,8 @@ describe('ColumnEditorModal', function () {
{...modalProps}
columns={['id', 'project']}
onColumnsChange={onColumnsChange}
- tags={tagOptions}
+ stringTags={stringTags}
+ numberTags={numberTags}
/>
),
{onClose: jest.fn()}
@@ -98,7 +108,8 @@ describe('ColumnEditorModal', function () {
{...modalProps}
columns={['id', 'project']}
onColumnsChange={onColumnsChange}
- tags={tagOptions}
+ stringTags={stringTags}
+ numberTags={numberTags}
/>
),
{onClose: jest.fn()}
@@ -117,14 +128,14 @@ describe('ColumnEditorModal', function () {
expect(column).toHaveTextContent(columns2[i]);
});
- const options = ['id', 'project', 'span.op'];
+ const options = ['id', 'project', 'span.duration', 'span.op'];
await userEvent.click(screen.getByRole('button', {name: 'None'}));
const columnOptions = await screen.findAllByRole('option');
columnOptions.forEach((option, i) => {
expect(option).toHaveTextContent(options[i]);
});
- await userEvent.click(columnOptions[2]);
+ await userEvent.click(columnOptions[3]);
const columns3 = ['id', 'project', 'span.op'];
screen.getAllByTestId('editor-column').forEach((column, i) => {
expect(column).toHaveTextContent(columns3[i]);
@@ -146,7 +157,8 @@ describe('ColumnEditorModal', function () {
{...modalProps}
columns={['id', 'project']}
onColumnsChange={onColumnsChange}
- tags={tagOptions}
+ stringTags={stringTags}
+ numberTags={numberTags}
/>
),
{onClose: jest.fn()}
@@ -158,14 +170,14 @@ describe('ColumnEditorModal', function () {
expect(column).toHaveTextContent(columns1[i]);
});
- const options = ['id', 'project', 'span.op'];
+ const options = ['id', 'project', 'span.duration', 'span.op'];
await userEvent.click(screen.getByRole('button', {name: 'project'}));
const columnOptions = await screen.findAllByRole('option');
columnOptions.forEach((option, i) => {
expect(option).toHaveTextContent(options[i]);
});
- await userEvent.click(columnOptions[2]);
+ await userEvent.click(columnOptions[3]);
const columns2 = ['id', 'span.op'];
screen.getAllByTestId('editor-column').forEach((column, i) => {
expect(column).toHaveTextContent(columns2[i]);
diff --git a/static/app/views/explore/tables/columnEditorModal.tsx b/static/app/views/explore/tables/columnEditorModal.tsx
index 82b16775775b1a..f115bf28497aaa 100644
--- a/static/app/views/explore/tables/columnEditorModal.tsx
+++ b/static/app/views/explore/tables/columnEditorModal.tsx
@@ -30,6 +30,7 @@ import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {TagCollection} from 'sentry/types/group';
import {defined} from 'sentry/utils';
+import {TypeBadge} from 'sentry/views/explore/components/typeBadge';
type Column = {
column: string | undefined;
@@ -38,8 +39,9 @@ type Column = {
interface ColumnEditorModalProps extends ModalRenderProps {
columns: string[];
+ numberTags: TagCollection;
onColumnsChange: (fields: string[]) => void;
- tags: TagCollection;
+ stringTags: TagCollection;
}
export function ColumnEditorModal({
@@ -49,7 +51,8 @@ export function ColumnEditorModal({
closeModal,
columns,
onColumnsChange,
- tags,
+ numberTags,
+ stringTags,
}: ColumnEditorModalProps) {
const [editableColumns, setEditableColumns] = useState(
// falsey ids are not draggable in dndkit
@@ -58,14 +61,38 @@ export function ColumnEditorModal({
const [nextId, setNextId] = useState(columns.length + 1);
- const tagOptions = useMemo(() => {
- return Object.values(tags).map(tag => {
- return {
- label: tag.key,
- value: tag.key,
- };
+ const tags: SelectOption[] = useMemo(() => {
+ const allTags = [
+ ...Object.values(stringTags).map(tag => {
+ return {
+ label: tag.name,
+ value: tag.key,
+ textValue: tag.name,
+ trailingItems: ,
+ };
+ }),
+ ...Object.values(numberTags).map(tag => {
+ return {
+ label: tag.name,
+ value: tag.key,
+ textValue: tag.name,
+ trailingItems: ,
+ };
+ }),
+ ];
+ allTags.sort((a, b) => {
+ if (a.label < b.label) {
+ return -1;
+ }
+
+ if (a.label > b.label) {
+ return 1;
+ }
+
+ return 0;
});
- }, [tags]);
+ return allTags;
+ }, [stringTags, numberTags]);
function handleApply() {
onColumnsChange(editableColumns.map(({column}) => column).filter(defined));
@@ -114,7 +141,7 @@ export function ColumnEditorModal({
onColumnChange={updateColumnAtIndex}
onColumnDelete={deleteColumnAtIndex}
onColumnSwap={swapColumnsAtIndex}
- tags={tagOptions}
+ tags={tags}
/>
@@ -224,6 +251,30 @@ function ColumnEditorRow({
}
}
+ // The compact select component uses the option label to render the current
+ // selection. This overrides it to render in a trailing item showing the type.
+ const label = useMemo(() => {
+ if (defined(column.column)) {
+ const tag = tags.find(option => option.value === column.column);
+ if (defined(tag)) {
+ return (
+
+ {tag.label}
+ {tag.trailingItems &&
+ (typeof tag.trailingItems === 'function'
+ ? tag.trailingItems({
+ disabled: false,
+ isFocused: false,
+ isSelected: false,
+ })
+ : tag.trailingItems)}
+
+ );
+ }
+ }
+ return {column.column ?? t('None')};
+ }, [column.column, tags]);
+
return (
p.theme.overflowEllipsis}
+ text-align: left;
+ line-height: normal;
+ display: flex;
+ justify-content: space-between;
+`;
diff --git a/static/app/views/explore/tables/index.tsx b/static/app/views/explore/tables/index.tsx
index 6cd8c71c62d4ce..afb925131ff7a6 100644
--- a/static/app/views/explore/tables/index.tsx
+++ b/static/app/views/explore/tables/index.tsx
@@ -42,7 +42,8 @@ function ExploreAggregatesTable() {
function ExploreSamplesTable() {
const [tab, setTab] = useState(Tab.SPAN);
const [fields, setFields] = useSampleFields();
- const tags = useSpanTags();
+ const numberTags = useSpanTags('number');
+ const stringTags = useSpanTags('string');
const openColumnEditor = useCallback(() => {
openModal(
@@ -51,12 +52,13 @@ function ExploreSamplesTable() {
{...modalProps}
columns={fields}
onColumnsChange={setFields}
- tags={tags}
+ stringTags={stringTags}
+ numberTags={numberTags}
/>
),
{closeEvents: 'escape-key'}
);
- }, [fields, setFields, tags]);
+ }, [fields, setFields, stringTags, numberTags]);
return (
diff --git a/static/app/views/explore/tables/spansTable.tsx b/static/app/views/explore/tables/spansTable.tsx
index f88b8a64e81680..22048fcddcaa51 100644
--- a/static/app/views/explore/tables/spansTable.tsx
+++ b/static/app/views/explore/tables/spansTable.tsx
@@ -25,6 +25,7 @@ import {
TableStatus,
useTableStyles,
} from 'sentry/views/explore/components/table';
+import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
import {useDataset} from 'sentry/views/explore/hooks/useDataset';
import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields';
import {useSorts} from 'sentry/views/explore/hooks/useSorts';
@@ -82,16 +83,22 @@ export function SpansTable({}: SpansTableProps) {
const meta = result.meta ?? {};
+ const numberTags = useSpanTags('number');
+ const stringTags = useSpanTags('string');
+
return (
- {fields.map((field, i) => (
-
- {field}
-
- ))}
+ {fields.map((field, i) => {
+ const tag = stringTags[field] ?? numberTags[field] ?? null;
+ return (
+
+ {tag?.name ?? field}
+
+ );
+ })}
diff --git a/static/app/views/explore/toolbar/index.spec.tsx b/static/app/views/explore/toolbar/index.spec.tsx
index 827dadffe111aa..d02a817f553494 100644
--- a/static/app/views/explore/toolbar/index.spec.tsx
+++ b/static/app/views/explore/toolbar/index.spec.tsx
@@ -54,7 +54,7 @@ describe('ExploreToolbar', function () {
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
@@ -81,7 +81,7 @@ describe('ExploreToolbar', function () {
expect(sampleFields).toEqual([
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
@@ -188,7 +188,7 @@ describe('ExploreToolbar', function () {
// check the default field options
const fields = [
'project',
- 'id',
+ 'span_id',
'span.op',
'span.description',
'span.duration',
diff --git a/static/app/views/explore/toolbar/toolbarGroupBy.tsx b/static/app/views/explore/toolbar/toolbarGroupBy.tsx
index c8a15b6832151c..646b3f6242b6ff 100644
--- a/static/app/views/explore/toolbar/toolbarGroupBy.tsx
+++ b/static/app/views/explore/toolbar/toolbarGroupBy.tsx
@@ -40,6 +40,7 @@ export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
return {
label: groupBy,
value: groupBy,
+ textValue: groupBy,
};
});
@@ -47,12 +48,13 @@ export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
return {
label: tagKey,
value: tagKey,
+ textValue: tagKey,
};
});
return [
// hard code in an empty option
- {label: t('None'), value: ''},
+ {label: t('None'), value: '', textValue: t('none')},
...unknownOptions,
...knownOptions,
];
diff --git a/static/app/views/explore/toolbar/toolbarSortBy.tsx b/static/app/views/explore/toolbar/toolbarSortBy.tsx
index 83d90a191e3163..f5dc7636185e6c 100644
--- a/static/app/views/explore/toolbar/toolbarSortBy.tsx
+++ b/static/app/views/explore/toolbar/toolbarSortBy.tsx
@@ -4,6 +4,8 @@ import type {SelectOption} from 'sentry/components/compactSelect';
import {CompactSelect} from 'sentry/components/compactSelect';
import {t} from 'sentry/locale';
import type {Sort} from 'sentry/utils/discover/fields';
+import {TypeBadge} from 'sentry/views/explore/components/typeBadge';
+import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
import type {Field} from 'sentry/views/explore/hooks/useSampleFields';
import {ToolbarHeader, ToolbarLabel, ToolbarRow, ToolbarSection} from './styles';
@@ -15,14 +17,20 @@ interface ToolbarSortByProps {
}
export function ToolbarSortBy({fields, setSorts, sorts}: ToolbarSortByProps) {
+ const numberTags = useSpanTags('number');
+ const stringTags = useSpanTags('string');
+
const fieldOptions: SelectOption[] = useMemo(() => {
return fields.map(field => {
+ const tag = stringTags[field] ?? numberTags[field] ?? null;
return {
- label: field,
- value: field,
+ label: tag?.name ?? field,
+ value: tag?.key ?? field,
+ textValue: tag?.name ?? field,
+ trailingItems: ,
};
});
- }, [fields]);
+ }, [fields, numberTags, stringTags]);
const setSortField = useCallback(
(i: number, {value}: SelectOption) => {
@@ -43,10 +51,12 @@ export function ToolbarSortBy({fields, setSorts, sorts}: ToolbarSortByProps) {
{
label: 'Desc',
value: 'desc',
+ textValue: t('Descending'),
},
{
label: 'Asc',
value: 'asc',
+ textValue: t('Ascending'),
},
];
}, []);
diff --git a/static/app/views/explore/toolbar/toolbarVisualize.tsx b/static/app/views/explore/toolbar/toolbarVisualize.tsx
index a0d1c1a6ba3c7f..a3f8ea6c38df6d 100644
--- a/static/app/views/explore/toolbar/toolbarVisualize.tsx
+++ b/static/app/views/explore/toolbar/toolbarVisualize.tsx
@@ -46,6 +46,7 @@ export function ToolbarVisualize({}: ToolbarVisualizeProps) {
return {
label: field,
value: field,
+ textValue: field,
};
});
@@ -54,6 +55,7 @@ export function ToolbarVisualize({}: ToolbarVisualizeProps) {
return {
label: aggregate,
value: aggregate,
+ textValue: aggregate,
};
});