diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx
index 2e392ba9ffb5ac..824ce00844f07c 100644
--- a/static/app/components/searchQueryBuilder/index.spec.tsx
+++ b/static/app/components/searchQueryBuilder/index.spec.tsx
@@ -980,31 +980,43 @@ describe('SearchQueryBuilder', () => {
it('can add a new token by clicking a key suggestion', async () => {
const mockOnChange = jest.fn();
render(, {
- organization: {features: ['search-query-builder-input-flow-changes']},
+ organization: {
+ features: [
+ 'search-query-builder-input-flow-changes',
+ 'search-query-builder-wildcard-operators',
+ 'search-query-builder-default-to-contains',
+ ],
+ },
});
await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
await userEvent.click(screen.getByRole('option', {name: 'browser.name'}));
// New token should be added with the correct key and default value
- expect(screen.getByRole('row', {name: 'browser.name:""'})).toBeInTheDocument();
+ expect(
+ screen.getByRole('row', {name: `browser.name:${WildcardOperators.CONTAINS}""`})
+ ).toBeInTheDocument();
// onChange should not be called until exiting edit mode
expect(mockOnChange).not.toHaveBeenCalled();
// Should have focus on the operator option
- const operatorOption = await screen.findByRole('option', {name: 'is'});
+ const operatorOption = await screen.findByRole('option', {name: 'contains'});
expect(operatorOption).toHaveFocus();
await userEvent.click(operatorOption);
await userEvent.click(await screen.findByRole('option', {name: 'Firefox'}));
// New token should have a value
- expect(screen.getByRole('row', {name: 'browser.name:Firefox'})).toBeInTheDocument();
+ expect(
+ screen.getByRole('row', {
+ name: `browser.name:${WildcardOperators.CONTAINS}Firefox`,
+ })
+ ).toBeInTheDocument();
// Now we call onChange
expect(mockOnChange).toHaveBeenCalledTimes(1);
expect(mockOnChange).toHaveBeenCalledWith(
- 'browser.name:Firefox',
+ `browser.name:${WildcardOperators.CONTAINS}Firefox`,
expect.anything()
);
});
@@ -1127,7 +1139,15 @@ describe('SearchQueryBuilder', () => {
});
it('converts text to filter when typing :', async () => {
- render();
+ render(, {
+ organization: {
+ features: [
+ 'search-query-builder-input-flow-changes',
+ 'search-query-builder-wildcard-operators',
+ 'search-query-builder-default-to-contains',
+ ],
+ },
+ });
await userEvent.click(getLastInput());
await userEvent.type(
@@ -1135,7 +1155,9 @@ describe('SearchQueryBuilder', () => {
'browser.name:'
);
- const browserNameFilter = await screen.findByRole('row', {name: 'browser.name:""'});
+ const browserNameFilter = await screen.findByRole('row', {
+ name: `browser.name:${WildcardOperators.CONTAINS}""`,
+ });
expect(browserNameFilter).toBeInTheDocument();
});
@@ -1155,16 +1177,28 @@ describe('SearchQueryBuilder', () => {
});
it('selects [Filtered] from dropdown', async () => {
- render();
+ render(, {
+ organization: {
+ features: [
+ 'search-query-builder-input-flow-changes',
+ 'search-query-builder-wildcard-operators',
+ 'search-query-builder-default-to-contains',
+ ],
+ },
+ });
await userEvent.click(getLastInput());
await userEvent.type(
screen.getByRole('combobox', {name: 'Add a search term'}),
'message:'
);
+ await userEvent.keyboard('{enter}');
await userEvent.click(screen.getByRole('option', {name: '[Filtered]'}));
+
expect(
- await screen.findByRole('row', {name: 'message:"[Filtered]"'})
+ await screen.findByRole('row', {
+ name: `message:${WildcardOperators.CONTAINS}"[Filtered]"`,
+ })
).toBeInTheDocument();
});
});
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
index 503335c8eb848a..fb6564fb1e4b2f 100644
--- a/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx
@@ -54,6 +54,10 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
getFieldDefinition(getKeyName(token.key))
);
+ const hasWildcardOperators =
+ organization.features.includes('search-query-builder-wildcard-operators') &&
+ organization.features.includes('search-query-builder-default-to-contains');
+
const handleSelectKey = useCallback(
(keyName: string) => {
const newFieldDef = getFieldDefinition(keyName);
@@ -108,7 +112,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
dispatch({
type: 'REPLACE_TOKENS_WITH_TEXT_ON_SELECT',
tokens: [token],
- text: getInitialFilterText(keyName, newFieldDef),
+ text: getInitialFilterText(keyName, newFieldDef, hasWildcardOperators),
focusOverride: {
itemKey: item.key.toString(),
part: 'value',
@@ -122,6 +126,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
currentInputValueRef,
dispatch,
getFieldDefinition,
+ hasWildcardOperators,
item.key,
onCommit,
organization,
diff --git a/static/app/components/searchQueryBuilder/tokens/freeText.tsx b/static/app/components/searchQueryBuilder/tokens/freeText.tsx
index c04dae7e627c65..107772a36f273c 100644
--- a/static/app/components/searchQueryBuilder/tokens/freeText.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/freeText.tsx
@@ -98,12 +98,13 @@ function replaceFocusedWordWithFilter(
value: string,
cursorPosition: number,
key: string,
- getFieldDefinition: FieldDefinitionGetter
+ getFieldDefinition: FieldDefinitionGetter,
+ hasWildcardOperators: boolean
) {
return replaceFocusedWord(
value,
cursorPosition,
- getInitialFilterText(key, getFieldDefinition(key))
+ getInitialFilterText(key, getFieldDefinition(key), hasWildcardOperators)
);
}
@@ -261,6 +262,9 @@ function SearchQueryBuilderInputInternal({
const hasInputChangeFlows = organization.features.includes(
'search-query-builder-input-flow-changes'
);
+ const hasWildcardOperators =
+ organization.features.includes('search-query-builder-wildcard-operators') &&
+ organization.features.includes('search-query-builder-default-to-contains');
const updateSelectionIndex = useCallback(() => {
setSelectionIndex(inputRef.current?.selectionStart ?? 0);
@@ -471,7 +475,8 @@ function SearchQueryBuilderInputInternal({
inputValue,
selectionIndex,
value,
- getFieldDefinition
+ getFieldDefinition,
+ hasWildcardOperators
),
focusOverride: calculateNextFocusForFilter(
state,
@@ -574,7 +579,8 @@ function SearchQueryBuilderInputInternal({
inputValue,
selectionIndex,
filterValue,
- getFieldDefinition
+ getFieldDefinition,
+ hasWildcardOperators
),
focusOverride: calculateNextFocusForFilter(
state,
@@ -617,7 +623,8 @@ function SearchQueryBuilderInputInternal({
inputValue,
selectionIndex,
filterKey,
- getFieldDefinition
+ getFieldDefinition,
+ hasWildcardOperators
),
focusOverride: calculateNextFocusForFilter(
state,
diff --git a/static/app/components/searchQueryBuilder/tokens/utils.tsx b/static/app/components/searchQueryBuilder/tokens/utils.tsx
index 394b7d1578880e..405ad4973752d1 100644
--- a/static/app/components/searchQueryBuilder/tokens/utils.tsx
+++ b/static/app/components/searchQueryBuilder/tokens/utils.tsx
@@ -7,7 +7,11 @@ import type {
SelectOptionOrSectionWithKey,
SelectSectionWithKey,
} from 'sentry/components/core/compactSelect/types';
-import type {ParseResultToken} from 'sentry/components/searchSyntax/parser';
+import {areWildcardOperatorsAllowed} from 'sentry/components/searchQueryBuilder/tokens/filter/utils';
+import {
+ WildcardOperators,
+ type ParseResultToken,
+} from 'sentry/components/searchSyntax/parser';
import {defined} from 'sentry/utils';
import {FieldKind, FieldValueType, type FieldDefinition} from 'sentry/utils/fields';
@@ -113,13 +117,17 @@ function getInitialValueType(fieldDefinition: FieldDefinition | null) {
export function getInitialFilterText(
key: string,
- fieldDefinition: FieldDefinition | null
+ fieldDefinition: FieldDefinition | null,
+ hasWildcardOperators: boolean
) {
const defaultValue = getDefaultFilterValue({fieldDefinition});
const keyText = getInitialFilterKeyText(key, fieldDefinition);
const valueType = getInitialValueType(fieldDefinition);
+ const allowContainsOperator =
+ hasWildcardOperators && areWildcardOperatorsAllowed(fieldDefinition);
+
switch (valueType) {
case FieldValueType.INTEGER:
case FieldValueType.NUMBER:
@@ -127,7 +135,11 @@ export function getInitialFilterText(
case FieldValueType.SIZE:
case FieldValueType.PERCENTAGE:
return `${keyText}:>${defaultValue}`;
- case FieldValueType.STRING:
+ case FieldValueType.STRING: {
+ return allowContainsOperator
+ ? `${keyText}:${WildcardOperators.CONTAINS}${defaultValue}`
+ : `${keyText}:${defaultValue}`;
+ }
default:
return `${keyText}:${defaultValue}`;
}