Skip to content

Commit 7c40b7b

Browse files
authored
Add search support for timesSeen. (#5009)
1 parent 06248c7 commit 7c40b7b

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Version 8.14 (Unreleased)
1818
- Plugins can now add tasks that run in sentry as celery workers.
1919
- Added the ability to verify TLS connections when fetching artifacts.
2020
- Added data migration to merge environments across an organization
21+
- Added ``timesSeen`` keyword to issue search.
2122

2223
API Changes
2324
~~~~~~~~~~~

src/sentry/search/django/backend.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def _build_queryset(self, project, query=None, status=None, tags=None,
7575
date_to=None, date_to_inclusive=True,
7676
active_at_from=None, active_at_from_inclusive=True,
7777
active_at_to=None, active_at_to_inclusive=True,
78+
times_seen=None,
79+
times_seen_lower=None, times_seen_lower_inclusive=True,
80+
times_seen_upper=None, times_seen_upper_inclusive=True,
7881
cursor=None, limit=None):
7982
from sentry.models import Event, Group, GroupSubscription, GroupStatus
8083

@@ -185,6 +188,23 @@ def _build_queryset(self, project, query=None, status=None, tags=None,
185188
params['active_at__lt'] = active_at_to
186189
queryset = queryset.filter(**params)
187190

191+
if times_seen is not None:
192+
queryset = queryset.filter(times_seen=times_seen)
193+
194+
if times_seen_lower is not None or times_seen_upper is not None:
195+
params = {}
196+
if times_seen_lower is not None:
197+
if times_seen_lower_inclusive:
198+
params['times_seen__gte'] = times_seen_lower
199+
else:
200+
params['times_seen__gt'] = times_seen_lower
201+
if times_seen_upper is not None:
202+
if times_seen_upper_inclusive:
203+
params['times_seen__lte'] = times_seen_upper
204+
else:
205+
params['times_seen__lt'] = times_seen_upper
206+
queryset = queryset.filter(**params)
207+
188208
if date_from or date_to:
189209
params = {
190210
'project_id': project.id,

src/sentry/search/utils.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,39 @@ def get_date_params(value, from_field, to_field):
164164
})
165165
return result
166166

167+
168+
numeric_modifiers = [
169+
('>=', lambda field, value: {
170+
'{}_lower'.format(field): value,
171+
'{}_lower_inclusive'.format(field): True,
172+
}),
173+
('<=', lambda field, value: {
174+
'{}_upper'.format(field): value,
175+
'{}_upper_inclusive'.format(field): True,
176+
}),
177+
('>', lambda field, value: {
178+
'{}_lower'.format(field): value,
179+
'{}_lower_inclusive'.format(field): False,
180+
}),
181+
('<', lambda field, value: {
182+
'{}_upper'.format(field): value,
183+
'{}_upper_inclusive'.format(field): False,
184+
}),
185+
]
186+
187+
188+
def get_numeric_field_value(field, raw_value, type=int):
189+
for modifier, function in numeric_modifiers:
190+
if raw_value.startswith(modifier):
191+
return function(
192+
field,
193+
type(raw_value[len(modifier):]),
194+
)
195+
else:
196+
return {
197+
field: type(raw_value),
198+
}
199+
167200
reserved_tag_names = frozenset([
168201
'query',
169202
'is',
@@ -190,7 +223,9 @@ def get_date_params(value, from_field, to_field):
190223
'app',
191224
'os.name',
192225
'url',
193-
'event.timestamp'])
226+
'event.timestamp'
227+
'timesSeen',
228+
])
194229

195230

196231
def tokenize_query(query):
@@ -312,6 +347,8 @@ def parse_query(project, query, user):
312347
project, key.split('.', 1)[1], value)
313348
elif key == 'event.timestamp':
314349
results.update(get_date_params(value, 'date_from', 'date_to'))
350+
elif key == 'timesSeen':
351+
results.update(get_numeric_field_value('times_seen', value))
315352
else:
316353
results['tags'][key] = value
317354

tests/sentry/search/test_utils.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,43 @@
88
from sentry.models import EventUser, GroupStatus, Release
99
from sentry.testutils import TestCase
1010
from sentry.search.base import ANY
11-
from sentry.search.utils import parse_query
11+
from sentry.search.utils import parse_query, get_numeric_field_value
12+
13+
14+
def test_get_numeric_field_value():
15+
assert get_numeric_field_value('foo', '10') == {
16+
'foo': 10,
17+
}
18+
19+
assert get_numeric_field_value('foo', '>10') == {
20+
'foo_lower': 10,
21+
'foo_lower_inclusive': False,
22+
}
23+
24+
assert get_numeric_field_value('foo', '>=10') == {
25+
'foo_lower': 10,
26+
'foo_lower_inclusive': True,
27+
}
28+
29+
assert get_numeric_field_value('foo', '<10') == {
30+
'foo_upper': 10,
31+
'foo_upper_inclusive': False,
32+
}
33+
34+
assert get_numeric_field_value('foo', '<=10') == {
35+
'foo_upper': 10,
36+
'foo_upper_inclusive': True,
37+
}
38+
39+
assert get_numeric_field_value('foo', '>3.5', type=float) == {
40+
'foo_lower': 3.5,
41+
'foo_lower_inclusive': False,
42+
}
43+
44+
assert get_numeric_field_value('foo', '<=-3.5', type=float) == {
45+
'foo_upper': -3.5,
46+
'foo_upper_inclusive': True,
47+
}
1248

1349

1450
class ParseQueryTest(TestCase):

0 commit comments

Comments
 (0)