import {useCallback, useMemo} from 'react'; import omit from 'lodash/omit'; import {fetchSpanFieldValues, fetchTagValues} from 'sentry/actionCreators/tags'; import { 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 'sentry/components/events/searchBarFieldConstants'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import type { CallbackSearchState, FilterKeySection, } from 'sentry/components/searchQueryBuilder/types'; import {t} from 'sentry/locale'; import {SavedSearchType, type TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils'; import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import type {Field} from 'sentry/utils/discover/fields'; import { ALL_INSIGHTS_FILTER_KEY_SECTIONS, COMBINED_DATASET_FILTER_KEY_SECTIONS, ERRORS_DATASET_FILTER_KEY_SECTIONS, isAggregateField, isEquation, isMeasurement, parseFunction, } 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 type Measurements from 'sentry/utils/measurements/measurements'; import {getMeasurements} from 'sentry/utils/measurements/measurements'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useTags from 'sentry/utils/useTags'; import {isCustomMeasurement} from 'sentry/views/dashboards/utils'; const getFunctionTags = (fields: Readonly | undefined) => { if (!fields?.length) { return []; } return fields.reduce((acc, item) => { if ( !STATIC_FIELD_TAGS_SET.has(item.field) && !isEquation(item.field) && !isCustomMeasurement(item.field) ) { const parsedFunction = parseFunction(item.field); if (parsedFunction) { acc[parsedFunction.name] = { key: parsedFunction.name, name: parsedFunction.name, kind: FieldKind.FUNCTION, }; } } return acc; }, {}); }; const getMeasurementTags = ( measurements: Parameters< React.ComponentProps['children'] >[0]['measurements'], customMeasurements: | Parameters['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); }; 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, }); type Props = { customMeasurements?: CustomMeasurementCollection; dataset?: DiscoverDatasets; fields?: Readonly; includeSessionTagsValues?: boolean; includeTransactions?: boolean; omitTags?: string[]; onChange?: (query: string, state: CallbackSearchState) => void; onSearch?: (query: string) => void; placeholder?: string; projectIds?: number[] | Readonly; query?: string; searchSource?: string; supportedTags?: TagCollection | undefined; }; const EXCLUDED_FILTER_KEYS = [FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT]; function ResultsSearchQueryBuilder(props: Props) { const { omitTags, fields, projectIds, includeSessionTagsValues, customMeasurements, dataset, includeTransactions = true, placeholder, } = props; const api = useApi(); const organization = useOrganization(); const {selection} = usePageFilters(); const tags = useTags(); const filteredTags = useMemo(() => { return omitTags && omitTags.length > 0 ? omit(tags, omitTags, EXCLUDED_FILTER_KEYS) : omit(tags, EXCLUDED_FILTER_KEYS); }, [tags, omitTags]); const placeholderText = useMemo(() => { return placeholder ?? t('Search for events, users, tags, and more'); }, [placeholder]); const measurements = useMemo(() => getMeasurements(), []); const functionTags = useMemo(() => getFunctionTags(fields), [fields]); // Returns array of tag values that substring match `query`; invokes `callback` // with data when ready const getEventFieldValues = useCallback( async (tag, query): Promise => { const projectIdStrings = (projectIds as Readonly[])?.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, endpointParams: normalizeDateTimeParams(selection.datetime), orgSlug: organization.slug, fieldKey: tag.key, search: query, projectIds: projectIdStrings, }) : fetchTagValues({ api, endpointParams: normalizeDateTimeParams(selection.datetime), orgSlug: organization.slug, tagKey: tag.key, search: query, projectIds: projectIdStrings, // 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, }); try { const results = await fetchPromise; return results.filter(({name}) => defined(name)).map(({name}) => name); } catch (error) { throw new Error('Unable to fetch event field values'); } }, [ api, organization, selection.datetime, projectIds, includeTransactions, includeSessionTagsValues, dataset, ] ); const getTagList: TagCollection = useMemo(() => { 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, filteredTags, STATIC_SEMVER_TAGS); combinedTags.has = getHasTag(combinedTags); return combinedTags; }, [ measurements, dataset, customMeasurements, functionTags, filteredTags, organization.features, ]); const filterKeySections = useMemo(() => { const customTagsSection: FilterKeySection = { value: 'custom_fields', label: 'Custom Tags', children: Object.keys(filteredTags), }; if ( dataset === DiscoverDatasets.TRANSACTIONS || dataset === DiscoverDatasets.METRICS_ENHANCED ) { return [...ALL_INSIGHTS_FILTER_KEY_SECTIONS, customTagsSection]; } if (dataset === DiscoverDatasets.ERRORS) { return [...ERRORS_DATASET_FILTER_KEY_SECTIONS, customTagsSection]; } return [...COMBINED_DATASET_FILTER_KEY_SECTIONS, customTagsSection]; }, [filteredTags, dataset]); return ( ); } export default ResultsSearchQueryBuilder;