test_discover_query.py 23 KB

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