test_utils.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  1. from datetime import UTC, datetime, timedelta
  2. import pytest
  3. from django.utils import timezone
  4. from sentry.models.group import GroupStatus
  5. from sentry.models.release import Release
  6. from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
  7. from sentry.models.releases.release_project import ReleaseProject
  8. from sentry.models.team import Team
  9. from sentry.search.base import ANY
  10. from sentry.search.utils import (
  11. DEVICE_CLASS,
  12. InvalidQuery,
  13. LatestReleaseOrders,
  14. convert_user_tag_to_query,
  15. get_first_last_release_for_group,
  16. get_latest_release,
  17. get_numeric_field_value,
  18. parse_duration,
  19. parse_numeric_value,
  20. parse_query,
  21. parse_size,
  22. tokenize_query,
  23. )
  24. from sentry.testutils.cases import APITestCase, SnubaTestCase, TestCase
  25. from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
  26. from sentry.testutils.silo import control_silo_test
  27. from sentry.users.services.user.model import RpcUser
  28. from sentry.users.services.user.serial import serialize_rpc_user
  29. from sentry.users.services.user.service import user_service
  30. def test_get_numeric_field_value():
  31. assert get_numeric_field_value("foo", "10") == {"foo": 10}
  32. assert get_numeric_field_value("foo", ">10") == {"foo_lower": 10, "foo_lower_inclusive": False}
  33. assert get_numeric_field_value("foo", ">=10") == {"foo_lower": 10, "foo_lower_inclusive": True}
  34. assert get_numeric_field_value("foo", "<10") == {"foo_upper": 10, "foo_upper_inclusive": False}
  35. assert get_numeric_field_value("foo", "<=10") == {"foo_upper": 10, "foo_upper_inclusive": True}
  36. assert get_numeric_field_value("foo", ">3.5", type=float) == {
  37. "foo_lower": 3.5,
  38. "foo_lower_inclusive": False,
  39. }
  40. assert get_numeric_field_value("foo", "<=-3.5", type=float) == {
  41. "foo_upper": -3.5,
  42. "foo_upper_inclusive": True,
  43. }
  44. class TestParseNumericValue(TestCase):
  45. def test_simple(self):
  46. assert parse_numeric_value("10", None) == 10
  47. def test_k(self):
  48. assert parse_numeric_value("1", "k") == 1000.0
  49. assert parse_numeric_value("1", "K") == 1000.0
  50. def test_m(self):
  51. assert parse_numeric_value("1", "m") == 1000000.0
  52. assert parse_numeric_value("1", "M") == 1000000.0
  53. def test_b(self):
  54. assert parse_numeric_value("1", "b") == 1000000000.0
  55. assert parse_numeric_value("1", "B") == 1000000000.0
  56. class TestParseSizeValue(TestCase):
  57. def test_simple(self):
  58. assert parse_size("8", "bit") == 1
  59. def test_uppercase(self):
  60. assert parse_size("1", "KB") == 1000
  61. assert parse_size("1", "kb") == 1000
  62. assert parse_size("1", "Kb") == 1000
  63. class TestParseDuration(TestCase):
  64. def test_ms(self):
  65. assert parse_duration("123", "ms") == 123
  66. def test_sec(self):
  67. assert parse_duration("456", "s") == 456000
  68. def test_minutes(self):
  69. assert parse_duration("789", "min") == 789 * 60 * 1000
  70. assert parse_duration("789", "m") == 789 * 60 * 1000
  71. def test_hours(self):
  72. assert parse_duration("234", "hr") == 234 * 60 * 60 * 1000
  73. assert parse_duration("234", "h") == 234 * 60 * 60 * 1000
  74. def test_days(self):
  75. assert parse_duration("567", "day") == 567 * 24 * 60 * 60 * 1000
  76. assert parse_duration("567", "d") == 567 * 24 * 60 * 60 * 1000
  77. def test_weeks(self):
  78. assert parse_duration("890", "wk") == 890 * 7 * 24 * 60 * 60 * 1000
  79. assert parse_duration("890", "w") == 890 * 7 * 24 * 60 * 60 * 1000
  80. def test_errors(self):
  81. with pytest.raises(InvalidQuery):
  82. parse_duration("test", "ms")
  83. with pytest.raises(InvalidQuery):
  84. parse_duration("123", "test")
  85. def test_large_durations(self):
  86. max_duration = 999999999 * 24 * 60 * 60 * 1000
  87. assert parse_duration("999999999", "d") == max_duration
  88. assert parse_duration(str(999999999 * 24), "h") == max_duration
  89. assert parse_duration(str(999999999 * 24 * 60), "m") == max_duration
  90. assert parse_duration(str(999999999 * 24 * 60 * 60), "s") == max_duration
  91. assert parse_duration(str(999999999 * 24 * 60 * 60 * 1000), "ms") == max_duration
  92. def test_overflow_durations(self):
  93. with pytest.raises(InvalidQuery):
  94. assert parse_duration(str(999999999 + 1), "d")
  95. with pytest.raises(InvalidQuery):
  96. assert parse_duration(str((999999999 + 1) * 24), "h")
  97. with pytest.raises(InvalidQuery):
  98. assert parse_duration(str((999999999 + 1) * 24 * 60 + 1), "m")
  99. with pytest.raises(InvalidQuery):
  100. assert parse_duration(str((999999999 + 1) * 24 * 60 * 60 + 1), "s")
  101. with pytest.raises(InvalidQuery):
  102. assert parse_duration(str((999999999 + 1) * 24 * 60 * 60 * 1000 + 1), "ms")
  103. def test_tokenize_query_only_keyed_fields():
  104. tests = [
  105. ("a:a", {"a": ["a"]}),
  106. ("(a:a AND b:b)", {"a": ["a"], "b": ["b"]}),
  107. ("( a:a AND (b:b OR c:c))", {"a": ["a"], "b": ["b"], "c": ["c"]}),
  108. ("( a:a AND (b:b OR c:c ) )", {"a": ["a"], "b": ["b"], "c": ["c"]}),
  109. (
  110. "(x y a:a AND (b:b OR c:c) z)",
  111. {"a": ["a"], "b": ["b"], "c": ["c"], "query": ["x", "y", "z"]},
  112. ),
  113. (
  114. "((x y)) a:a AND (b:b OR c:c) z)",
  115. {"a": ["a"], "b": ["b"], "c": ["c"], "query": ["x", "y", "z"]},
  116. ),
  117. (
  118. "((x y)) a():>a AND (!b:b OR c():<c) z)",
  119. {"a()": [">a"], "!b": ["b"], "c()": ["<c"], "query": ["x", "y", "z"]},
  120. ),
  121. ('a:"\\"a\\""', {"a": ['\\"a\\"']}),
  122. (
  123. 'a:"i \\" quote" b:"b\\"bb" c:"cc"',
  124. {"a": ['i \\" quote'], "b": ['b\\"bb'], "c": ["cc"]},
  125. ),
  126. ]
  127. for test in tests:
  128. assert tokenize_query(test[0]) == test[1], test[0]
  129. def test_get_numeric_field_value_invalid():
  130. with pytest.raises(InvalidQuery):
  131. get_numeric_field_value("foo", ">=1k")
  132. class ParseQueryTest(APITestCase, SnubaTestCase):
  133. @property
  134. def rpc_user(self):
  135. return user_service.get_user(user_id=self.user.id)
  136. @property
  137. def current_rpc_user(self):
  138. # This doesn't include useremails. Used in filters
  139. # where the current user is passed back
  140. return serialize_rpc_user(self.user)
  141. def parse_query(self, query):
  142. return parse_query([self.project], query, self.user, [])
  143. def test_simple(self):
  144. result = self.parse_query("foo bar")
  145. assert result == {"tags": {}, "query": "foo bar"}
  146. def test_useless_prefix(self):
  147. result = self.parse_query("foo: bar")
  148. assert result == {"tags": {}, "query": "foo: bar"}
  149. def test_useless_prefix_with_symbol(self):
  150. result = self.parse_query("foo: @ba$r")
  151. assert result == {"tags": {}, "query": "foo: @ba$r"}
  152. def test_useless_prefix_with_colon(self):
  153. result = self.parse_query("foo: :ba:r::foo:")
  154. assert result == {"tags": {}, "query": "foo: :ba:r::foo:"}
  155. def test_handles_space_separation_after_useless_prefix_exception(self):
  156. result = self.parse_query("foo: bar foo:bar")
  157. assert result == {"tags": {"foo": "bar"}, "query": "foo: bar"}
  158. def test_handles_period_in_tag_key(self):
  159. result = self.parse_query("foo.bar:foobar")
  160. assert result == {"tags": {"foo.bar": "foobar"}, "query": ""}
  161. def test_handles_dash_in_tag_key(self):
  162. result = self.parse_query("foo-bar:foobar")
  163. assert result == {"tags": {"foo-bar": "foobar"}, "query": ""}
  164. # TODO: update docs to include minutes, days, and weeks suffixes
  165. @freeze_time("2016-01-01")
  166. def test_age_tag_negative_value(self):
  167. start = timezone.now()
  168. expected = start - timedelta(hours=12)
  169. result = self.parse_query("age:-12h")
  170. assert result == {"tags": {}, "query": "", "age_from": expected, "age_from_inclusive": True}
  171. @freeze_time("2016-01-01")
  172. def test_age_tag_positive_value(self):
  173. start = timezone.now()
  174. expected = start - timedelta(hours=12)
  175. result = self.parse_query("age:+12h")
  176. assert result == {"tags": {}, "query": "", "age_to": expected, "age_to_inclusive": True}
  177. @freeze_time("2016-01-01")
  178. def test_age_tag_weeks(self):
  179. start = timezone.now()
  180. expected = start - timedelta(days=35)
  181. result = self.parse_query("age:+5w")
  182. assert result == {"tags": {}, "query": "", "age_to": expected, "age_to_inclusive": True}
  183. @freeze_time("2016-01-01")
  184. def test_age_tag_days(self):
  185. start = timezone.now()
  186. expected = start - timedelta(days=10)
  187. result = self.parse_query("age:+10d")
  188. assert result == {"tags": {}, "query": "", "age_to": expected, "age_to_inclusive": True}
  189. @freeze_time("2016-01-01")
  190. def test_age_tag_hours(self):
  191. start = timezone.now()
  192. expected = start - timedelta(hours=10)
  193. result = self.parse_query("age:+10h")
  194. assert result == {"tags": {}, "query": "", "age_to": expected, "age_to_inclusive": True}
  195. @freeze_time("2016-01-01")
  196. def test_age_tag_minutes(self):
  197. start = timezone.now()
  198. expected = start - timedelta(minutes=30)
  199. result = self.parse_query("age:+30m")
  200. assert result == {"tags": {}, "query": "", "age_to": expected, "age_to_inclusive": True}
  201. @freeze_time("2016-01-01")
  202. def test_two_age_tags(self):
  203. start = timezone.now()
  204. expected_to = start - timedelta(hours=12)
  205. expected_from = start - timedelta(hours=24)
  206. result = self.parse_query("age:+12h age:-24h")
  207. assert result == {
  208. "tags": {},
  209. "query": "",
  210. "age_to": expected_to,
  211. "age_from": expected_from,
  212. "age_to_inclusive": True,
  213. "age_from_inclusive": True,
  214. }
  215. def test_event_timestamp_syntax(self):
  216. result = self.parse_query("event.timestamp:2016-01-02")
  217. assert result == {
  218. "query": "",
  219. "date_from": datetime(2016, 1, 2, tzinfo=UTC),
  220. "date_from_inclusive": True,
  221. "date_to": datetime(2016, 1, 3, tzinfo=UTC),
  222. "date_to_inclusive": False,
  223. "tags": {},
  224. }
  225. def test_times_seen_syntax(self):
  226. result = self.parse_query("timesSeen:10")
  227. assert result == {"tags": {}, "times_seen": 10, "query": ""}
  228. # TODO: query parser for '>' timestamp should set inclusive to False.
  229. @pytest.mark.xfail
  230. def test_greater_than_comparator(self):
  231. result = self.parse_query("timesSeen:>10 event.timestamp:>2016-01-02")
  232. assert result == {
  233. "tags": {},
  234. "query": "",
  235. "times_seen_lower": 10,
  236. "times_seen_lower_inclusive": False,
  237. "date_from": datetime(2016, 1, 2, tzinfo=UTC),
  238. "date_from_inclusive": False,
  239. }
  240. def test_greater_than_equal_comparator(self):
  241. result = self.parse_query("timesSeen:>=10 event.timestamp:>=2016-01-02")
  242. assert result == {
  243. "tags": {},
  244. "query": "",
  245. "times_seen_lower": 10,
  246. "times_seen_lower_inclusive": True,
  247. "date_from": datetime(2016, 1, 2, tzinfo=UTC),
  248. "date_from_inclusive": True,
  249. }
  250. def test_less_than_comparator(self):
  251. result = self.parse_query("event.timestamp:<2016-01-02 timesSeen:<10")
  252. assert result == {
  253. "tags": {},
  254. "query": "",
  255. "times_seen_upper": 10,
  256. "times_seen_upper_inclusive": False,
  257. "date_to": datetime(2016, 1, 2, tzinfo=UTC),
  258. "date_to_inclusive": False,
  259. }
  260. # TODO: query parser for '<=' timestamp should set inclusive to True.
  261. @pytest.mark.xfail
  262. def test_less_than_equal_comparator(self):
  263. result = self.parse_query("event.timestamp:<=2016-01-02 timesSeen:<=10")
  264. assert result == {
  265. "tags": {},
  266. "query": "",
  267. "times_seen_upper": 10,
  268. "times_seen_upper_inclusive": True,
  269. "date_to": datetime(2016, 1, 2, tzinfo=UTC),
  270. "date_to_inclusive": True,
  271. }
  272. def test_handles_underscore_in_tag_key(self):
  273. result = self.parse_query("foo_bar:foobar")
  274. assert result == {"tags": {"foo_bar": "foobar"}, "query": ""}
  275. def test_mix_tag_and_query(self):
  276. result = self.parse_query("foo bar key:value")
  277. assert result == {"tags": {"key": "value"}, "query": "foo bar"}
  278. def test_single_tag(self):
  279. result = self.parse_query("key:value")
  280. assert result == {"tags": {"key": "value"}, "query": ""}
  281. def test_tag_with_colon_in_value(self):
  282. result = self.parse_query("url:http://example.com")
  283. assert result == {"tags": {"url": "http://example.com"}, "query": ""}
  284. def test_single_space_in_value(self):
  285. result = self.parse_query('key:"value1 value2"')
  286. assert result == {"tags": {"key": "value1 value2"}, "query": ""}
  287. def test_multiple_spaces_in_value(self):
  288. result = self.parse_query('key:"value1 value2"')
  289. assert result == {"tags": {"key": "value1 value2"}, "query": ""}
  290. def test_invalid_tag_as_query(self):
  291. result = self.parse_query("Resque::DirtyExit")
  292. assert result == {"tags": {}, "query": "Resque::DirtyExit"}
  293. def test_colons_in_tag_value(self):
  294. result = self.parse_query("key:Resque::DirtyExit")
  295. assert result == {"tags": {"key": "Resque::DirtyExit"}, "query": ""}
  296. def test_multiple_tags(self):
  297. result = self.parse_query("foo:bar key:value")
  298. assert result == {"tags": {"key": "value", "foo": "bar"}, "query": ""}
  299. def test_single_tag_with_quotes(self):
  300. result = self.parse_query('foo:"bar"')
  301. assert result == {"tags": {"foo": "bar"}, "query": ""}
  302. def test_tag_with_quotes_and_query(self):
  303. result = self.parse_query('key:"a value" hello')
  304. assert result == {"tags": {"key": "a value"}, "query": "hello"}
  305. def test_is_resolved(self):
  306. result = self.parse_query("is:resolved")
  307. assert result == {"status": GroupStatus.RESOLVED, "tags": {}, "query": ""}
  308. def test_assigned_me(self):
  309. result = self.parse_query("assigned:me")
  310. assert result == {"assigned_to": self.current_rpc_user, "tags": {}, "query": ""}
  311. def test_assigned_none(self):
  312. result = self.parse_query("assigned:none")
  313. assert result == {"assigned_to": None, "tags": {}, "query": ""}
  314. def test_assigned_email(self):
  315. result = self.parse_query(f"assigned:{self.user.email}")
  316. assert result == {"assigned_to": self.rpc_user, "tags": {}, "query": ""}
  317. def test_assigned_unknown_user(self):
  318. result = self.parse_query("assigned:fake@example.com")
  319. assert isinstance(result["assigned_to"], RpcUser)
  320. assert result["assigned_to"].id == 0
  321. def test_assigned_valid_team(self):
  322. result = self.parse_query(f"assigned:#{self.team.slug}")
  323. assert result["assigned_to"] == self.team
  324. def test_assigned_unassociated_team(self):
  325. team2 = self.create_team(organization=self.organization)
  326. result = self.parse_query(f"assigned:#{team2.slug}")
  327. assert isinstance(result["assigned_to"], Team)
  328. assert result["assigned_to"].id == 0
  329. def test_assigned_invalid_team(self):
  330. result = self.parse_query("assigned:#invalid")
  331. assert isinstance(result["assigned_to"], Team)
  332. assert result["assigned_to"].id == 0
  333. def test_bookmarks_me(self):
  334. result = self.parse_query("bookmarks:me")
  335. assert result == {"bookmarked_by": self.current_rpc_user, "tags": {}, "query": ""}
  336. def test_bookmarks_email(self):
  337. result = self.parse_query(f"bookmarks:{self.user.email}")
  338. assert result == {"bookmarked_by": self.rpc_user, "tags": {}, "query": ""}
  339. def test_bookmarks_unknown_user(self):
  340. result = self.parse_query("bookmarks:fake@example.com")
  341. assert result["bookmarked_by"].id == 0
  342. def test_first_release(self):
  343. result = self.parse_query("first-release:bar")
  344. assert result == {"first_release": ["bar"], "tags": {}, "query": ""}
  345. def test_first_release_latest(self):
  346. result = self.parse_query("first-release:latest")
  347. assert result == {"first_release": [""], "tags": {}, "query": ""}
  348. release = self.create_release(
  349. project=self.project,
  350. version="older_release",
  351. date_added=timezone.now() - timedelta(days=1),
  352. )
  353. result = self.parse_query("first-release:latest")
  354. assert result == {"first_release": [release.version], "tags": {}, "query": ""}
  355. release = self.create_release(
  356. project=self.project, version="new_release", date_added=timezone.now()
  357. )
  358. result = self.parse_query("first-release:latest")
  359. assert result == {"first_release": [release.version], "tags": {}, "query": ""}
  360. def test_release(self):
  361. result = self.parse_query("release:bar")
  362. assert result == {"tags": {"sentry:release": ["bar"]}, "query": ""}
  363. def test_release_latest(self):
  364. result = self.parse_query("release:latest")
  365. assert result == {"tags": {"sentry:release": [""]}, "query": ""}
  366. release = self.create_release(
  367. project=self.project,
  368. version="older_release",
  369. date_added=timezone.now() - timedelta(days=1),
  370. )
  371. result = self.parse_query("release:latest")
  372. assert result == {"tags": {"sentry:release": [release.version]}, "query": ""}
  373. release = self.create_release(
  374. project=self.project, version="new_release", date_added=timezone.now()
  375. )
  376. result = self.parse_query("release:latest")
  377. assert result == {"tags": {"sentry:release": [release.version]}, "query": ""}
  378. def test_dist(self):
  379. result = self.parse_query("dist:123")
  380. assert result == {"tags": {"sentry:dist": "123"}, "query": ""}
  381. def test_padded_spacing(self):
  382. result = self.parse_query("release:bar foo bar")
  383. assert result == {"tags": {"sentry:release": ["bar"]}, "query": "foo bar"}
  384. def test_unknown_user_with_dot_query(self):
  385. result = self.parse_query("user.email:fake@example.com")
  386. assert result["tags"]["sentry:user"] == "email:fake@example.com"
  387. def test_unknown_user_value(self):
  388. result = self.parse_query("user.xxxxxx:example")
  389. assert result["tags"]["sentry:user"] == "xxxxxx:example"
  390. def test_user_lookup_with_dot_query(self):
  391. self.project.date_added = timezone.now() - timedelta(minutes=10)
  392. self.project.save()
  393. self.store_event(
  394. data={
  395. "user": {
  396. "id": 1,
  397. "email": "foo@example.com",
  398. "username": "foobar",
  399. "ip_address": "127.0.0.1",
  400. },
  401. "timestamp": iso_format(before_now(seconds=10)),
  402. },
  403. project_id=self.project.id,
  404. )
  405. result = self.parse_query("user.username:foobar")
  406. assert result["tags"]["sentry:user"] == "id:1"
  407. def test_unknown_user_legacy_syntax(self):
  408. result = self.parse_query("user:email:fake@example.com")
  409. assert result["tags"]["sentry:user"] == "email:fake@example.com"
  410. def test_user_lookup_legacy_syntax(self):
  411. self.project.date_added = timezone.now() - timedelta(minutes=10)
  412. self.project.save()
  413. self.store_event(
  414. data={
  415. "user": {
  416. "id": 1,
  417. "email": "foo@example.com",
  418. "username": "foobar",
  419. "ip_address": "127.0.0.1",
  420. },
  421. "timestamp": iso_format(before_now(seconds=10)),
  422. },
  423. project_id=self.project.id,
  424. )
  425. result = self.parse_query("user:username:foobar")
  426. assert result["tags"]["sentry:user"] == "id:1"
  427. def test_is_unassigned(self):
  428. result = self.parse_query("is:unassigned")
  429. assert result == {"unassigned": True, "tags": {}, "query": ""}
  430. def test_is_assigned(self):
  431. result = self.parse_query("is:assigned")
  432. assert result == {"unassigned": False, "tags": {}, "query": ""}
  433. def test_is_inbox(self):
  434. result = self.parse_query("is:for_review")
  435. assert result == {"for_review": True, "tags": {}, "query": ""}
  436. def test_is_unlinked(self):
  437. result = self.parse_query("is:unlinked")
  438. assert result == {"linked": False, "tags": {}, "query": ""}
  439. def test_is_linked(self):
  440. result = self.parse_query("is:linked")
  441. assert result == {"linked": True, "tags": {}, "query": ""}
  442. def test_age_from(self):
  443. result = self.parse_query("age:-24h")
  444. assert result["age_from"] > timezone.now() - timedelta(hours=25)
  445. assert result["age_from"] < timezone.now() - timedelta(hours=23)
  446. assert not result.get("age_to")
  447. def test_age_to(self):
  448. result = self.parse_query("age:+24h")
  449. assert result["age_to"] > timezone.now() - timedelta(hours=25)
  450. assert result["age_to"] < timezone.now() - timedelta(hours=23)
  451. assert not result.get("age_from")
  452. def test_age_range(self):
  453. result = self.parse_query("age:-24h age:+12h")
  454. assert result["age_from"] > timezone.now() - timedelta(hours=25)
  455. assert result["age_from"] < timezone.now() - timedelta(hours=23)
  456. assert result["age_to"] > timezone.now() - timedelta(hours=13)
  457. assert result["age_to"] < timezone.now() - timedelta(hours=11)
  458. def test_first_seen_range(self):
  459. result = self.parse_query("firstSeen:-24h firstSeen:+12h")
  460. assert result["age_from"] > timezone.now() - timedelta(hours=25)
  461. assert result["age_from"] < timezone.now() - timedelta(hours=23)
  462. assert result["age_to"] > timezone.now() - timedelta(hours=13)
  463. assert result["age_to"] < timezone.now() - timedelta(hours=11)
  464. def test_date_range(self):
  465. result = self.parse_query("event.timestamp:>2016-01-01 event.timestamp:<2016-01-02")
  466. assert result["date_from"] == datetime(2016, 1, 1, tzinfo=UTC)
  467. assert result["date_from_inclusive"] is False
  468. assert result["date_to"] == datetime(2016, 1, 2, tzinfo=UTC)
  469. assert result["date_to_inclusive"] is False
  470. def test_date_range_with_timezone(self):
  471. result = self.parse_query(
  472. "event.timestamp:>2016-01-01T10:00:00-03:00 event.timestamp:<2016-01-02T10:00:00+02:00"
  473. )
  474. assert result["date_from"] == datetime(2016, 1, 1, 13, 0, 0, tzinfo=UTC)
  475. assert result["date_from_inclusive"] is False
  476. assert result["date_to"] == datetime(2016, 1, 2, 8, 0, tzinfo=UTC)
  477. assert result["date_to_inclusive"] is False
  478. def test_date_range_with_z_timezone(self):
  479. result = self.parse_query(
  480. "event.timestamp:>2016-01-01T10:00:00Z event.timestamp:<2016-01-02T10:00:00Z"
  481. )
  482. assert result["date_from"] == datetime(2016, 1, 1, 10, 0, 0, tzinfo=UTC)
  483. assert result["date_from_inclusive"] is False
  484. assert result["date_to"] == datetime(2016, 1, 2, 10, 0, tzinfo=UTC)
  485. assert result["date_to_inclusive"] is False
  486. def test_date_range_inclusive(self):
  487. result = self.parse_query("event.timestamp:>=2016-01-01 event.timestamp:<=2016-01-02")
  488. assert result["date_from"] == datetime(2016, 1, 1, tzinfo=UTC)
  489. assert result["date_from_inclusive"] is True
  490. assert result["date_to"] == datetime(2016, 1, 2, tzinfo=UTC)
  491. assert result["date_to_inclusive"] is True
  492. def test_date_approx_day(self):
  493. date_value = datetime(2016, 1, 1, tzinfo=UTC)
  494. result = self.parse_query("event.timestamp:2016-01-01")
  495. assert result["date_from"] == date_value
  496. assert result["date_from_inclusive"]
  497. assert result["date_to"] == date_value + timedelta(days=1)
  498. assert not result["date_to_inclusive"]
  499. def test_date_approx_precise(self):
  500. date_value = datetime(2016, 1, 1, tzinfo=UTC)
  501. result = self.parse_query("event.timestamp:2016-01-01T00:00:00")
  502. assert result["date_from"] == date_value - timedelta(minutes=5)
  503. assert result["date_from_inclusive"]
  504. assert result["date_to"] == date_value + timedelta(minutes=6)
  505. assert not result["date_to_inclusive"]
  506. def test_date_approx_precise_with_timezone(self):
  507. date_value = datetime(2016, 1, 1, 5, 0, 0, tzinfo=UTC)
  508. result = self.parse_query("event.timestamp:2016-01-01T00:00:00-05:00")
  509. assert result["date_from"] == date_value - timedelta(minutes=5)
  510. assert result["date_from_inclusive"]
  511. assert result["date_to"] == date_value + timedelta(minutes=6)
  512. assert not result["date_to_inclusive"]
  513. def test_last_seen_range(self):
  514. result = self.parse_query("lastSeen:-24h lastSeen:+12h")
  515. assert result["last_seen_from"] > timezone.now() - timedelta(hours=25)
  516. assert result["last_seen_from"] < timezone.now() - timedelta(hours=23)
  517. assert result["last_seen_to"] > timezone.now() - timedelta(hours=13)
  518. assert result["last_seen_to"] < timezone.now() - timedelta(hours=11)
  519. def test_has_tag(self):
  520. result = self.parse_query("has:foo")
  521. assert result["tags"]["foo"] == ANY
  522. result = self.parse_query("has:foo foo:value")
  523. assert result["tags"]["foo"] == "value"
  524. def test_has_user(self):
  525. result = self.parse_query("has:user")
  526. assert result["tags"]["sentry:user"] == ANY
  527. def test_has_release(self):
  528. result = self.parse_query("has:release")
  529. assert result["tags"]["sentry:release"] == ANY
  530. def test_quoted_string(self):
  531. result = self.parse_query('"release:foo"')
  532. assert result == {"tags": {}, "query": "release:foo"}
  533. def test_quoted_tag_value(self):
  534. result = self.parse_query('event.type:error title:"QueryExecutionError: Code: 141."')
  535. assert result["query"] == ""
  536. assert result["tags"]["title"] == "QueryExecutionError: Code: 141."
  537. assert result["tags"]["event.type"] == "error"
  538. def test_leading_colon(self):
  539. result = self.parse_query("country:canada :unresolved")
  540. assert result["query"] == ":unresolved"
  541. assert result["tags"]["country"] == "canada"
  542. def test_assigned_or_suggested_me(self):
  543. result = self.parse_query("assigned_or_suggested:me")
  544. assert result == {"assigned_or_suggested": self.current_rpc_user, "tags": {}, "query": ""}
  545. def test_assigned_or_suggested_none(self):
  546. result = self.parse_query("assigned_or_suggested:none")
  547. assert result == {
  548. "assigned_or_suggested": None,
  549. "tags": {},
  550. "query": "",
  551. }
  552. def test_owner_email(self):
  553. result = self.parse_query(f"assigned_or_suggested:{self.user.email}")
  554. assert result == {"assigned_or_suggested": self.rpc_user, "tags": {}, "query": ""}
  555. def test_assigned_or_suggested_unknown_user(self):
  556. result = self.parse_query("assigned_or_suggested:fake@example.com")
  557. assert isinstance(result["assigned_or_suggested"], RpcUser)
  558. assert result["assigned_or_suggested"].id == 0
  559. def test_owner_valid_team(self):
  560. result = self.parse_query(f"assigned_or_suggested:#{self.team.slug}")
  561. assert result["assigned_or_suggested"] == self.team
  562. def test_assigned_or_suggested_unassociated_team(self):
  563. team2 = self.create_team(organization=self.organization)
  564. result = self.parse_query(f"assigned_or_suggested:#{team2.slug}")
  565. assert isinstance(result["assigned_or_suggested"], Team)
  566. assert result["assigned_or_suggested"].id == 0
  567. def test_owner_invalid_team(self):
  568. result = self.parse_query("assigned_or_suggested:#invalid")
  569. assert isinstance(result["assigned_or_suggested"], Team)
  570. assert result["assigned_or_suggested"].id == 0
  571. class GetLatestReleaseTest(TestCase):
  572. def test(self):
  573. with pytest.raises(Release.DoesNotExist):
  574. # no releases exist period
  575. environment = None
  576. get_latest_release([self.project], environment)
  577. old = self.create_release(version="old")
  578. new_date = old.date_added + timedelta(minutes=1)
  579. new = self.create_release(
  580. version="new-but-in-environment",
  581. environments=[self.environment],
  582. date_released=new_date,
  583. )
  584. newest = self.create_release(
  585. version="newest-overall", date_released=old.date_added + timedelta(minutes=5)
  586. )
  587. # latest overall (no environment filter)
  588. environment = None
  589. result = get_latest_release([self.project], environment)
  590. assert result == [newest.version]
  591. # latest in environment
  592. environment = self.environment
  593. result = get_latest_release([self.project], [environment])
  594. assert result == [new.version]
  595. assert get_latest_release([self.project.id], [environment]) == []
  596. assert get_latest_release(
  597. [self.project.id], [environment], self.project.organization_id
  598. ) == [new.version]
  599. # Verify that not passing an environment correctly gets the latest one
  600. assert get_latest_release([self.project], None) == [newest.version]
  601. assert get_latest_release([self.project], []) == [newest.version]
  602. with pytest.raises(Release.DoesNotExist):
  603. # environment with no releases
  604. new_environment = self.create_environment()
  605. get_latest_release([self.project], [new_environment])
  606. project_2 = self.create_project()
  607. other_project_env_release = self.create_release(
  608. project_2, version="other_project_env", environments=[self.environment]
  609. )
  610. other_project_release = self.create_release(project_2, version="other_project")
  611. assert get_latest_release([project_2], None) == [other_project_release.version]
  612. assert get_latest_release([project_2], [environment]) == [other_project_env_release.version]
  613. assert get_latest_release([self.project, project_2], None) == [
  614. newest.version,
  615. other_project_release.version,
  616. ]
  617. assert get_latest_release([self.project, project_2], [environment]) == [
  618. new.version,
  619. other_project_env_release.version,
  620. ]
  621. with pytest.raises(Release.DoesNotExist):
  622. assert get_latest_release([self.project, project_2], [environment], adopted=True) == [
  623. new.version,
  624. other_project_env_release.version,
  625. ]
  626. ReleaseProjectEnvironment.objects.filter(
  627. release__in=[new, other_project_env_release]
  628. ).update(adopted=timezone.now())
  629. assert get_latest_release([self.project, project_2], [environment], adopted=True) == [
  630. new.version,
  631. other_project_env_release.version,
  632. ]
  633. def test_semver(self):
  634. project_2 = self.create_project()
  635. release_1 = self.create_release(version="test@2.0.0", environments=[self.environment])
  636. env_2 = self.create_environment()
  637. self.create_release(version="test@1.3.2", environments=[env_2])
  638. self.create_release(version="test@1.0.0", environments=[self.environment, env_2])
  639. # Check when we're using a single project that we sort by semver
  640. assert get_latest_release([self.project], None) == [release_1.version]
  641. assert get_latest_release([project_2, self.project], None) == [release_1.version]
  642. release_3 = self.create_release(
  643. project_2, version="test@1.3.3", environments=[self.environment, env_2]
  644. )
  645. assert get_latest_release([project_2, self.project], None) == [
  646. release_3.version,
  647. release_1.version,
  648. ]
  649. with pytest.raises(Release.DoesNotExist):
  650. get_latest_release([project_2, self.project], [self.environment, env_2], adopted=True)
  651. ReleaseProjectEnvironment.objects.filter(release__in=[release_3, release_1]).update(
  652. adopted=timezone.now()
  653. )
  654. assert get_latest_release(
  655. [project_2, self.project], [self.environment, env_2], adopted=True
  656. ) == [
  657. release_3.version,
  658. release_1.version,
  659. ]
  660. assert get_latest_release([project_2, self.project], [env_2], adopted=True) == [
  661. release_3.version,
  662. ]
  663. # Make sure unadopted releases are ignored
  664. ReleaseProjectEnvironment.objects.filter(release__in=[release_3]).update(
  665. unadopted=timezone.now()
  666. )
  667. assert get_latest_release(
  668. [project_2, self.project], [self.environment, env_2], adopted=True
  669. ) == [
  670. release_1.version,
  671. ]
  672. ReleaseProject.objects.filter(release__in=[release_1]).update(adopted=timezone.now())
  673. assert get_latest_release([project_2, self.project], None, adopted=True) == [
  674. release_1.version,
  675. ]
  676. def test_multiple_projects_mixed_versions(self):
  677. project_2 = self.create_project()
  678. release_1 = self.create_release(version="test@2.0.0")
  679. self.create_release(project_2, version="not_semver")
  680. release_2 = self.create_release(project_2, version="not_semver_2")
  681. self.create_release(version="test@1.0.0")
  682. assert get_latest_release([project_2, self.project], None) == [
  683. release_2.version,
  684. release_1.version,
  685. ]
  686. class GetFirstLastReleaseForGroupTest(TestCase):
  687. def test_date(self):
  688. with pytest.raises(Release.DoesNotExist):
  689. get_first_last_release_for_group(self.group, LatestReleaseOrders.DATE, True)
  690. oldest = self.create_release(version="old")
  691. self.create_group_release(group=self.group, release=oldest)
  692. newest = self.create_release(
  693. version="newest", date_released=oldest.date_added + timedelta(minutes=5)
  694. )
  695. self.create_group_release(group=self.group, release=newest)
  696. assert newest == get_first_last_release_for_group(
  697. self.group, LatestReleaseOrders.DATE, True
  698. )
  699. assert oldest == get_first_last_release_for_group(
  700. self.group, LatestReleaseOrders.DATE, False
  701. )
  702. group_2 = self.create_group()
  703. with pytest.raises(Release.DoesNotExist):
  704. get_first_last_release_for_group(group_2, LatestReleaseOrders.DATE, True)
  705. self.create_group_release(group=group_2, release=oldest)
  706. assert oldest == get_first_last_release_for_group(group_2, LatestReleaseOrders.DATE, True)
  707. assert oldest == get_first_last_release_for_group(group_2, LatestReleaseOrders.DATE, False)
  708. def test_semver(self):
  709. with pytest.raises(Release.DoesNotExist):
  710. get_first_last_release_for_group(self.group, LatestReleaseOrders.SEMVER, True)
  711. latest = self.create_release(version="test@2.0.0")
  712. middle = self.create_release(version="test@1.3.2")
  713. earliest = self.create_release(
  714. version="test@1.0.0", date_released=latest.date_added + timedelta(minutes=5)
  715. )
  716. self.create_group_release(group=self.group, release=latest)
  717. self.create_group_release(group=self.group, release=middle)
  718. self.create_group_release(group=self.group, release=earliest)
  719. assert latest == get_first_last_release_for_group(
  720. self.group, LatestReleaseOrders.SEMVER, True
  721. )
  722. assert earliest == get_first_last_release_for_group(
  723. self.group, LatestReleaseOrders.DATE, True
  724. )
  725. assert earliest == get_first_last_release_for_group(
  726. self.group, LatestReleaseOrders.SEMVER, False
  727. )
  728. assert latest == get_first_last_release_for_group(
  729. self.group, LatestReleaseOrders.DATE, False
  730. )
  731. group_2 = self.create_group()
  732. with pytest.raises(Release.DoesNotExist):
  733. get_first_last_release_for_group(group_2, LatestReleaseOrders.SEMVER, True)
  734. self.create_group_release(group=group_2, release=latest)
  735. self.create_group_release(group=group_2, release=middle)
  736. assert latest == get_first_last_release_for_group(group_2, LatestReleaseOrders.SEMVER, True)
  737. assert middle == get_first_last_release_for_group(
  738. group_2, LatestReleaseOrders.SEMVER, False
  739. )
  740. @control_silo_test
  741. class ConvertUserTagTest(TestCase):
  742. def test_simple_user_tag(self):
  743. assert convert_user_tag_to_query("user", "id:123456") == 'user.id:"123456"'
  744. def test_user_tag_with_quote(self):
  745. assert convert_user_tag_to_query("user", 'id:123"456') == 'user.id:"123\\"456"'
  746. def test_user_tag_with_space(self):
  747. assert convert_user_tag_to_query("user", "id:123 456") == 'user.id:"123 456"'
  748. def test_non_user_tag(self):
  749. assert convert_user_tag_to_query("user", 'fake:123"456') is None
  750. def test_valid_device_class_mapping():
  751. assert set(DEVICE_CLASS.keys()) == {"low", "medium", "high"}, "Only 3 possible classes"
  752. # should all be integers
  753. device_classes = {key: {int(value) for value in values} for key, values in DEVICE_CLASS.items()}
  754. assert all(
  755. 0 not in values for values in device_classes.values()
  756. ), "`0` is not a valid classes as it represents unclassified"