test_organization_events_stats.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300
  1. from __future__ import absolute_import
  2. import mock
  3. import six
  4. import uuid
  5. from pytz import utc
  6. from datetime import timedelta
  7. from django.core.urlresolvers import reverse
  8. from sentry.testutils import APITestCase, SnubaTestCase
  9. from sentry.testutils.helpers.datetime import iso_format, before_now
  10. from sentry.utils.compat import zip
  11. from sentry.utils.samples import load_data
  12. class OrganizationEventsStatsEndpointTest(APITestCase, SnubaTestCase):
  13. def setUp(self):
  14. super(OrganizationEventsStatsEndpointTest, self).setUp()
  15. self.login_as(user=self.user)
  16. self.authed_user = self.user
  17. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  18. self.project = self.create_project()
  19. self.project2 = self.create_project()
  20. self.user = self.create_user()
  21. self.user2 = self.create_user()
  22. self.store_event(
  23. data={
  24. "event_id": "a" * 32,
  25. "message": "very bad",
  26. "timestamp": iso_format(self.day_ago + timedelta(minutes=1)),
  27. "fingerprint": ["group1"],
  28. "tags": {"sentry:user": self.user.email},
  29. },
  30. project_id=self.project.id,
  31. )
  32. self.store_event(
  33. data={
  34. "event_id": "b" * 32,
  35. "message": "oh my",
  36. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)),
  37. "fingerprint": ["group2"],
  38. "tags": {"sentry:user": self.user2.email},
  39. },
  40. project_id=self.project2.id,
  41. )
  42. self.store_event(
  43. data={
  44. "event_id": "c" * 32,
  45. "message": "very bad",
  46. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  47. "fingerprint": ["group2"],
  48. "tags": {"sentry:user": self.user2.email},
  49. },
  50. project_id=self.project2.id,
  51. )
  52. self.url = reverse(
  53. "sentry-api-0-organization-events-stats",
  54. kwargs={"organization_slug": self.project.organization.slug},
  55. )
  56. def test_simple(self):
  57. response = self.client.get(
  58. self.url,
  59. data={
  60. "start": iso_format(self.day_ago),
  61. "end": iso_format(self.day_ago + timedelta(hours=2)),
  62. "interval": "1h",
  63. },
  64. format="json",
  65. )
  66. assert response.status_code == 200, response.content
  67. assert [attrs for time, attrs in response.data["data"]] == [
  68. [{"count": 1}],
  69. [{"count": 2}],
  70. ]
  71. def test_no_projects(self):
  72. org = self.create_organization(owner=self.user)
  73. self.login_as(user=self.user)
  74. url = reverse(
  75. "sentry-api-0-organization-events-stats", kwargs={"organization_slug": org.slug}
  76. )
  77. response = self.client.get(url, format="json")
  78. assert response.status_code == 200, response.content
  79. assert len(response.data["data"]) == 0
  80. def test_user_count(self):
  81. self.store_event(
  82. data={
  83. "event_id": "d" * 32,
  84. "message": "something",
  85. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  86. "tags": {"sentry:user": self.user2.email},
  87. "fingerprint": ["group2"],
  88. },
  89. project_id=self.project2.id,
  90. )
  91. response = self.client.get(
  92. self.url,
  93. data={
  94. "start": iso_format(self.day_ago),
  95. "end": iso_format(self.day_ago + timedelta(hours=2)),
  96. "interval": "1h",
  97. "yAxis": "user_count",
  98. },
  99. format="json",
  100. )
  101. assert response.status_code == 200, response.content
  102. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 2}], [{"count": 1}]]
  103. def test_discover2_backwards_compatibility(self):
  104. with self.feature("organizations:discover-basic"):
  105. response = self.client.get(
  106. self.url,
  107. data={
  108. "start": iso_format(self.day_ago),
  109. "end": iso_format(self.day_ago + timedelta(hours=2)),
  110. "interval": "1h",
  111. "yAxis": "user_count",
  112. },
  113. format="json",
  114. )
  115. assert response.status_code == 200, response.content
  116. assert len(response.data["data"]) > 0
  117. with self.feature("organizations:discover-basic"):
  118. response = self.client.get(
  119. self.url,
  120. data={
  121. "start": iso_format(self.day_ago),
  122. "end": iso_format(self.day_ago + timedelta(hours=2)),
  123. "interval": "1h",
  124. "yAxis": "event_count",
  125. },
  126. format="json",
  127. )
  128. assert response.status_code == 200, response.content
  129. assert len(response.data["data"]) > 0
  130. def test_with_event_count_flag(self):
  131. response = self.client.get(
  132. self.url,
  133. data={
  134. "start": iso_format(self.day_ago),
  135. "end": iso_format(self.day_ago + timedelta(hours=2)),
  136. "interval": "1h",
  137. "yAxis": "event_count",
  138. },
  139. format="json",
  140. )
  141. assert response.status_code == 200, response.content
  142. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  143. def test_performance_view_feature(self):
  144. with self.feature(
  145. {"organizations:performance-view": True, "organizations:discover-basic": False}
  146. ):
  147. response = self.client.get(
  148. self.url,
  149. format="json",
  150. data={
  151. "end": iso_format(before_now()),
  152. "start": iso_format(before_now(hours=2)),
  153. "query": "project_id:1",
  154. "interval": "30m",
  155. "yAxis": "count()",
  156. },
  157. )
  158. assert response.status_code == 200
  159. def test_aggregate_function_count(self):
  160. with self.feature("organizations:discover-basic"):
  161. response = self.client.get(
  162. self.url,
  163. format="json",
  164. data={
  165. "start": iso_format(self.day_ago),
  166. "end": iso_format(self.day_ago + timedelta(hours=2)),
  167. "interval": "1h",
  168. "yAxis": "count()",
  169. },
  170. )
  171. assert response.status_code == 200, response.content
  172. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 2}]]
  173. def test_invalid_aggregate(self):
  174. with self.feature("organizations:discover-basic"):
  175. response = self.client.get(
  176. self.url,
  177. format="json",
  178. data={
  179. "start": iso_format(self.day_ago),
  180. "end": iso_format(self.day_ago + timedelta(hours=2)),
  181. "interval": "1h",
  182. "yAxis": "rubbish",
  183. },
  184. )
  185. assert response.status_code == 400, response.content
  186. def test_aggregate_function_user_count(self):
  187. with self.feature("organizations:discover-basic"):
  188. response = self.client.get(
  189. self.url,
  190. format="json",
  191. data={
  192. "start": iso_format(self.day_ago),
  193. "end": iso_format(self.day_ago + timedelta(hours=2)),
  194. "interval": "1h",
  195. "yAxis": "count_unique(user)",
  196. },
  197. )
  198. assert response.status_code == 200, response.content
  199. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
  200. def test_aggregate_invalid(self):
  201. with self.feature("organizations:discover-basic"):
  202. response = self.client.get(
  203. self.url,
  204. format="json",
  205. data={
  206. "start": iso_format(self.day_ago),
  207. "end": iso_format(self.day_ago + timedelta(hours=2)),
  208. "interval": "1h",
  209. "yAxis": "nope(lol)",
  210. },
  211. )
  212. assert response.status_code == 400, response.content
  213. def test_throughput_epm_hour_rollup(self):
  214. project = self.create_project()
  215. # Each of these denotes how many events to create in each hour
  216. event_counts = [6, 0, 6, 3, 0, 3]
  217. for hour, count in enumerate(event_counts):
  218. for minute in range(count):
  219. self.store_event(
  220. data={
  221. "event_id": six.binary_type(six.text_type(uuid.uuid1()).encode("ascii")),
  222. "message": "very bad",
  223. "timestamp": iso_format(
  224. self.day_ago + timedelta(hours=hour, minutes=minute)
  225. ),
  226. "fingerprint": ["group1"],
  227. "tags": {"sentry:user": self.user.email},
  228. },
  229. project_id=project.id,
  230. )
  231. with self.feature("organizations:discover-basic"):
  232. response = self.client.get(
  233. self.url,
  234. format="json",
  235. data={
  236. "start": iso_format(self.day_ago),
  237. "end": iso_format(self.day_ago + timedelta(hours=6)),
  238. "interval": "1h",
  239. "yAxis": "epm()",
  240. "project": project.id,
  241. },
  242. )
  243. assert response.status_code == 200, response.content
  244. data = response.data["data"]
  245. assert len(data) == 6
  246. rows = data[0:6]
  247. for test in zip(event_counts, rows):
  248. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  249. def test_throughput_epm_day_rollup(self):
  250. project = self.create_project()
  251. # Each of these denotes how many events to create in each minute
  252. event_counts = [6, 0, 6, 3, 0, 3]
  253. for hour, count in enumerate(event_counts):
  254. for minute in range(count):
  255. self.store_event(
  256. data={
  257. "event_id": six.binary_type(six.text_type(uuid.uuid1()).encode("ascii")),
  258. "message": "very bad",
  259. "timestamp": iso_format(
  260. self.day_ago + timedelta(hours=hour, minutes=minute)
  261. ),
  262. "fingerprint": ["group1"],
  263. "tags": {"sentry:user": self.user.email},
  264. },
  265. project_id=project.id,
  266. )
  267. with self.feature("organizations:discover-basic"):
  268. response = self.client.get(
  269. self.url,
  270. format="json",
  271. data={
  272. "start": iso_format(self.day_ago),
  273. "end": iso_format(self.day_ago + timedelta(hours=24)),
  274. "interval": "24h",
  275. "yAxis": "epm()",
  276. "project": project.id,
  277. },
  278. )
  279. assert response.status_code == 200, response.content
  280. data = response.data["data"]
  281. assert len(data) == 2
  282. assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
  283. def test_throughput_eps_minute_rollup(self):
  284. project = self.create_project()
  285. # Each of these denotes how many events to create in each minute
  286. event_counts = [6, 0, 6, 3, 0, 3]
  287. for minute, count in enumerate(event_counts):
  288. for second in range(count):
  289. self.store_event(
  290. data={
  291. "event_id": six.binary_type(six.text_type(uuid.uuid1()).encode("ascii")),
  292. "message": "very bad",
  293. "timestamp": iso_format(
  294. self.day_ago + timedelta(minutes=minute, seconds=second)
  295. ),
  296. "fingerprint": ["group1"],
  297. "tags": {"sentry:user": self.user.email},
  298. },
  299. project_id=project.id,
  300. )
  301. with self.feature("organizations:discover-basic"):
  302. response = self.client.get(
  303. self.url,
  304. format="json",
  305. data={
  306. "start": iso_format(self.day_ago),
  307. "end": iso_format(self.day_ago + timedelta(minutes=6)),
  308. "interval": "1m",
  309. "yAxis": "eps()",
  310. "project": project.id,
  311. },
  312. )
  313. assert response.status_code == 200, response.content
  314. data = response.data["data"]
  315. assert len(data) == 6
  316. rows = data[0:6]
  317. for test in zip(event_counts, rows):
  318. assert test[1][1][0]["count"] == test[0] / 60.0
  319. def test_throughput_eps_no_rollup(self):
  320. project = self.create_project()
  321. # Each of these denotes how many events to create in each minute
  322. event_counts = [6, 0, 6, 3, 0, 3]
  323. for minute, count in enumerate(event_counts):
  324. for second in range(count):
  325. self.store_event(
  326. data={
  327. "event_id": six.binary_type(six.text_type(uuid.uuid1()).encode("ascii")),
  328. "message": "very bad",
  329. "timestamp": iso_format(
  330. self.day_ago + timedelta(minutes=minute, seconds=second)
  331. ),
  332. "fingerprint": ["group1"],
  333. "tags": {"sentry:user": self.user.email},
  334. },
  335. project_id=project.id,
  336. )
  337. with self.feature("organizations:discover-basic"):
  338. response = self.client.get(
  339. self.url,
  340. format="json",
  341. data={
  342. "start": iso_format(self.day_ago),
  343. "end": iso_format(self.day_ago + timedelta(minutes=1)),
  344. "interval": "1s",
  345. "yAxis": "eps()",
  346. "project": project.id,
  347. },
  348. )
  349. assert response.status_code == 200, response.content
  350. data = response.data["data"]
  351. # expect 60 data points between time span of 0 and 60 seconds
  352. assert len(data) == 60
  353. rows = data[0:6]
  354. for row in rows:
  355. assert row[1][0]["count"] == 1
  356. def test_transaction_events(self):
  357. prototype = {
  358. "type": "transaction",
  359. "transaction": "api.issue.delete",
  360. "spans": [],
  361. "contexts": {"trace": {"op": "foobar", "trace_id": "a" * 32, "span_id": "a" * 16}},
  362. "tags": {"important": "yes"},
  363. }
  364. fixtures = (
  365. ("d" * 32, before_now(minutes=32)),
  366. ("e" * 32, before_now(hours=1, minutes=2)),
  367. ("f" * 32, before_now(hours=1, minutes=35)),
  368. )
  369. for fixture in fixtures:
  370. data = prototype.copy()
  371. data["event_id"] = fixture[0]
  372. data["timestamp"] = iso_format(fixture[1])
  373. data["start_timestamp"] = iso_format(fixture[1] - timedelta(seconds=1))
  374. self.store_event(data=data, project_id=self.project.id)
  375. with self.feature("organizations:discover-basic"):
  376. response = self.client.get(
  377. self.url,
  378. format="json",
  379. data={
  380. "end": iso_format(before_now()),
  381. "start": iso_format(before_now(hours=2)),
  382. "query": "event.type:transaction",
  383. "interval": "30m",
  384. "yAxis": "count()",
  385. },
  386. )
  387. assert response.status_code == 200, response.content
  388. items = [item for time, item in response.data["data"] if item]
  389. # We could get more results depending on where the 30 min
  390. # windows land.
  391. assert len(items) >= 3
  392. def test_project_id_query_filter(self):
  393. with self.feature("organizations:discover-basic"):
  394. response = self.client.get(
  395. self.url,
  396. format="json",
  397. data={
  398. "end": iso_format(before_now()),
  399. "start": iso_format(before_now(hours=2)),
  400. "query": "project_id:1",
  401. "interval": "30m",
  402. "yAxis": "count()",
  403. },
  404. )
  405. assert response.status_code == 200
  406. def test_latest_release_query_filter(self):
  407. with self.feature("organizations:discover-basic"):
  408. response = self.client.get(
  409. self.url,
  410. format="json",
  411. data={
  412. "end": iso_format(before_now()),
  413. "start": iso_format(before_now(hours=2)),
  414. "query": "release:latest",
  415. "interval": "30m",
  416. "yAxis": "count()",
  417. },
  418. )
  419. assert response.status_code == 200
  420. def test_conditional_filter(self):
  421. with self.feature(
  422. {"organizations:discover-basic": True, "organizations:global-views": True}
  423. ):
  424. response = self.client.get(
  425. self.url,
  426. format="json",
  427. data={
  428. "start": iso_format(self.day_ago),
  429. "end": iso_format(self.day_ago + timedelta(hours=2)),
  430. "query": "id:{} OR id:{}".format("a" * 32, "b" * 32),
  431. "interval": "30m",
  432. "yAxis": "count()",
  433. },
  434. )
  435. assert response.status_code == 200, response.content
  436. data = response.data["data"]
  437. assert len(data) == 4
  438. assert data[0][1][0]["count"] == 1
  439. assert data[2][1][0]["count"] == 1
  440. def test_simple_multiple_yaxis(self):
  441. with self.feature("organizations:discover-basic"):
  442. response = self.client.get(
  443. self.url,
  444. data={
  445. "start": iso_format(self.day_ago),
  446. "end": iso_format(self.day_ago + timedelta(hours=2)),
  447. "interval": "1h",
  448. "yAxis": ["user_count", "event_count"],
  449. },
  450. format="json",
  451. )
  452. assert response.status_code == 200, response.content
  453. response.data["user_count"]["order"] == 0
  454. assert [attrs for time, attrs in response.data["user_count"]["data"]] == [
  455. [{"count": 1}],
  456. [{"count": 1}],
  457. ]
  458. response.data["event_count"]["order"] == 1
  459. assert [attrs for time, attrs in response.data["event_count"]["data"]] == [
  460. [{"count": 1}],
  461. [{"count": 2}],
  462. ]
  463. def test_large_interval_no_drop_values(self):
  464. self.store_event(
  465. data={
  466. "event_id": "d" * 32,
  467. "message": "not good",
  468. "timestamp": iso_format(before_now(minutes=10)),
  469. "fingerprint": ["group3"],
  470. },
  471. project_id=self.project.id,
  472. )
  473. with self.feature("organizations:discover-basic"):
  474. response = self.client.get(
  475. self.url,
  476. format="json",
  477. data={
  478. "end": iso_format(before_now()),
  479. "start": iso_format(before_now(hours=24)),
  480. "query": 'message:"not good"',
  481. "interval": "1d",
  482. "yAxis": "count()",
  483. },
  484. )
  485. assert response.status_code == 200
  486. assert [attrs for time, attrs in response.data["data"]] == [
  487. [{"count": 0}],
  488. [{"count": 1}],
  489. ]
  490. @mock.patch("sentry.snuba.discover.timeseries_query", return_value={})
  491. def test_multiple_yaxis_only_one_query(self, mock_query):
  492. with self.feature("organizations:discover-basic"):
  493. self.client.get(
  494. self.url,
  495. data={
  496. "start": iso_format(self.day_ago),
  497. "end": iso_format(self.day_ago + timedelta(hours=2)),
  498. "interval": "1h",
  499. "yAxis": ["user_count", "event_count", "epm()", "eps()"],
  500. },
  501. format="json",
  502. )
  503. assert mock_query.call_count == 1
  504. def test_invalid_interval(self):
  505. with self.feature("organizations:discover-basic"):
  506. response = self.client.get(
  507. self.url,
  508. format="json",
  509. data={
  510. "end": iso_format(before_now()),
  511. "start": iso_format(before_now(hours=24)),
  512. "query": "",
  513. "interval": "1s",
  514. "yAxis": "count()",
  515. },
  516. )
  517. assert response.status_code == 400
  518. def test_out_of_retention(self):
  519. with self.options({"system.event-retention-days": 10}):
  520. with self.feature("organizations:discover-basic"):
  521. response = self.client.get(
  522. self.url,
  523. format="json",
  524. data={
  525. "start": iso_format(before_now(days=20)),
  526. "end": iso_format(before_now(days=15)),
  527. "query": "",
  528. "interval": "30m",
  529. "yAxis": "count()",
  530. },
  531. )
  532. assert response.status_code == 400
  533. @mock.patch("sentry.utils.snuba.quantize_time")
  534. def test_quantize_dates(self, mock_quantize):
  535. mock_quantize.return_value = before_now(days=1).replace(tzinfo=utc)
  536. with self.feature("organizations:discover-basic"):
  537. # Don't quantize short time periods
  538. self.client.get(
  539. self.url,
  540. format="json",
  541. data={"statsPeriod": "1h", "query": "", "interval": "30m", "yAxis": "count()"},
  542. )
  543. # Don't quantize absolute date periods
  544. self.client.get(
  545. self.url,
  546. format="json",
  547. data={
  548. "start": iso_format(before_now(days=20)),
  549. "end": iso_format(before_now(days=15)),
  550. "query": "",
  551. "interval": "30m",
  552. "yAxis": "count()",
  553. },
  554. )
  555. assert len(mock_quantize.mock_calls) == 0
  556. # Quantize long date periods
  557. self.client.get(
  558. self.url,
  559. format="json",
  560. data={"statsPeriod": "90d", "query": "", "interval": "30m", "yAxis": "count()"},
  561. )
  562. assert len(mock_quantize.mock_calls) == 2
  563. class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
  564. def setUp(self):
  565. super(OrganizationEventsStatsTopNEvents, self).setUp()
  566. self.login_as(user=self.user)
  567. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  568. self.project = self.create_project()
  569. self.project2 = self.create_project()
  570. self.user2 = self.create_user()
  571. transaction_data = load_data("transaction")
  572. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  573. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=4))
  574. self.event_data = [
  575. {
  576. "data": {
  577. "message": "poof",
  578. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  579. "user": {"email": self.user.email},
  580. "fingerprint": ["group1"],
  581. },
  582. "project": self.project2,
  583. "count": 7,
  584. },
  585. {
  586. "data": {
  587. "message": "voof",
  588. "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)),
  589. "fingerprint": ["group2"],
  590. "user": {"email": self.user2.email},
  591. },
  592. "project": self.project2,
  593. "count": 6,
  594. },
  595. {
  596. "data": {
  597. "message": "very bad",
  598. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  599. "fingerprint": ["group3"],
  600. "user": {"email": "foo@example.com"},
  601. },
  602. "project": self.project,
  603. "count": 5,
  604. },
  605. {
  606. "data": {
  607. "message": "oh no",
  608. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  609. "fingerprint": ["group4"],
  610. "user": {"email": "bar@example.com"},
  611. },
  612. "project": self.project,
  613. "count": 4,
  614. },
  615. {"data": transaction_data, "project": self.project, "count": 3},
  616. # Not in the top 5
  617. {
  618. "data": {
  619. "message": "sorta bad",
  620. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  621. "fingerprint": ["group5"],
  622. "user": {"email": "bar@example.com"},
  623. },
  624. "project": self.project,
  625. "count": 2,
  626. },
  627. {
  628. "data": {
  629. "message": "not so bad",
  630. "timestamp": iso_format(self.day_ago + timedelta(minutes=2)),
  631. "fingerprint": ["group6"],
  632. "user": {"email": "bar@example.com"},
  633. },
  634. "project": self.project,
  635. "count": 1,
  636. },
  637. ]
  638. self.events = []
  639. for index, event_data in enumerate(self.event_data):
  640. data = event_data["data"].copy()
  641. for i in range(event_data["count"]):
  642. data["event_id"] = "{}{}".format(index, i) * 16
  643. event = self.store_event(data, project_id=event_data["project"].id)
  644. self.events.append(event)
  645. self.transaction = self.events[4]
  646. self.url = reverse(
  647. "sentry-api-0-organization-events-stats",
  648. kwargs={"organization_slug": self.project.organization.slug},
  649. )
  650. def test_simple_top_events(self):
  651. with self.feature("organizations:discover-basic"):
  652. response = self.client.get(
  653. self.url,
  654. data={
  655. "start": iso_format(self.day_ago),
  656. "end": iso_format(self.day_ago + timedelta(hours=2)),
  657. "interval": "1h",
  658. "yAxis": "count()",
  659. "orderby": ["-count()"],
  660. "field": ["count()", "message", "user.email"],
  661. "topEvents": 5,
  662. },
  663. format="json",
  664. )
  665. data = response.data
  666. assert response.status_code == 200, response.content
  667. assert len(data) == 5
  668. for index, event in enumerate(self.events[:5]):
  669. message = event.message or event.transaction
  670. results = data[
  671. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  672. ]
  673. assert results["order"] == index
  674. assert [{"count": self.event_data[index]["count"]}] in [
  675. attrs for time, attrs in results["data"]
  676. ]
  677. def test_top_events_limits(self):
  678. data = {
  679. "start": iso_format(self.day_ago),
  680. "end": iso_format(self.day_ago + timedelta(hours=2)),
  681. "interval": "1h",
  682. "yAxis": "count()",
  683. "orderby": ["-count()"],
  684. "field": ["count()", "message", "user.email"],
  685. }
  686. with self.feature("organizations:discover-basic"):
  687. data["topEvents"] = 50
  688. response = self.client.get(self.url, data, format="json",)
  689. assert response.status_code == 400
  690. data["topEvents"] = 0
  691. response = self.client.get(self.url, data, format="json",)
  692. assert response.status_code == 400
  693. data["topEvents"] = "a"
  694. response = self.client.get(self.url, data, format="json",)
  695. assert response.status_code == 400
  696. def test_top_events_with_projects(self):
  697. with self.feature("organizations:discover-basic"):
  698. response = self.client.get(
  699. self.url,
  700. data={
  701. "start": iso_format(self.day_ago),
  702. "end": iso_format(self.day_ago + timedelta(hours=2)),
  703. "interval": "1h",
  704. "yAxis": "count()",
  705. "orderby": ["-count()"],
  706. "field": ["count()", "message", "project"],
  707. "topEvents": 5,
  708. },
  709. format="json",
  710. )
  711. data = response.data
  712. assert response.status_code == 200, response.content
  713. assert len(data) == 5
  714. for index, event in enumerate(self.events[:5]):
  715. message = event.message or event.transaction
  716. results = data[",".join([message, event.project.slug])]
  717. assert results["order"] == index
  718. assert [{"count": self.event_data[index]["count"]}] in [
  719. attrs for time, attrs in results["data"]
  720. ]
  721. def test_top_events_with_issue(self):
  722. # delete a group to make sure if this happens the value becomes unknown
  723. event_group = self.events[0].group
  724. event_group.delete()
  725. with self.feature("organizations:discover-basic"):
  726. response = self.client.get(
  727. self.url,
  728. data={
  729. "start": iso_format(self.day_ago),
  730. "end": iso_format(self.day_ago + timedelta(hours=2)),
  731. "interval": "1h",
  732. "yAxis": "count()",
  733. "orderby": ["-count()"],
  734. "field": ["count()", "message", "issue"],
  735. "topEvents": 5,
  736. },
  737. format="json",
  738. )
  739. data = response.data
  740. assert response.status_code == 200, response.content
  741. assert len(data) == 5
  742. for index, event in enumerate(self.events[:5]):
  743. message = event.message or event.transaction
  744. # Because we deleted the group for event 0
  745. if index == 0 or event.group is None:
  746. issue = "unknown"
  747. else:
  748. issue = event.group.qualified_short_id
  749. results = data[",".join([issue, message])]
  750. assert results["order"] == index
  751. assert [{"count": self.event_data[index]["count"]}] in [
  752. attrs for time, attrs in results["data"]
  753. ]
  754. def test_top_events_with_functions(self):
  755. with self.feature("organizations:discover-basic"):
  756. response = self.client.get(
  757. self.url,
  758. data={
  759. "start": iso_format(self.day_ago),
  760. "end": iso_format(self.day_ago + timedelta(hours=2)),
  761. "interval": "1h",
  762. "yAxis": "count()",
  763. "orderby": ["-p99()"],
  764. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  765. "topEvents": 5,
  766. },
  767. format="json",
  768. )
  769. data = response.data
  770. assert response.status_code == 200, response.content
  771. assert len(data) == 1
  772. results = data[self.transaction.transaction]
  773. assert results["order"] == 0
  774. assert [attrs for time, attrs in results["data"]] == [
  775. [{"count": 3}],
  776. [{"count": 0}],
  777. ]
  778. def test_top_events_with_functions_on_different_transactions(self):
  779. """ Transaction2 has less events, but takes longer so order should be self.transaction then transaction2 """
  780. transaction_data = load_data("transaction")
  781. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  782. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  783. transaction_data["transaction"] = "/foo_bar/"
  784. transaction2 = self.store_event(transaction_data, project_id=self.project.id)
  785. with self.feature("organizations:discover-basic"):
  786. response = self.client.get(
  787. self.url,
  788. data={
  789. "start": iso_format(self.day_ago),
  790. "end": iso_format(self.day_ago + timedelta(hours=2)),
  791. "interval": "1h",
  792. "yAxis": "count()",
  793. "orderby": ["-p99()"],
  794. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  795. "topEvents": 5,
  796. },
  797. format="json",
  798. )
  799. data = response.data
  800. assert response.status_code == 200, response.content
  801. assert len(data) == 2
  802. results = data[self.transaction.transaction]
  803. assert results["order"] == 1
  804. assert [attrs for time, attrs in results["data"]] == [
  805. [{"count": 3}],
  806. [{"count": 0}],
  807. ]
  808. results = data[transaction2.transaction]
  809. assert results["order"] == 0
  810. assert [attrs for time, attrs in results["data"]] == [
  811. [{"count": 1}],
  812. [{"count": 0}],
  813. ]
  814. def test_top_events_with_query(self):
  815. transaction_data = load_data("transaction")
  816. transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2))
  817. transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6))
  818. transaction_data["transaction"] = "/foo_bar/"
  819. self.store_event(transaction_data, project_id=self.project.id)
  820. with self.feature("organizations:discover-basic"):
  821. response = self.client.get(
  822. self.url,
  823. data={
  824. "start": iso_format(self.day_ago),
  825. "end": iso_format(self.day_ago + timedelta(hours=2)),
  826. "interval": "1h",
  827. "yAxis": "count()",
  828. "orderby": ["-p99()"],
  829. "query": "transaction:/foo_bar/",
  830. "field": ["transaction", "avg(transaction.duration)", "p99()"],
  831. "topEvents": 5,
  832. },
  833. format="json",
  834. )
  835. data = response.data
  836. assert response.status_code == 200, response.content
  837. assert len(data) == 1
  838. transaction2_data = data["/foo_bar/"]
  839. assert transaction2_data["order"] == 0
  840. assert [attrs for time, attrs in transaction2_data["data"]] == [
  841. [{"count": 1}],
  842. [{"count": 0}],
  843. ]
  844. def test_top_events_with_epm(self):
  845. with self.feature("organizations:discover-basic"):
  846. response = self.client.get(
  847. self.url,
  848. data={
  849. "start": iso_format(self.day_ago),
  850. "end": iso_format(self.day_ago + timedelta(hours=2)),
  851. "interval": "1h",
  852. "yAxis": "epm()",
  853. "orderby": ["-count()"],
  854. "field": ["message", "user.email", "count()"],
  855. "topEvents": 5,
  856. },
  857. format="json",
  858. )
  859. data = response.data
  860. assert response.status_code == 200, response.content
  861. assert len(data) == 5
  862. for index, event in enumerate(self.events[:5]):
  863. message = event.message or event.transaction
  864. results = data[
  865. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  866. ]
  867. assert results["order"] == index
  868. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  869. attrs for time, attrs in results["data"]
  870. ]
  871. def test_top_events_with_multiple_yaxis(self):
  872. with self.feature("organizations:discover-basic"):
  873. response = self.client.get(
  874. self.url,
  875. data={
  876. "start": iso_format(self.day_ago),
  877. "end": iso_format(self.day_ago + timedelta(hours=2)),
  878. "interval": "1h",
  879. "yAxis": ["epm()", "count()"],
  880. "orderby": ["-count()"],
  881. "field": ["message", "user.email", "count()"],
  882. "topEvents": 5,
  883. },
  884. format="json",
  885. )
  886. data = response.data
  887. assert response.status_code == 200, response.content
  888. assert len(data) == 5
  889. for index, event in enumerate(self.events[:5]):
  890. message = event.message or event.transaction
  891. results = data[
  892. ",".join([message, self.event_data[index]["data"]["user"].get("email", "None")])
  893. ]
  894. assert results["order"] == index
  895. assert results["epm()"]["order"] == 0
  896. assert results["count()"]["order"] == 1
  897. assert [{"count": self.event_data[index]["count"] / (3600.0 / 60.0)}] in [
  898. attrs for time, attrs in results["epm()"]["data"]
  899. ]
  900. assert [{"count": self.event_data[index]["count"]}] in [
  901. attrs for time, attrs in results["count()"]["data"]
  902. ]
  903. def test_top_events_with_boolean(self):
  904. with self.feature("organizations:discover-basic"):
  905. response = self.client.get(
  906. self.url,
  907. data={
  908. "start": iso_format(self.day_ago),
  909. "end": iso_format(self.day_ago + timedelta(hours=2)),
  910. "interval": "1h",
  911. "yAxis": "count()",
  912. "orderby": ["-count()"],
  913. "field": ["count()", "message", "device.charging"],
  914. "topEvents": 5,
  915. },
  916. format="json",
  917. )
  918. data = response.data
  919. assert response.status_code == 200, response.content
  920. assert len(data) == 5
  921. for index, event in enumerate(self.events[:5]):
  922. message = event.message or event.transaction
  923. results = data[",".join(["False", message])]
  924. assert results["order"] == index
  925. assert [{"count": self.event_data[index]["count"]}] in [
  926. attrs for time, attrs in results["data"]
  927. ]
  928. def test_top_events_with_timestamp(self):
  929. with self.feature("organizations:discover-basic"):
  930. response = self.client.get(
  931. self.url,
  932. data={
  933. "start": iso_format(self.day_ago),
  934. "end": iso_format(self.day_ago + timedelta(hours=2)),
  935. "interval": "1h",
  936. "yAxis": "count()",
  937. "orderby": ["-count()"],
  938. "query": "event.type:default",
  939. "field": ["count()", "message", "timestamp"],
  940. "topEvents": 5,
  941. },
  942. format="json",
  943. )
  944. data = response.data
  945. assert response.status_code == 200, response.content
  946. assert len(data) == 5
  947. # Transactions won't be in the results because of the query
  948. del self.events[4]
  949. del self.event_data[4]
  950. for index, event in enumerate(self.events[:5]):
  951. results = data[",".join([event.message, event.timestamp])]
  952. assert results["order"] == index
  953. assert [{"count": self.event_data[index]["count"]}] in [
  954. attrs for time, attrs in results["data"]
  955. ]
  956. def test_top_events_with_int(self):
  957. with self.feature("organizations:discover-basic"):
  958. response = self.client.get(
  959. self.url,
  960. data={
  961. "start": iso_format(self.day_ago),
  962. "end": iso_format(self.day_ago + timedelta(hours=2)),
  963. "interval": "1h",
  964. "yAxis": "count()",
  965. "orderby": ["-count()"],
  966. "field": ["count()", "message", "transaction.duration"],
  967. "topEvents": 5,
  968. },
  969. format="json",
  970. )
  971. data = response.data
  972. assert response.status_code == 200, response.content
  973. assert len(data) == 1
  974. results = data[",".join([self.transaction.transaction, "120000"])]
  975. assert results["order"] == 0
  976. assert [attrs for time, attrs in results["data"]] == [
  977. [{"count": 3}],
  978. [{"count": 0}],
  979. ]
  980. def test_top_events_with_user(self):
  981. with self.feature("organizations:discover-basic"):
  982. response = self.client.get(
  983. self.url,
  984. data={
  985. "start": iso_format(self.day_ago),
  986. "end": iso_format(self.day_ago + timedelta(hours=2)),
  987. "interval": "1h",
  988. "yAxis": "count()",
  989. "orderby": ["-count()"],
  990. "field": ["user", "count()"],
  991. "topEvents": 5,
  992. },
  993. format="json",
  994. )
  995. data = response.data
  996. assert response.status_code == 200, response.content
  997. assert len(data) == 5
  998. assert data["email:bar@example.com"]["order"] == 0
  999. assert [attrs for time, attrs in data["email:bar@example.com"]["data"]] == [
  1000. [{"count": 7}],
  1001. [{"count": 0}],
  1002. ]
  1003. assert [attrs for time, attrs in data["ip:127.0.0.1"]["data"]] == [
  1004. [{"count": 3}],
  1005. [{"count": 0}],
  1006. ]
  1007. def test_top_events_with_user_and_email(self):
  1008. with self.feature("organizations:discover-basic"):
  1009. response = self.client.get(
  1010. self.url,
  1011. data={
  1012. "start": iso_format(self.day_ago),
  1013. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1014. "interval": "1h",
  1015. "yAxis": "count()",
  1016. "orderby": ["-count()"],
  1017. "field": ["user", "user.email", "count()"],
  1018. "topEvents": 5,
  1019. },
  1020. format="json",
  1021. )
  1022. data = response.data
  1023. assert response.status_code == 200, response.content
  1024. assert len(data) == 5
  1025. assert data["email:bar@example.com,bar@example.com"]["order"] == 0
  1026. assert [attrs for time, attrs in data["email:bar@example.com,bar@example.com"]["data"]] == [
  1027. [{"count": 7}],
  1028. [{"count": 0}],
  1029. ]
  1030. assert [attrs for time, attrs in data["ip:127.0.0.1,None"]["data"]] == [
  1031. [{"count": 3}],
  1032. [{"count": 0}],
  1033. ]
  1034. def test_top_events_none_filter(self):
  1035. """ When a field is None in one of the top events, make sure we filter by it
  1036. In this case event[4] is a transaction and has no issue
  1037. """
  1038. with self.feature("organizations:discover-basic"):
  1039. response = self.client.get(
  1040. self.url,
  1041. data={
  1042. "start": iso_format(self.day_ago),
  1043. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1044. "interval": "1h",
  1045. "yAxis": "count()",
  1046. "orderby": ["-count()"],
  1047. "field": ["count()", "issue"],
  1048. "topEvents": 5,
  1049. },
  1050. format="json",
  1051. )
  1052. data = response.data
  1053. assert response.status_code == 200, response.content
  1054. assert len(data) == 5
  1055. for index, event in enumerate(self.events[:5]):
  1056. if event.group is None:
  1057. issue = "unknown"
  1058. else:
  1059. issue = event.group.qualified_short_id
  1060. results = data[issue]
  1061. assert results["order"] == index
  1062. assert [{"count": self.event_data[index]["count"]}] in [
  1063. attrs for time, attrs in results["data"]
  1064. ]
  1065. def test_top_events_one_field_with_none(self):
  1066. with self.feature("organizations:discover-basic"):
  1067. response = self.client.get(
  1068. self.url,
  1069. data={
  1070. "start": iso_format(self.day_ago),
  1071. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1072. "interval": "1h",
  1073. "yAxis": "count()",
  1074. "orderby": ["-count()"],
  1075. "query": "event.type:transaction",
  1076. "field": ["count()", "issue"],
  1077. "topEvents": 5,
  1078. },
  1079. format="json",
  1080. )
  1081. data = response.data
  1082. assert response.status_code == 200, response.content
  1083. assert len(data) == 1
  1084. results = data["unknown"]
  1085. assert [attrs for time, attrs in results["data"]] == [
  1086. [{"count": 3}],
  1087. [{"count": 0}],
  1088. ]
  1089. assert results["order"] == 0
  1090. def test_top_events_with_error_handled(self):
  1091. data = self.event_data[0]
  1092. data["data"]["level"] = "error"
  1093. data["data"]["exception"] = {
  1094. "values": [
  1095. {
  1096. "type": "ValidationError",
  1097. "value": "Bad request",
  1098. "mechanism": {"handled": True, "type": "generic"},
  1099. }
  1100. ]
  1101. }
  1102. self.store_event(data["data"], project_id=data["project"].id)
  1103. data["data"]["exception"] = {
  1104. "values": [
  1105. {
  1106. "type": "ValidationError",
  1107. "value": "Bad request",
  1108. "mechanism": {"handled": False, "type": "generic"},
  1109. }
  1110. ]
  1111. }
  1112. self.store_event(data["data"], project_id=data["project"].id)
  1113. with self.feature("organizations:discover-basic"):
  1114. response = self.client.get(
  1115. self.url,
  1116. data={
  1117. "start": iso_format(self.day_ago),
  1118. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1119. "interval": "1h",
  1120. "yAxis": "count()",
  1121. "orderby": ["-count()"],
  1122. "field": ["count()", "error.handled"],
  1123. "topEvents": 5,
  1124. },
  1125. format="json",
  1126. )
  1127. assert response.status_code == 200, response.content
  1128. data = response.data
  1129. assert len(data) == 3
  1130. results = data[""]
  1131. assert [attrs for time, attrs in results["data"]] == [
  1132. [{"count": 22}],
  1133. [{"count": 6}],
  1134. ]
  1135. assert results["order"] == 0
  1136. results = data["1"]
  1137. assert [attrs for time, attrs in results["data"]] == [
  1138. [{"count": 1}],
  1139. [{"count": 0}],
  1140. ]
  1141. results = data["0"]
  1142. assert [attrs for time, attrs in results["data"]] == [
  1143. [{"count": 1}],
  1144. [{"count": 0}],
  1145. ]
  1146. def test_top_events_with_aggregate_condition(self):
  1147. with self.feature("organizations:discover-basic"):
  1148. response = self.client.get(
  1149. self.url,
  1150. data={
  1151. "start": iso_format(self.day_ago),
  1152. "end": iso_format(self.day_ago + timedelta(hours=2)),
  1153. "interval": "1h",
  1154. "yAxis": "count()",
  1155. "orderby": ["-count()"],
  1156. "field": ["message", "count()"],
  1157. "query": "count():>4",
  1158. "topEvents": 5,
  1159. },
  1160. format="json",
  1161. )
  1162. assert response.status_code == 200, response.content
  1163. data = response.data
  1164. assert len(data) == 3
  1165. for index, event in enumerate(self.events[:3]):
  1166. message = event.message or event.transaction
  1167. results = data[message]
  1168. assert results["order"] == index
  1169. assert [{"count": self.event_data[index]["count"]}] in [
  1170. attrs for time, attrs in results["data"]
  1171. ]