test_organization_events_stats.py 56 KB

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