Skip to content

Commit 1425da6

Browse files
committed
Merge branch 'master' into nikki/discover-split-self-hosted
2 parents b293f6d + 24d0105 commit 1425da6

File tree

12 files changed

+173
-9
lines changed

12 files changed

+173
-9
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nodestore: 0001_squashed_0002_nodestore_no_dictfield
2121

2222
replays: 0001_squashed_0005_drop_replay_index
2323

24-
sentry: 0911_split_discover_dataset_dashboards_self_hosted
24+
sentry: 0912_split_discover_dataset_dashboards_self_hosted
2525

2626
social_auth: 0001_squashed_0002_default_auto_field
2727

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 5.2.1 on 2025-05-21 23:05
2+
3+
from django.db import migrations, models
4+
5+
import sentry.db.models.fields.citext
6+
from sentry.new_migrations.migrations import CheckedMigration
7+
8+
9+
class Migration(CheckedMigration):
10+
# This flag is used to mark that a migration shouldn't be automatically run in production.
11+
# This should only be used for operations where it's safe to run the migration after your
12+
# code has deployed. So this should not be used for most operations that alter the schema
13+
# of a table.
14+
# Here are some things that make sense to mark as post deployment:
15+
# - Large data migrations. Typically we want these to be run manually so that they can be
16+
# monitored and not block the deploy for a long period of time while they run.
17+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
18+
# run this outside deployments so that we don't block them. Note that while adding an index
19+
# is a schema change, it's completely safe to run the operation after the code has deployed.
20+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
21+
22+
is_post_deployment = False
23+
24+
dependencies = [
25+
("sentry", "0910_make_organizationmemberteam_is_active_default"),
26+
]
27+
28+
operations = [
29+
migrations.AlterField(
30+
model_name="email",
31+
name="email",
32+
field=sentry.db.models.fields.citext.CIEmailField(max_length=200, unique=True),
33+
),
34+
migrations.AlterField(
35+
model_name="useremail",
36+
name="email",
37+
field=models.EmailField(max_length=200),
38+
),
39+
]

src/sentry/users/models/email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Email(Model):
2222
__relocation_dependencies__ = {"sentry.User"}
2323
__relocation_custom_ordinal__ = ["email"]
2424

25-
email = CIEmailField(_("email address"), unique=True, max_length=75)
25+
email = CIEmailField(_("email address"), unique=True, max_length=200)
2626
date_added = models.DateTimeField(default=timezone.now)
2727

2828
class Meta:

src/sentry/users/models/useremail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class UserEmail(ControlOutboxProducingModel):
5959
__relocation_custom_ordinal__ = ["user", "email"]
6060

