import { FilterType, joinQuery, parseSearch, type SearchConfig, Token, } from 'sentry/components/searchSyntax/parser'; import {defined} from 'sentry/utils'; import { MetricSeriesFilterUpdateType, type MetricsQuery, } from 'sentry/utils/metrics/types'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {addToFilter, excludeFromFilter} from '../../discover/table/cellAction'; /** * Updates query string with tag values from the passed groupBys. */ export function extendQueryWithGroupBys( query: string, groupBys: (Record | undefined)[] ) { const mutableSearch = new MutableSearch(query); groupBys.forEach(groupBy => { if (!groupBy) { return; } Object.entries(groupBy).forEach(([key, value]) => { if (!value) { return; } addToFilter(mutableSearch, key, value); }); }); return mutableSearch.formatString(); } /** * Wraps text filters of a search string in quotes if they are not already. */ export function ensureQuotedTextFilters( query: string, configOverrides?: Partial ) { const parsedSearch = parseSearch(query, configOverrides); if (!parsedSearch) { return query; } for (let i = 0; i < parsedSearch.length; i++) { const token = parsedSearch[i]; if (token.type === Token.FILTER && token.filter === FilterType.TEXT) { // joinQuery() does not access nested tokens, so we need to manipulate the text of the filter instead of its value if (!token.value.quoted) { token.text = `${token.negated ? '!' : ''}${token.key.text}:"${token.value.text}"`; } const spaceToken = parsedSearch[i + 1]; const afterSpaceToken = parsedSearch[i + 2]; if ( spaceToken && afterSpaceToken && spaceToken.type === Token.SPACES && spaceToken.text === '' && afterSpaceToken.type === Token.FILTER ) { // Ensure there is a space between two filters spaceToken.text = ' '; } } } return joinQuery(parsedSearch); } /** * Used when a user clicks on filter button in the series summary table. Applies * tag values to the filter string of the query. Removes the tags from query groupyBy */ export function updateQueryWithSeriesFilter( query: MetricsQuery, groupBys: Record, updateType: MetricSeriesFilterUpdateType ) { // TODO(metrics): This is a temporary solution to handle the case where the query has OR operator. // since addToFilter and excludeFromFilter do not handle it properly. We should refactor this to use // search syntax parser instead. const queryStr = query.query?.includes('OR') ? `(${query.query})` : query.query ?? ''; const mutableSearch = new MutableSearch(queryStr); const groupByEntries = Object.entries(groupBys); groupByEntries.forEach(([key, value]) => { if (!defined(value)) { return; } updateType === MetricSeriesFilterUpdateType.ADD ? addToFilter(mutableSearch, key, value) : excludeFromFilter(mutableSearch, key, value); }); const extendedQuery = mutableSearch.formatString(); const newGroupBy = (query.groupBy ?? []).filter(tag => !groupBys[tag]); return { ...query, query: ensureQuotedTextFilters(extendedQuery), groupBy: newGroupBy, }; }