Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions static/app/components/performance/spanSearchQueryBuilder.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {renderHook} from 'sentry-test/reactTestingLibrary';

import {useSpanSearchQueryBuilderProps} from 'sentry/components/performance/spanSearchQueryBuilder';
import {FieldKind} from 'sentry/utils/fields';
import type {TraceItemSearchQueryBuilderProps} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions';

const mockUseSpanItemAttributes = jest.fn();
const mockUseTraceItemSearchQueryBuilderProps = jest.fn(
(props: TraceItemSearchQueryBuilderProps) => props
);

jest.mock('sentry/views/explore/hooks/useTraceItemAttributes', () => ({
useSpanItemAttributes: (params: unknown, type: string) =>
mockUseSpanItemAttributes(params, type),
}));

jest.mock('sentry/views/explore/components/traceItemSearchQueryBuilder', () => ({
useTraceItemSearchQueryBuilderProps: (props: TraceItemSearchQueryBuilderProps) =>
mockUseTraceItemSearchQueryBuilderProps(props),
}));

const spanAttributesByType = {
boolean: {
'span.cached': {
key: 'span.cached',
name: 'span.cached',
kind: FieldKind.BOOLEAN,
},
},
number: {
'span.duration': {
key: 'span.duration',
name: 'span.duration',
kind: FieldKind.MEASUREMENT,
},
},
string: {
'span.op': {
key: 'span.op',
name: 'span.op',
kind: FieldKind.TAG,
},
},
};

function makeValidationData(
fields: EventValidationData['query']['fields']
): EventValidationData {
return {
dataset: [],
environment: [],
field: [],
orderby: [],
projects: [],
query: {error: null, fields, valid: true},
valid: false,
};
}

describe('useSpanSearchQueryBuilderProps', () => {
beforeEach(() => {
mockUseSpanItemAttributes.mockImplementation((_params, type) => ({
attributes: spanAttributesByType[type as keyof typeof spanAttributesByType],
isLoading: false,
secondaryAliases: {},
}));
mockUseTraceItemSearchQueryBuilderProps.mockClear();
});

it('adds valid validation-only query keys and forwards invalid query keys', () => {
const validatedSearchQueryData = makeValidationData([
{attrType: 'boolean', error: null, name: 'custom.flag', valid: true},
{attrType: 'number', error: null, name: 'custom.duration', valid: true},
{attrType: 'string', error: null, name: 'custom.tag', valid: true},
{attrType: 'string', error: null, name: 'span.op', valid: true},
{attrType: null, error: 'unknown attribute', name: 'missing.key', valid: false},
]);

const {result} = renderHook(() =>
useSpanSearchQueryBuilderProps({
initialQuery: 'custom.tag:value missing.key:value',
searchSource: 'test',
validatedSearchQueryData,
})
);

expect(result.current.spanSearchQueryBuilderProps.booleanAttributes).toMatchObject({
'custom.flag': {key: 'custom.flag', kind: FieldKind.BOOLEAN},
'span.cached': {key: 'span.cached', kind: FieldKind.BOOLEAN},
});
expect(result.current.spanSearchQueryBuilderProps.numberAttributes).toMatchObject({
'custom.duration': {key: 'custom.duration', kind: FieldKind.MEASUREMENT},
'span.duration': {key: 'span.duration', kind: FieldKind.MEASUREMENT},
});
expect(result.current.spanSearchQueryBuilderProps.stringAttributes).toMatchObject({
'custom.tag': {key: 'custom.tag', kind: FieldKind.TAG},
'span.op': {key: 'span.op', kind: FieldKind.TAG},
});
expect(result.current.spanSearchQueryBuilderProps.invalidFilterKeys).toEqual([
'missing.key',
]);

expect(mockUseTraceItemSearchQueryBuilderProps).toHaveBeenCalledWith(
expect.objectContaining({
invalidFilterKeys: ['missing.key'],
stringAttributes: expect.objectContaining({
'custom.tag': expect.objectContaining({kind: FieldKind.TAG}),
'span.op': expect.objectContaining({kind: FieldKind.TAG}),
}),
})
);
});
});
135 changes: 97 additions & 38 deletions static/app/components/performance/spanSearchQueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import type {CaseInsensitive} from 'sentry/components/searchQueryBuilder/hooks';
import type {CallbackSearchState} from 'sentry/components/searchQueryBuilder/types';
import type {PageFilters} from 'sentry/types/core';
import type {TagCollection} from 'sentry/types/group';
import {type AggregationKey} from 'sentry/utils/fields';
import {FieldKind, type AggregationKey} from 'sentry/utils/fields';
import {prettifyAttributeName} from 'sentry/views/explore/components/traceItemAttributes/utils';
import {
useTraceItemSearchQueryBuilderProps,
type TraceItemSearchQueryBuilderProps,
} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
import {useSpanItemAttributes} from 'sentry/views/explore/hooks/useTraceItemAttributes';
import {TraceItemDataset} from 'sentry/views/explore/types';
import type {EventValidationData} from 'sentry/views/explore/utils/validateEventParamsOptions';
import {SpanFields} from 'sentry/views/insights/types';

