test_discover_query.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. from datetime import datetime, timedelta
  2. import pytest
  3. from django.urls import reverse
  4. from sentry.testutils import APITestCase, SnubaTestCase
  5. from sentry.testutils.helpers.datetime import before_now, iso_format
  6. class DiscoverQueryTest(APITestCase, SnubaTestCase):
  7. def setUp(self):
  8. super().setUp()
  9. self.now = datetime.now()
  10. self.one_second_ago = iso_format(before_now(seconds=1))
  11. self.login_as(user=self.user, superuser=False)
  12. self.org = self.create_organization(owner=self.user, name="foo")
  13. self.project = self.create_project(name="bar", organization=self.org)
  14. self.other_project = self.create_project(name="other")
  15. self.event = self.store_event(
  16. data={
  17. "platform": "python",
  18. "timestamp": self.one_second_ago,
  19. "environment": "production",
  20. "tags": {"sentry:release": "foo", "error.custom": "custom"},
  21. "exception": {
  22. "values": [
  23. {
  24. "type": "ValidationError",
  25. "value": "Bad request",
  26. "mechanism": {"type": "1", "value": "1"},
  27. "stacktrace": {
  28. "frames": [
  29. {
  30. "function": "?",
  31. "filename": "http://localhost:1337/error.js",
  32. "lineno": 29,
  33. "colno": 3,
  34. "in_app": True,
  35. }
  36. ]
  37. },
  38. }
  39. ]
  40. },
  41. },
  42. project_id=self.project.id,
  43. )
  44. def test(self):
  45. with self.feature("organizations:discover"):
  46. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  47. response = self.client.post(
  48. url,
  49. {
  50. "projects": [self.project.id],
  51. "fields": ["environment", "platform.name"],
  52. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  53. "end": iso_format(datetime.now()),
  54. "orderby": "-timestamp",
  55. "range": None,
  56. },
  57. )
  58. assert response.status_code == 200, response.content
  59. assert len(response.data["data"]) == 1
  60. assert response.data["data"][0]["environment"] == "production"
  61. assert response.data["data"][0]["platform.name"] == "python"
  62. def test_with_discover_basic(self):
  63. # Dashboards requires access to the discover1 endpoints for now.
  64. # But newer saas plans don't include discover1, only discover2 (discover-basic).
  65. with self.feature("organizations:discover-basic"):
  66. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  67. response = self.client.post(
  68. url,
  69. {
  70. "projects": [self.project.id],
  71. "fields": ["environment", "platform.name"],
  72. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  73. "end": iso_format(datetime.now()),
  74. "orderby": "-timestamp",
  75. "range": None,
  76. },
  77. )
  78. assert response.status_code == 200, response.content
  79. def test_relative_dates(self):
  80. with self.feature("organizations:discover"):
  81. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  82. response = self.client.post(
  83. url,
  84. {
  85. "projects": [self.project.id],
  86. "fields": ["environment", "platform.name"],
  87. "range": "1d",
  88. "orderby": "-timestamp",
  89. "start": None,
  90. "end": None,
  91. },
  92. )
  93. assert response.status_code == 200, response.content
  94. assert len(response.data["data"]) == 1
  95. assert response.data["data"][0]["environment"] == "production"
  96. assert response.data["data"][0]["platform.name"] == "python"
  97. def test_invalid_date_request(self):
  98. with self.feature("organizations:discover"):
  99. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  100. response = self.client.post(
  101. url,
  102. {
  103. "projects": [self.project.id],
  104. "fields": ["message", "platform"],
  105. "range": "1d",
  106. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  107. "end": iso_format(datetime.now()),
  108. "orderby": "-timestamp",
  109. },
  110. )
  111. assert response.status_code == 400, response.content
  112. with self.feature("organizations:discover"):
  113. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  114. response = self.client.post(
  115. url,
  116. {
  117. "projects": [self.project.id],
  118. "fields": ["message", "platform"],
  119. "statsPeriodStart": "7d",
  120. "statsPeriodEnd": "1d",
  121. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  122. "end": iso_format(datetime.now()),
  123. "orderby": "-timestamp",
  124. },
  125. )
  126. assert response.status_code == 400, response.content
  127. def test_conditional_fields(self):
  128. with self.feature("organizations:discover"):
  129. self.store_event(
  130. data={
  131. "platform": "javascript",
  132. "environment": "production",
  133. "tags": {"sentry:release": "bar"},
  134. "timestamp": self.one_second_ago,
  135. },
  136. project_id=self.project.id,
  137. )
  138. self.store_event(
  139. data={
  140. "platform": "javascript",
  141. "environment": "production",
  142. "tags": {"sentry:release": "baz"},
  143. "timestamp": self.one_second_ago,
  144. },
  145. project_id=self.project.id,
  146. )
  147. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  148. response = self.client.post(
  149. url,
  150. {
  151. "projects": [self.project.id],
  152. "aggregations": [["count()", None, "count"]],
  153. "conditionFields": [
  154. [
  155. "if",
  156. [["in", ["release", ["tuple", ["'foo'"]]]], "release", "'other'"],
  157. "release",
  158. ]
  159. ],
  160. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  161. "end": iso_format(datetime.now()),
  162. "groupby": ["time", "release"],
  163. "rollup": 86400,
  164. "limit": 1000,
  165. "orderby": "-time",
  166. "range": None,
  167. },
  168. )
  169. assert response.status_code == 200, response.content
  170. # rollup is by one day and diff of start/end is 10 seconds, so we only have one day
  171. assert len(response.data["data"]) == 2
  172. for data in response.data["data"]:
  173. # note this "release" key represents the alias for the column condition
  174. # and is also used in `groupby`, it is NOT the release tag
  175. if data["release"] == "foo":
  176. assert data["count"] == 1
  177. elif data["release"] == "other":
  178. assert data["count"] == 2
  179. def test_invalid_range_value(self):
  180. with self.feature("organizations:discover"):
  181. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  182. response = self.client.post(
  183. url,
  184. {
  185. "projects": [self.project.id],
  186. "fields": ["message", "platform"],
  187. "range": "1x",
  188. "orderby": "-timestamp",
  189. "start": None,
  190. "end": None,
  191. },
  192. )
  193. assert response.status_code == 400, response.content
  194. def test_invalid_aggregation_function(self):
  195. with self.feature("organizations:discover"):
  196. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  197. response = self.client.post(
  198. url,
  199. {
  200. "projects": [self.project.id],
  201. "fields": ["message", "platform"],
  202. "aggregations": [["test", "test", "test"]],
  203. "range": "14d",
  204. "orderby": "-timestamp",
  205. "start": None,
  206. "end": None,
  207. },
  208. )
  209. assert response.status_code == 400, response.content
  210. @pytest.mark.xfail(reason="Failing due to constrain_columns_to_dataset")
  211. def test_boolean_condition(self):
  212. with self.feature("organizations:discover"):
  213. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  214. response = self.client.post(
  215. url,
  216. {
  217. "projects": [self.project.id],
  218. "fields": ["environment", "platform.name"],
  219. "conditions": [["stack.in_app", "=", True]],
  220. "start": (datetime.now() - timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  221. "end": (datetime.now()).strftime("%Y-%m-%dT%H:%M:%S"),
  222. "orderby": "-timestamp",
  223. "range": None,
  224. },
  225. )
  226. assert response.status_code == 200, response.content
  227. assert len(response.data["data"]) == 1
  228. assert response.data["data"][0]["environment"] == "production"
  229. assert response.data["data"][0]["platform.name"] == "python"
  230. def test_strip_double_quotes_in_condition_strings(self):
  231. with self.feature("organizations:discover"):
  232. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  233. response = self.client.post(
  234. url,
  235. {
  236. "projects": [self.project.id],
  237. "fields": ["environment"],
  238. "conditions": [["environment", "=", '"production"']],
  239. "range": "14d",
  240. "orderby": "-timestamp",
  241. },
  242. )
  243. assert response.status_code == 200, response.content
  244. assert len(response.data["data"]) == 1
  245. assert response.data["data"][0]["environment"] == "production"
  246. def test_array_join(self):
  247. with self.feature("organizations:discover"):
  248. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  249. response = self.client.post(
  250. url,
  251. {
  252. "projects": [self.project.id],
  253. "fields": ["message", "error.type"],
  254. "start": (datetime.now() - timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  255. "end": (datetime.now() + timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  256. "orderby": "-timestamp",
  257. "range": None,
  258. },
  259. )
  260. assert response.status_code == 200, response.content
  261. assert len(response.data["data"]) == 1
  262. assert response.data["data"][0]["error.type"] == "ValidationError"
  263. def test_array_condition_equals(self):
  264. with self.feature("organizations:discover"):
  265. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  266. response = self.client.post(
  267. url,
  268. {
  269. "projects": [self.project.id],
  270. "conditions": [["error.type", "=", "ValidationError"]],
  271. "fields": ["message"],
  272. "start": (datetime.now() - timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  273. "end": (datetime.now()).strftime("%Y-%m-%dT%H:%M:%S"),
  274. "orderby": "-timestamp",
  275. "range": None,
  276. },
  277. )
  278. assert response.status_code == 200, response.content
  279. assert len(response.data["data"]) == 1
  280. def test_array_condition_not_equals(self):
  281. with self.feature("organizations:discover"):
  282. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  283. response = self.client.post(
  284. url,
  285. {
  286. "projects": [self.project.id],
  287. "conditions": [["error.type", "!=", "ValidationError"]],
  288. "fields": ["message"],
  289. "start": (datetime.now() - timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  290. "end": (datetime.now()).strftime("%Y-%m-%dT%H:%M:%S"),
  291. "orderby": "-timestamp",
  292. "range": None,
  293. },
  294. )
  295. assert response.status_code == 200, response.content
  296. assert len(response.data["data"]) == 0
  297. def test_array_condition_custom_tag(self):
  298. with self.feature("organizations:discover"):
  299. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  300. response = self.client.post(
  301. url,
  302. {
  303. "projects": [self.project.id],
  304. "conditions": [["error.custom", "!=", "custom"]],
  305. "fields": ["message"],
  306. "start": (datetime.now() - timedelta(seconds=10)).strftime("%Y-%m-%dT%H:%M:%S"),
  307. "end": (datetime.now()).strftime("%Y-%m-%dT%H:%M:%S"),
  308. "orderby": "-timestamp",
  309. "range": None,
  310. },
  311. )
  312. assert response.status_code == 200, response.content
  313. assert len(response.data["data"]) == 0
  314. def test_select_project_name(self):
  315. with self.feature("organizations:discover"):
  316. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  317. response = self.client.post(
  318. url,
  319. {
  320. "projects": [self.project.id],
  321. "fields": ["project.name"],
  322. "range": "14d",
  323. "orderby": "-timestamp",
  324. "start": None,
  325. "end": None,
  326. },
  327. )
  328. assert response.status_code == 200, response.content
  329. assert len(response.data["data"]) == 1
  330. assert (response.data["data"][0]["project.name"]) == "bar"
  331. def test_groupby_project_name(self):
  332. with self.feature("organizations:discover"):
  333. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  334. response = self.client.post(
  335. url,
  336. {
  337. "projects": [self.project.id],
  338. "aggregations": [["count()", "", "count"]],
  339. "fields": ["project.name"],
  340. "range": "14d",
  341. "orderby": "-count",
  342. "start": None,
  343. "end": None,
  344. },
  345. )
  346. assert response.status_code == 200, response.content
  347. assert len(response.data["data"]) == 1
  348. assert (response.data["data"][0]["project.name"]) == "bar"
  349. assert (response.data["data"][0]["count"]) == 1
  350. def test_zerofilled_dates_when_rollup_relative(self):
  351. with self.feature("organizations:discover"):
  352. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  353. response = self.client.post(
  354. url,
  355. {
  356. "projects": [self.project.id],
  357. "aggregations": [["count()", "", "count"]],
  358. "fields": ["project.name"],
  359. "groupby": ["time"],
  360. "orderby": "time",
  361. "range": "5d",
  362. "rollup": 86400,
  363. "start": None,
  364. "end": None,
  365. },
  366. )
  367. assert response.status_code == 200, response.content
  368. assert len(response.data["data"]) == 6
  369. assert (response.data["data"][5]["time"]) > response.data["data"][4]["time"]
  370. assert (response.data["data"][5]["project.name"]) == "bar"
  371. assert (response.data["data"][5]["count"]) == 1
  372. def test_zerofilled_dates_when_rollup_absolute(self):
  373. with self.feature("organizations:discover"):
  374. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  375. response = self.client.post(
  376. url,
  377. {
  378. "projects": [self.project.id],
  379. "aggregations": [["count()", "", "count"]],
  380. "fields": ["project.name"],
  381. "groupby": ["time"],
  382. "orderby": "-time",
  383. "start": (self.now - timedelta(seconds=300)).strftime("%Y-%m-%dT%H:%M:%S"),
  384. "end": self.now.strftime("%Y-%m-%dT%H:%M:%S"),
  385. "rollup": 60,
  386. "range": None,
  387. },
  388. )
  389. assert response.status_code == 200, response.content
  390. assert len(response.data["data"]) == 6
  391. event_record = response.data["data"][0]
  392. # This test can span across an hour, where the start is in hour 1, end is in hour 2, and event is in hour 2.
  393. # That pushes the result to the second row.
  394. if "project.name" not in event_record:
  395. event_record = response.data["data"][1]
  396. assert (event_record["time"]) > response.data["data"][2]["time"]
  397. assert (event_record["project.name"]) == "bar"
  398. assert (event_record["count"]) == 1
  399. def test_uniq_project_name(self):
  400. with self.feature("organizations:discover"):
  401. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  402. response = self.client.post(
  403. url,
  404. {
  405. "projects": [self.project.id],
  406. "aggregations": [["uniq", "project.name", "uniq_project_name"]],
  407. "range": "14d",
  408. "orderby": "-uniq_project_name",
  409. "start": None,
  410. "end": None,
  411. },
  412. )
  413. assert response.status_code == 200, response.content
  414. assert len(response.data["data"]) == 1
  415. assert (response.data["data"][0]["uniq_project_name"]) == 1
  416. def test_meta_types(self):
  417. with self.feature("organizations:discover"):
  418. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  419. response = self.client.post(
  420. url,
  421. {
  422. "projects": [self.project.id],
  423. "fields": ["project.id", "project.name"],
  424. "aggregations": [["count()", "", "count"]],
  425. "range": "14d",
  426. "orderby": "-count",
  427. "start": None,
  428. "end": None,
  429. },
  430. )
  431. assert response.status_code == 200, response.content
  432. assert response.data["meta"] == [
  433. {"name": "project.id", "type": "integer"},
  434. {"name": "project.name", "type": "string"},
  435. {"name": "count", "type": "integer"},
  436. ]
  437. def test_no_feature_access(self):
  438. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  439. with self.feature({"organizations:discover": False, "organizations:discover-basic": False}):
  440. response = self.client.post(
  441. url,
  442. {
  443. "projects": [self.project.id],
  444. "fields": ["message", "platform"],
  445. "range": "14d",
  446. "orderby": "-timestamp",
  447. "start": None,
  448. "end": None,
  449. },
  450. )
  451. assert response.status_code == 404, response.content
  452. def test_invalid_project(self):
  453. with self.feature("organizations:discover"):
  454. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  455. response = self.client.post(
  456. url,
  457. {
  458. "projects": [self.other_project.id],
  459. "fields": ["message", "platform"],
  460. "range": "14d",
  461. "orderby": "-timestamp",
  462. "start": None,
  463. "end": None,
  464. },
  465. )
  466. assert response.status_code == 403, response.content
  467. def test_superuser(self):
  468. self.new_org = self.create_organization(name="foo_new")
  469. self.new_project = self.create_project(name="bar_new", organization=self.new_org)
  470. self.login_as(user=self.user, superuser=True)
  471. with self.feature("organizations:discover"):
  472. url = reverse("sentry-api-0-discover-query", args=[self.new_org.slug])
  473. response = self.client.post(
  474. url,
  475. {
  476. "projects": [self.new_project.id],
  477. "fields": ["message", "platform"],
  478. "start": iso_format(datetime.now() - timedelta(seconds=10)),
  479. "end": iso_format(datetime.now()),
  480. "orderby": "-timestamp",
  481. "range": None,
  482. },
  483. )
  484. assert response.status_code == 200, response.content
  485. def test_all_projects(self):
  486. project = self.create_project(organization=self.org)
  487. self.event = self.store_event(
  488. data={
  489. "message": "other message",
  490. "platform": "python",
  491. "timestamp": iso_format(self.now - timedelta(minutes=1)),
  492. },
  493. project_id=project.id,
  494. )
  495. with self.feature("organizations:discover"):
  496. url = reverse("sentry-api-0-discover-query", args=[self.org.slug])
  497. response = self.client.post(
  498. url,
  499. {
  500. "projects": [-1],
  501. "fields": ["message", "platform.name"],
  502. "range": "1d",
  503. "orderby": "-timestamp",
  504. "start": None,
  505. "end": None,
  506. },
  507. )
  508. assert response.status_code == 200, response.content
  509. assert len(response.data["data"]) == 2