123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- import {useEffect, useMemo} from 'react';
- import memoize from 'lodash/memoize';
- import omit from 'lodash/omit';
- import {fetchSpanFieldValues, fetchTagValues} from 'sentry/actionCreators/tags';
- import type {SearchConfig} from 'sentry/components/searchSyntax/parser';
- import {defaultConfig} from 'sentry/components/searchSyntax/parser';
- import SmartSearchBar from 'sentry/components/smartSearchBar';
- import type {TagCollection} from 'sentry/types/group';
- import {SavedSearchType} from 'sentry/types/group';
- import type {Organization} from 'sentry/types/organization';
- import {defined} from 'sentry/utils';
- import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
- import type {Field} from 'sentry/utils/discover/fields';
- import {isAggregateField, isEquation, isMeasurement} from 'sentry/utils/discover/fields';
- import {
- DiscoverDatasets,
- DiscoverDatasetsToDatasetMap,
- } from 'sentry/utils/discover/types';
- import {
- DEVICE_CLASS_TAG_VALUES,
- FieldKey,
- FieldKind,
- isDeviceClass,
- } from 'sentry/utils/fields';
- import Measurements from 'sentry/utils/measurements/measurements';
- import useApi from 'sentry/utils/useApi';
- import withTags from 'sentry/utils/withTags';
- import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
- import {
- SEARCH_SPECIAL_CHARS_REGEXP,
- STATIC_FIELD_TAGS,
- STATIC_FIELD_TAGS_SET,
- STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS,
- STATIC_FIELD_TAGS_WITHOUT_TRACING,
- STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS,
- STATIC_SEMVER_TAGS,
- STATIC_SPAN_TAGS,
- } from './searchBarFieldConstants';
- const getFunctionTags = (fields: Readonly<Field[]> | undefined) => {
- if (!fields?.length) {
- return [];
- }
- return fields.reduce((acc, item) => {
- if (
- !STATIC_FIELD_TAGS_SET.has(item.field) &&
- !isEquation(item.field) &&
- !isCustomMeasurement(item.field)
- ) {
- acc[item.field] = {key: item.field, name: item.field, kind: FieldKind.FUNCTION};
- }
- return acc;
- }, {});
- };
- const getMeasurementTags = (
- measurements: Parameters<
- React.ComponentProps<typeof Measurements>['children']
- >[0]['measurements'],
- customMeasurements:
- | Parameters<React.ComponentProps<typeof Measurements>['children']>[0]['measurements']
- | undefined
- ) => {
- const measurementsWithKind = Object.keys(measurements).reduce((tags, key) => {
- tags[key] = {
- ...measurements[key],
- kind: FieldKind.MEASUREMENT,
- };
- return tags;
- }, {});
- if (!customMeasurements) {
- return measurementsWithKind;
- }
- return Object.keys(customMeasurements).reduce((tags, key) => {
- tags[key] = {
- ...customMeasurements[key],
- kind: FieldKind.MEASUREMENT,
- };
- return tags;
- }, measurementsWithKind);
- };
- const getSearchConfigFromCustomPerformanceMetrics = (
- customPerformanceMetrics?: CustomMeasurementCollection
- ): Partial<SearchConfig> => {
- if (!customPerformanceMetrics) {
- return {};
- }
- const searchConfigMap: Record<string, string[]> = {
- sizeKeys: [...defaultConfig.sizeKeys],
- durationKeys: [...defaultConfig.durationKeys],
- percentageKeys: [...defaultConfig.percentageKeys],
- numericKeys: [...defaultConfig.numericKeys],
- };
- Object.keys(customPerformanceMetrics).forEach(metricName => {
- const {fieldType} = customPerformanceMetrics[metricName];
- switch (fieldType) {
- case 'size':
- searchConfigMap.sizeKeys.push(metricName);
- break;
- case 'duration':
- searchConfigMap.durationKeys.push(metricName);
- break;
- case 'percentage':
- searchConfigMap.percentageKeys.push(metricName);
- break;
- default:
- searchConfigMap.numericKeys.push(metricName);
- }
- });
- const searchConfig = {
- sizeKeys: new Set(searchConfigMap.sizeKeys),
- durationKeys: new Set(searchConfigMap.durationKeys),
- percentageKeys: new Set(searchConfigMap.percentageKeys),
- numericKeys: new Set(searchConfigMap.numericKeys),
- };
- return searchConfig;
- };
- export const getHasTag = (tags: TagCollection) => ({
- key: FieldKey.HAS,
- name: 'Has property',
- values: Object.keys(tags).sort((a, b) => {
- return a.toLowerCase().localeCompare(b.toLowerCase());
- }),
- predefined: true,
- kind: FieldKind.FIELD,
- });
- export type SearchBarProps = Omit<React.ComponentProps<typeof SmartSearchBar>, 'tags'> & {
- organization: Organization;
- tags: TagCollection;
- customMeasurements?: CustomMeasurementCollection;
- dataset?: DiscoverDatasets;
- fields?: Readonly<Field[]>;
- includeSessionTagsValues?: boolean;
- includeTransactions?: boolean;
- /**
- * Used to define the max height of the menu in px.
- */
- maxMenuHeight?: number;
- maxSearchItems?: React.ComponentProps<typeof SmartSearchBar>['maxSearchItems'];
- omitTags?: string[];
- projectIds?: number[] | Readonly<number[]>;
- savedSearchType?: SavedSearchType;
- supportedTags?: TagCollection | undefined;
- };
- function SearchBar(props: SearchBarProps) {
- const {
- maxSearchItems,
- organization,
- tags,
- omitTags,
- fields,
- projectIds,
- includeSessionTagsValues,
- maxMenuHeight,
- customMeasurements,
- dataset,
- savedSearchType = SavedSearchType.EVENT,
- includeTransactions = true,
- } = props;
- const api = useApi();
- const functionTags = useMemo(() => getFunctionTags(fields), [fields]);
- const tagsWithKind = useMemo(() => {
- return Object.keys(tags).reduce((acc, key) => {
- acc[key] = {
- ...tags[key],
- kind: FieldKind.TAG,
- };
- return acc;
- }, {});
- }, [tags]);
- useEffect(() => {
- // Clear memoized data on mount to make tests more consistent.
- getEventFieldValues.cache.clear?.();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [projectIds]);
- // Returns array of tag values that substring match `query`; invokes `callback`
- // with data when ready
- const getEventFieldValues = memoize(
- (tag, query, endpointParams): Promise<string[]> => {
- const projectIdStrings = (projectIds as Readonly<number>[])?.map(String);
- if (isAggregateField(tag.key) || isMeasurement(tag.key)) {
- // We can't really auto suggest values for aggregate fields
- // or measurements, so we simply don't
- return Promise.resolve([]);
- }
- // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
- // and low search filter values because discover maps device.class to these values.
- if (isDeviceClass(tag.key)) {
- return Promise.resolve(DEVICE_CLASS_TAG_VALUES);
- }
- const fetchPromise =
- dataset === DiscoverDatasets.SPANS_INDEXED
- ? fetchSpanFieldValues({
- api,
- orgSlug: organization.slug,
- fieldKey: tag.key,
- search: query,
- projectIds: projectIdStrings,
- endpointParams,
- })
- : fetchTagValues({
- api,
- orgSlug: organization.slug,
- tagKey: tag.key,
- search: query,
- projectIds: projectIdStrings,
- endpointParams,
- // allows searching for tags on transactions as well
- includeTransactions: includeTransactions,
- // allows searching for tags on sessions as well
- includeSessions: includeSessionTagsValues,
- dataset: dataset ? DiscoverDatasetsToDatasetMap[dataset] : undefined,
- });
- return fetchPromise.then(
- results => results.filter(({name}) => defined(name)).map(({name}) => name),
- () => {
- throw new Error('Unable to fetch event field values');
- }
- );
- },
- ({key}, query) => `${key}-${query}`
- );
- const getTagList = (
- measurements: Parameters<
- React.ComponentProps<typeof Measurements>['children']
- >[0]['measurements']
- ) => {
- const measurementsWithKind = getMeasurementTags(measurements, customMeasurements);
- const orgHasPerformanceView = organization.features.includes('performance-view');
- const combinedTags: TagCollection =
- dataset === DiscoverDatasets.ERRORS
- ? Object.assign({}, functionTags, STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS)
- : dataset === DiscoverDatasets.TRANSACTIONS ||
- dataset === DiscoverDatasets.METRICS_ENHANCED
- ? Object.assign(
- {},
- measurementsWithKind,
- functionTags,
- STATIC_SPAN_TAGS,
- STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS
- )
- : orgHasPerformanceView
- ? Object.assign(
- {},
- measurementsWithKind,
- functionTags,
- STATIC_SPAN_TAGS,
- STATIC_FIELD_TAGS
- )
- : Object.assign({}, STATIC_FIELD_TAGS_WITHOUT_TRACING);
- Object.assign(combinedTags, tagsWithKind, STATIC_SEMVER_TAGS);
- combinedTags.has = getHasTag(combinedTags);
- const list =
- omitTags && omitTags.length > 0 ? omit(combinedTags, omitTags) : combinedTags;
- return list;
- };
- const customPerformanceMetricsSearchConfig = useMemo(
- () => getSearchConfigFromCustomPerformanceMetrics(customMeasurements),
- [customMeasurements]
- );
- return (
- <Measurements>
- {({measurements}) => (
- <SmartSearchBar
- hasRecentSearches
- savedSearchType={savedSearchType}
- projectIds={projectIds}
- onGetTagValues={getEventFieldValues}
- supportedTags={getTagList(measurements)}
- prepareQuery={query => {
- // Prepare query string (e.g. strip special characters like negation operator)
- return query.replace(SEARCH_SPECIAL_CHARS_REGEXP, '');
- }}
- maxSearchItems={maxSearchItems}
- excludedTags={[FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT]}
- maxMenuHeight={maxMenuHeight ?? 300}
- {...customPerformanceMetricsSearchConfig}
- {...props}
- />
- )}
- </Measurements>
- );
- }
- export default withTags(SearchBar);
|