export interface UseSpanSearchQueryBuilderProps {
Expand All @@ -32,6 +34,7 @@ export interface UseSpanSearchQueryBuilderProps {
projects?: PageFilters['projects'];
supportedAggregates?: AggregationKey[];
useEap?: boolean;
validatedSearchQueryData?: EventValidationData;
}
export interface SpanSearchQueryBuilderProps extends UseSpanSearchQueryBuilderProps {
booleanAttributes: TagCollection;
Expand All @@ -51,64 +54,120 @@ export function useSpanSearchQueryBuilderProps(props: UseSpanSearchQueryBuilderP
spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps;
spanSearchQueryBuilderProviderProps: UseTraceItemSearchQueryBuilderPropsReturnType;
} {
const {attributes: numberAttributes, secondaryAliases: numberSecondaryAliases} =
const {attributes: spanBooleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useSpanItemAttributes({}, 'boolean');
const {attributes: spanNumberAttributes, secondaryAliases: numberSecondaryAliases} =
useSpanItemAttributes({}, 'number');
const {attributes: stringAttributes, secondaryAliases: stringSecondaryAliases} =
const {attributes: spanStringAttributes, secondaryAliases: stringSecondaryAliases} =
useSpanItemAttributes({}, 'string');
const {attributes: booleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useSpanItemAttributes({}, 'boolean');

const stringAttributesWithSemver = useMemo(() => {
if (SpanFields.RELEASE in stringAttributes) {
const spanStringAttributesWithSemver = useMemo(() => {
if (SpanFields.RELEASE in spanStringAttributes) {
return {
...stringAttributes,
...spanStringAttributes,
...STATIC_SEMVER_TAGS,
};
}
return stringAttributes;
}, [stringAttributes]);
return spanStringAttributes;
}, [spanStringAttributes]);

const spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps = useMemo(
() => ({
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes: stringAttributesWithSemver,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
}),
[
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
numberSecondaryAliases,
props,
stringAttributesWithSemver,
stringSecondaryAliases,
]
);
const {booleanAttributes, numberAttributes, stringAttributes, invalidFilterKeys} =
useMemo(() => {
const localInvalidFilterKeys: string[] = [];
const localBooleanAttributes = {...spanBooleanAttributes};
const localNumberAttributes = {...spanNumberAttributes};
const localStringAttributes = {...spanStringAttributesWithSemver};

if (props.validatedSearchQueryData?.query.fields.length) {
for (const item of props.validatedSearchQueryData.query.fields) {
if (item.valid) {
if (item.attrType === 'boolean' && item.name) {
localBooleanAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.BOOLEAN,
};
}

if (item.attrType === 'number' && item.name) {
localNumberAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.MEASUREMENT,
};
}

if (item.attrType === 'string' && item.name) {
localStringAttributes[item.name] ??= {
key: item.name,
name: prettifyAttributeName(item.name),
kind: FieldKind.TAG,
};
}

continue;
}

if (item.name) {
localInvalidFilterKeys.push(item.name);
}
}
}

return {
booleanAttributes: localBooleanAttributes,
numberAttributes: localNumberAttributes,
stringAttributes: localStringAttributes,
invalidFilterKeys: localInvalidFilterKeys,
};
}, [
props.validatedSearchQueryData?.query.fields,
spanBooleanAttributes,
spanNumberAttributes,
spanStringAttributesWithSemver,
]);

const spanSearchQueryBuilderProviderProps = useTraceItemSearchQueryBuilderProps({
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes: stringAttributesWithSemver,
stringAttributes,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
onCaseInsensitiveClick: props.onCaseInsensitiveClick,
invalidFilterKeys,
});

return useMemo(
() => ({
return useMemo(() => {
const spanSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps = {
...props,
itemType: TraceItemDataset.SPANS,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
stringAttributes,
numberSecondaryAliases,
stringSecondaryAliases,
caseInsensitive: props.caseInsensitive ? true : undefined,
invalidFilterKeys,
};

return {
spanSearchQueryBuilderProps,
spanSearchQueryBuilderProviderProps,
}),
[spanSearchQueryBuilderProps, spanSearchQueryBuilderProviderProps]
);
};
}, [
booleanAttributes,
booleanSecondaryAliases,
invalidFilterKeys,
numberAttributes,
numberSecondaryAliases,
props,
spanSearchQueryBuilderProviderProps,
stringAttributes,
stringSecondaryAliases,
]);
}
Loading
Loading