6161
user = FlexibleForeignKey(settings.AUTH_USER_MODEL, related_name="emails")
62-
email = models.EmailField(_("email address"), max_length=75)
62+
email = models.EmailField(_("email address"), max_length=200)
6363
validation_hash = models.CharField(max_length=32, default=get_secure_token)
6464
date_hash_added = models.DateTimeField(default=timezone.now)
6565
is_verified = models.BooleanField(

static/app/components/searchQueryBuilder/context.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface SearchQueryBuilderContextData {
3636
filterKeys: TagCollection;
3737
focusOverride: FocusOverride | null;
3838
getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null;
39+
getSuggestedFilterKey: (key: string) => string | null;
3940
getTagValues: (tag: Tag, query: string) => Promise<string[]>;
4041
handleSearch: (query: string) => void;
4142
parseQuery: (query: string) => ParseResult | null;
@@ -78,6 +79,7 @@ export function SearchQueryBuilderProvider({
7879
filterKeys,
7980
filterKeyMenuWidth = 360,
8081
filterKeySections,
82+
getSuggestedFilterKey,
8183
getTagValues,
8284
onSearch,
8385
placeholder,
@@ -139,6 +141,7 @@ export function SearchQueryBuilderProvider({
139141
filterKeySections: filterKeySections ?? [],
140142
filterKeyMenuWidth,
141143
filterKeys,
144+
getSuggestedFilterKey: getSuggestedFilterKey ?? ((key: string) => key),
142145
getTagValues,
143146
getFieldDefinition: fieldDefinitionGetter,
144147
dispatch,
@@ -160,6 +163,7 @@ export function SearchQueryBuilderProvider({
160163
filterKeySections,
161164
filterKeyMenuWidth,
162165
filterKeys,
166+
getSuggestedFilterKey,
163167
getTagValues,
164168
fieldDefinitionGetter,
165169
dispatch,

static/app/components/searchQueryBuilder/index.spec.tsx

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ const FILTER_KEYS: TagCollection = {
9090
key: 'tags[foo,string]',
9191
name: 'foo',
9292
},
93-
'tags[bar,string]': {
94-
key: 'tags[bar,string]',
93+
'tags[bar,number]': {
94+
key: 'tags[bar,number]',
9595
name: 'bar',
9696
},
9797
};
@@ -3458,4 +3458,90 @@ describe('SearchQueryBuilder', function () {
34583458
expect(option).toHaveTextContent('bar');
34593459
});
34603460
});
3461+
3462+
describe('autocomplete using suggestions', function () {
3463+
function getSuggestedFilterKey(key: string) {
3464+
if (key === 'foo') {
3465+
return 'tags[foo,string]';
3466+
}
3467+
3468+
if (key === 'bar') {
3469+
return 'tags[bar,number]';
3470+
}
3471+
3472+
return key;
3473+
}
3474+
3475+
it('replace string key with suggestion when autocompleting', async function () {
3476+
render(
3477+
<SearchQueryBuilder
3478+
{...defaultProps}
3479+
getSuggestedFilterKey={getSuggestedFilterKey}
3480+
/>
3481+
);
3482+
3483+
await userEvent.click(getLastInput());
3484+
await userEvent.keyboard('foo:');
3485+
await userEvent.keyboard('{Escape}');
3486+
3487+
expect(
3488+
screen.getByRole('button', {name: 'Edit key for filter: tags[foo,string]'})
3489+
).toHaveTextContent('foo');
3490+
});
3491+
3492+
it('replace number key with suggestion when autocompleting', async function () {
3493+
render(
3494+
<SearchQueryBuilder
3495+
{...defaultProps}
3496+
getSuggestedFilterKey={getSuggestedFilterKey}
3497+
/>
3498+
);
3499+
3500+
await userEvent.click(getLastInput());
3501+
await userEvent.keyboard('bar:');
3502+
await userEvent.keyboard('{Escape}');
3503+
3504+
expect(
3505+
screen.getByRole('button', {name: 'Edit key for filter: tags[bar,number]'})
3506+
).toHaveTextContent('bar');
3507+
});
3508+
3509+
it('replaces string key with suggestion on enter', async function () {
3510+
render(
3511+
<SearchQueryBuilder
3512+
{...defaultProps}
3513+
initialQuery="browser.name:firefox"
3514+
getSuggestedFilterKey={getSuggestedFilterKey}
3515+
/>
3516+
);
3517+
3518+
await userEvent.click(
3519+
screen.getByRole('button', {name: 'Edit key for filter: browser.name'})
3520+
);
3521+
await userEvent.keyboard('foo{Enter}{Escape}');
3522+
3523+
expect(
3524+
screen.getByRole('button', {name: 'Edit key for filter: tags[foo,string]'})
3525+
).toHaveTextContent('foo');
3526+
});
3527+
3528+
it('replaces number key with suggestion on enter', async function () {
3529+
render(
3530+
<SearchQueryBuilder
3531+
{...defaultProps}
3532+
initialQuery="browser.name:firefox"
3533+
getSuggestedFilterKey={getSuggestedFilterKey}
3534+
/>
3535+
);
3536+
3537+
await userEvent.click(
3538+
screen.getByRole('button', {name: 'Edit key for filter: browser.name'})
3539+
);
3540+
await userEvent.keyboard('bar{Enter}{Escape}');
3541+
3542+
expect(
3543+
screen.getByRole('button', {name: 'Edit key for filter: tags[bar,number]'})
3544+
).toHaveTextContent('bar');
3545+
});
3546+
});
34613547
});

static/app/components/searchQueryBuilder/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export interface SearchQueryBuilderProps {
8181
* will only render a warning if the value is truthy
8282
*/
8383
getFilterTokenWarning?: (key: string) => React.ReactNode;
84+
/**
85+
* This is used when a user types in a search key and submits the token.
86+
* The submission happens when the user types a colon or presses enter.
87+
* When this happens, this function is used to map the user input to a
88+
* known column.
89+
*/
90+
getSuggestedFilterKey?: (key: string) => string | null;
8491
/**
8592
* Allows for customization of the invalid token messages.
8693
*/

static/app/components/searchQueryBuilder/tokens/filter/filterKeyCombobox.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
3333
inputValue,
3434
includeSuggestions: false,
3535
});
36-
const {dispatch, getFieldDefinition} = useSearchQueryBuilder();
36+
const {dispatch, getFieldDefinition, getSuggestedFilterKey} = useSearchQueryBuilder();
3737

3838
const currentFilterValueType = getFilterValueType(
3939
token,
@@ -88,6 +88,13 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
8888
[handleSelectKey]
8989
);
9090

91+
const onValueCommited = useCallback(
92+
(keyName: string) => {
93+
handleSelectKey(getSuggestedFilterKey(keyName) ?? keyName);
94+
},
95+
[handleSelectKey, getSuggestedFilterKey]
96+
);
97+
9198
const onCustomValueBlurred = useCallback(() => {
9299
onCommit();
93100
}, [onCommit]);
@@ -103,7 +110,7 @@ export function FilterKeyCombobox({token, onCommit, item}: KeyComboboxProps) {
103110
items={sortedFilterKeys}
104111
placeholder={getKeyLabel(token.key)}
105112
onOptionSelected={onOptionSelected}
106-
onCustomValueCommitted={handleSelectKey}
113+
onCustomValueCommitted={onValueCommited}
107114
onCustomValueBlurred={onCustomValueBlurred}
108115
onExit={onExit}
109116
inputValue={inputValue}

static/app/components/searchQueryBuilder/tokens/freeText.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ function SearchQueryBuilderInputInternal({
254254
filterKeys,
255255
dispatch,
256256
getFieldDefinition,
257+
getSuggestedFilterKey,
257258
handleSearch,
258259
placeholder,
259260
searchSource,
@@ -516,7 +517,7 @@ function SearchQueryBuilderInputInternal({
516517
textToken.type === Token.FILTER && textToken.key.text === filterValue
517518
)
518519
) {
519-
const filterKey = filterValue;
520+
const filterKey = getSuggestedFilterKey(filterValue) ?? filterValue;
520521
const key = filterKeys[filterKey];
521522
dispatch({
522523
type: 'UPDATE_FREE_TEXT',

static/app/views/explore/components/traceItemSearchQueryBuilder.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useMemo} from 'react';
1+
import {useCallback, useMemo} from 'react';
22

33
import {getHasTag} from 'sentry/components/events/searchBar';
44
import type {EAPSpanSearchQueryBuilderProps} from 'sentry/components/performance/spanSearchQueryBuilder';
@@ -76,6 +76,25 @@ export function useSearchQueryBuilderProps({
7676
projectIds: projects,
7777
});
7878

79+
const getSuggestedFilterKey = useCallback(
80+
(key: string) => {
81+
// prioritize exact matches first
82+
if (filterTags.hasOwnProperty(key)) {
83+
return key;
84+
}
85+
86+
// try to see if there's numeric attribute by the same name
87+
const explicitNumberTag = `tags[${key},number]`;
88+
if (filterTags.hasOwnProperty(explicitNumberTag)) {
89+
return explicitNumberTag;
90+
}
91+
92+
// give up, and fall back to the default behaviour
93+
return null;
94+
},
95+
[filterTags]
96+
);
97+
7998
return {
8099
searchOnChange: organization.features.includes('ui-search-on-change'),
81100
placeholder: placeholderText,
@@ -88,6 +107,7 @@ export function useSearchQueryBuilderProps({
88107
getFilterTokenWarning,
89108
searchSource,
90109
filterKeySections,
110+
getSuggestedFilterKey,
91111
getTagValues: getTraceItemAttributeValues,
92112
disallowUnsupportedFilters: true,
93113
recentSearches: itemTypeToRecentSearches(itemType),

0 commit comments

Comments
 (0)