test_organization_events_trends.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. from datetime import timedelta
  2. from django.urls import reverse
  3. from sentry.testutils.cases import APITestCase, SnubaTestCase
  4. from sentry.testutils.helpers import parse_link_header
  5. from sentry.testutils.helpers.datetime import before_now, iso_format
  6. from sentry.testutils.silo import region_silo_test
  7. from sentry.utils.samples import load_data
  8. class OrganizationEventsTrendsBase(APITestCase, SnubaTestCase):
  9. def setUp(self):
  10. super().setUp()
  11. self.login_as(user=self.user)
  12. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  13. self.prototype = load_data("transaction")
  14. data = self.prototype.copy()
  15. data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
  16. data["user"] = {"email": "foo@example.com"}
  17. data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=2))
  18. data["measurements"]["lcp"]["value"] = 2000
  19. self.store_event(data, project_id=self.project.id)
  20. second = [0, 2, 10]
  21. for i in range(3):
  22. data = self.prototype.copy()
  23. data["start_timestamp"] = iso_format(self.day_ago + timedelta(hours=1, minutes=30 + i))
  24. data["timestamp"] = iso_format(
  25. self.day_ago + timedelta(hours=1, minutes=30 + i, seconds=second[i])
  26. )
  27. data["measurements"]["lcp"]["value"] = second[i] * 1000
  28. data["user"] = {"email": f"foo{i}@example.com"}
  29. self.store_event(data, project_id=self.project.id)
  30. self.expected_data = {
  31. "count_range_1": 1,
  32. "count_range_2": 3,
  33. "transaction": self.prototype["transaction"],
  34. "project": self.project.slug,
  35. }
  36. def assert_event(self, data):
  37. for key, value in self.expected_data.items():
  38. assert data[key] == value, key
  39. @region_silo_test
  40. class OrganizationEventsTrendsEndpointTest(OrganizationEventsTrendsBase):
  41. def setUp(self):
  42. super().setUp()
  43. self.url = reverse(
  44. "sentry-api-0-organization-events-trends",
  45. kwargs={"organization_slug": self.project.organization.slug},
  46. )
  47. self.features = {"organizations:performance-view": True}
  48. def test_simple(self):
  49. with self.feature(self.features):
  50. response = self.client.get(
  51. self.url,
  52. format="json",
  53. data={
  54. "end": iso_format(self.day_ago + timedelta(hours=2)),
  55. "start": iso_format(self.day_ago),
  56. "field": ["project", "transaction"],
  57. "query": "event.type:transaction",
  58. "trendType": "regression",
  59. },
  60. )
  61. assert response.status_code == 200, response.content
  62. events = response.data
  63. assert len(events["data"]) == 1
  64. self.expected_data.update(
  65. {
  66. "aggregate_range_1": 2000,
  67. "aggregate_range_2": 2000,
  68. "count_percentage": 3.0,
  69. "trend_difference": 0.0,
  70. "trend_percentage": 1.0,
  71. }
  72. )
  73. self.assert_event(events["data"][0])
  74. def test_web_vital(self):
  75. with self.feature(self.features):
  76. response = self.client.get(
  77. self.url,
  78. format="json",
  79. data={
  80. "end": iso_format(self.day_ago + timedelta(hours=2)),
  81. "start": iso_format(self.day_ago),
  82. "field": ["project", "transaction"],
  83. "query": "event.type:transaction",
  84. "trendType": "regression",
  85. "trendFunction": "p50(measurements.lcp)",
  86. },
  87. )
  88. assert response.status_code == 200, response.content
  89. events = response.data
  90. assert len(events["data"]) == 1
  91. # LCP values are identical to duration
  92. self.expected_data.update(
  93. {
  94. "aggregate_range_1": 2000,
  95. "aggregate_range_2": 2000,
  96. "count_percentage": 3.0,
  97. "trend_difference": 0.0,
  98. "trend_percentage": 1.0,
  99. }
  100. )
  101. self.assert_event(events["data"][0])
  102. def test_p75(self):
  103. with self.feature(self.features):
  104. response = self.client.get(
  105. self.url,
  106. format="json",
  107. data={
  108. "end": iso_format(self.day_ago + timedelta(hours=2)),
  109. "start": iso_format(self.day_ago),
  110. "field": ["project", "transaction"],
  111. "query": "event.type:transaction",
  112. "trendFunction": "p75()",
  113. },
  114. )
  115. assert response.status_code == 200, response.content
  116. events = response.data
  117. assert len(events["data"]) == 1
  118. self.expected_data.update(
  119. {
  120. "aggregate_range_1": 2000,
  121. "aggregate_range_2": 6000,
  122. "count_percentage": 3.0,
  123. "trend_difference": 4000.0,
  124. "trend_percentage": 3.0,
  125. }
  126. )
  127. self.assert_event(events["data"][0])
  128. def test_p95(self):
  129. with self.feature(self.features):
  130. response = self.client.get(
  131. self.url,
  132. format="json",
  133. data={
  134. "end": iso_format(self.day_ago + timedelta(hours=2)),
  135. "start": iso_format(self.day_ago),
  136. "field": ["project", "transaction"],
  137. "query": "event.type:transaction",
  138. "trendFunction": "p95()",
  139. },
  140. )
  141. assert response.status_code == 200, response.content
  142. events = response.data
  143. assert len(events["data"]) == 1
  144. self.expected_data.update(
  145. {
  146. "aggregate_range_1": 2000,
  147. "aggregate_range_2": 9200,
  148. "count_percentage": 3.0,
  149. "trend_difference": 7200.0,
  150. "trend_percentage": 4.6,
  151. }
  152. )
  153. self.assert_event(events["data"][0])
  154. def test_p99(self):
  155. with self.feature(self.features):
  156. response = self.client.get(
  157. self.url,
  158. format="json",
  159. data={
  160. "end": iso_format(self.day_ago + timedelta(hours=2)),
  161. "start": iso_format(self.day_ago),
  162. "field": ["project", "transaction"],
  163. "query": "event.type:transaction",
  164. "trendFunction": "p99()",
  165. },
  166. )
  167. assert response.status_code == 200, response.content
  168. events = response.data
  169. assert len(events["data"]) == 1
  170. self.expected_data.update(
  171. {
  172. "aggregate_range_1": 2000,
  173. "aggregate_range_2": 9840,
  174. "count_percentage": 3.0,
  175. "trend_difference": 7840.0,
  176. "trend_percentage": 4.92,
  177. }
  178. )
  179. self.assert_event(events["data"][0])
  180. def test_trend_percentage_query_alias(self):
  181. queries = [
  182. ("trend_percentage():>0%", "regression", 1),
  183. ("trend_percentage():392%", "regression", 1),
  184. ("trend_percentage():>0%", "improved", 0),
  185. ("trend_percentage():392%", "improved", 0),
  186. ]
  187. for query_data in queries:
  188. with self.feature(self.features):
  189. response = self.client.get(
  190. self.url,
  191. format="json",
  192. data={
  193. "end": iso_format(self.day_ago + timedelta(hours=2)),
  194. "start": iso_format(self.day_ago),
  195. "field": ["project", "transaction"],
  196. "query": f"event.type:transaction {query_data[0]}",
  197. "trendType": query_data[1],
  198. # Use p99 since it has the most significant change
  199. "trendFunction": "p99()",
  200. },
  201. )
  202. assert response.status_code == 200, response.content
  203. events = response.data
  204. assert len(events["data"]) == query_data[2], query_data
  205. def test_trend_percentage_query_alias_as_sort(self):
  206. with self.feature(self.features):
  207. response = self.client.get(
  208. self.url,
  209. format="json",
  210. data={
  211. "end": iso_format(self.day_ago + timedelta(hours=2)),
  212. "start": iso_format(self.day_ago),
  213. "field": ["project", "transaction"],
  214. "query": "event.type:transaction",
  215. "trendType": "improved",
  216. "trendFunction": "p50()",
  217. "sort": "trend_percentage()",
  218. },
  219. )
  220. assert response.status_code == 200, response.content
  221. events = response.data
  222. assert len(events["data"]) == 1
  223. def test_trend_difference_query_alias(self):
  224. queries = [
  225. ("trend_difference():>7s", "regression", 1),
  226. ("trend_difference():7.84s", "regression", 1),
  227. ("trend_difference():>7s", "improved", 0),
  228. ("trend_difference():7.84s", "improved", 0),
  229. ]
  230. for query_data in queries:
  231. with self.feature(self.features):
  232. response = self.client.get(
  233. self.url,
  234. format="json",
  235. data={
  236. "end": iso_format(self.day_ago + timedelta(hours=2)),
  237. "start": iso_format(self.day_ago),
  238. "field": ["project", "transaction"],
  239. "query": f"event.type:transaction {query_data[0]}",
  240. "trendType": query_data[1],
  241. # Use p99 since it has the most significant change
  242. "trendFunction": "p99()",
  243. },
  244. )
  245. assert response.status_code == 200, response.content
  246. events = response.data
  247. assert len(events["data"]) == query_data[2], query_data
  248. def test_avg_trend_function(self):
  249. with self.feature(self.features):
  250. response = self.client.get(
  251. self.url,
  252. format="json",
  253. data={
  254. "end": iso_format(self.day_ago + timedelta(hours=2)),
  255. "start": iso_format(self.day_ago),
  256. "field": ["project", "transaction"],
  257. "query": "event.type:transaction",
  258. "trendFunction": "avg(transaction.duration)",
  259. "project": [self.project.id],
  260. },
  261. )
  262. assert response.status_code == 200, response.content
  263. events = response.data
  264. assert len(events["data"]) == 1
  265. self.expected_data.update(
  266. {
  267. "aggregate_range_1": 2000,
  268. "aggregate_range_2": 4000,
  269. "count_percentage": 3.0,
  270. "trend_difference": 2000.0,
  271. "trend_percentage": 2.0,
  272. }
  273. )
  274. self.assert_event(events["data"][0])
  275. def test_invalid_trend_function(self):
  276. with self.feature(self.features):
  277. response = self.client.get(
  278. self.url,
  279. format="json",
  280. data={
  281. "end": iso_format(self.day_ago + timedelta(hours=2)),
  282. "start": iso_format(self.day_ago),
  283. "field": ["project", "transaction"],
  284. "query": "event.type:transaction",
  285. "trendFunction": "apdex(450)",
  286. "project": [self.project.id],
  287. },
  288. )
  289. assert response.status_code == 400
  290. def test_divide_by_zero(self):
  291. with self.feature(self.features):
  292. response = self.client.get(
  293. self.url,
  294. format="json",
  295. data={
  296. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  297. "end": iso_format(self.day_ago + timedelta(hours=2)),
  298. "start": iso_format(self.day_ago - timedelta(hours=2)),
  299. "field": ["project", "transaction"],
  300. "query": "event.type:transaction",
  301. "project": [self.project.id],
  302. },
  303. )
  304. assert response.status_code == 200, response.content
  305. events = response.data
  306. assert len(events["data"]) == 1
  307. self.expected_data.update(
  308. {
  309. "count_range_2": 4,
  310. "count_range_1": 0,
  311. "aggregate_range_1": 0,
  312. "aggregate_range_2": 2000.0,
  313. "count_percentage": None,
  314. "trend_difference": 0,
  315. "trend_percentage": None,
  316. }
  317. )
  318. self.assert_event(events["data"][0])
  319. def test_auto_aggregation(self):
  320. # absolute_correlation is automatically added, and not a part of data otherwise
  321. with self.feature(self.features):
  322. response = self.client.get(
  323. self.url,
  324. format="json",
  325. data={
  326. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  327. "end": iso_format(self.day_ago + timedelta(hours=2)),
  328. "start": iso_format(self.day_ago - timedelta(hours=2)),
  329. "field": ["project", "transaction"],
  330. "query": "event.type:transaction absolute_correlation():>0.2",
  331. "project": [self.project.id],
  332. },
  333. )
  334. assert response.status_code == 200, response.content
  335. events = response.data
  336. assert len(events["data"]) == 1
  337. self.expected_data.update(
  338. {
  339. "count_range_2": 4,
  340. "count_range_1": 0,
  341. "aggregate_range_1": 0,
  342. "aggregate_range_2": 2000.0,
  343. "count_percentage": None,
  344. "trend_difference": 0,
  345. "trend_percentage": None,
  346. }
  347. )
  348. self.assert_event(events["data"][0])
  349. @region_silo_test
  350. class OrganizationEventsTrendsStatsEndpointTest(OrganizationEventsTrendsBase):
  351. def setUp(self):
  352. super().setUp()
  353. self.url = reverse(
  354. "sentry-api-0-organization-events-trends-stats",
  355. kwargs={"organization_slug": self.project.organization.slug},
  356. )
  357. self.features = {"organizations:performance-view": True}
  358. def test_simple(self):
  359. with self.feature(self.features):
  360. response = self.client.get(
  361. self.url,
  362. format="json",
  363. data={
  364. "end": iso_format(self.day_ago + timedelta(hours=2)),
  365. "start": iso_format(self.day_ago),
  366. "interval": "1h",
  367. "field": ["project", "transaction"],
  368. "query": "event.type:transaction",
  369. },
  370. )
  371. assert response.status_code == 200, response.content
  372. events = response.data["events"]
  373. result_stats = response.data["stats"]
  374. assert len(events["data"]) == 1
  375. self.expected_data.update(
  376. {
  377. "aggregate_range_1": 2000,
  378. "aggregate_range_2": 2000,
  379. "count_percentage": 3.0,
  380. "trend_difference": 0.0,
  381. "trend_percentage": 1.0,
  382. }
  383. )
  384. self.assert_event(events["data"][0])
  385. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  386. assert [attrs for time, attrs in stats["data"]] == [
  387. [{"count": 2000}],
  388. [{"count": 2000}],
  389. ]
  390. def test_web_vital(self):
  391. with self.feature(self.features):
  392. response = self.client.get(
  393. self.url,
  394. format="json",
  395. data={
  396. "end": iso_format(self.day_ago + timedelta(hours=2)),
  397. "start": iso_format(self.day_ago),
  398. "interval": "1h",
  399. "field": ["project", "transaction"],
  400. "query": "event.type:transaction",
  401. "trendFunction": "p50(measurements.lcp)",
  402. },
  403. )
  404. assert response.status_code == 200, response.content
  405. events = response.data["events"]
  406. result_stats = response.data["stats"]
  407. assert len(events["data"]) == 1
  408. self.expected_data.update(
  409. {
  410. "aggregate_range_1": 2000,
  411. "aggregate_range_2": 2000,
  412. "count_percentage": 3.0,
  413. "trend_difference": 0.0,
  414. "trend_percentage": 1.0,
  415. }
  416. )
  417. self.assert_event(events["data"][0])
  418. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  419. assert [attrs for time, attrs in stats["data"]] == [
  420. [{"count": 2000}],
  421. [{"count": 2000}],
  422. ]
  423. def test_p75(self):
  424. with self.feature(self.features):
  425. response = self.client.get(
  426. self.url,
  427. format="json",
  428. data={
  429. "end": iso_format(self.day_ago + timedelta(hours=2)),
  430. "start": iso_format(self.day_ago),
  431. "interval": "1h",
  432. "field": ["project", "transaction"],
  433. "query": "event.type:transaction",
  434. "trendFunction": "p75()",
  435. },
  436. )
  437. assert response.status_code == 200, response.content
  438. events = response.data["events"]
  439. result_stats = response.data["stats"]
  440. assert len(events["data"]) == 1
  441. self.expected_data.update(
  442. {
  443. "aggregate_range_1": 2000,
  444. "aggregate_range_2": 6000,
  445. "count_percentage": 3.0,
  446. "trend_difference": 4000.0,
  447. "trend_percentage": 3.0,
  448. }
  449. )
  450. self.assert_event(events["data"][0])
  451. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  452. assert [attrs for time, attrs in stats["data"]] == [
  453. [{"count": 2000}],
  454. [{"count": 6000}],
  455. ]
  456. def test_p95(self):
  457. with self.feature(self.features):
  458. response = self.client.get(
  459. self.url,
  460. format="json",
  461. data={
  462. "end": iso_format(self.day_ago + timedelta(hours=2)),
  463. "start": iso_format(self.day_ago),
  464. "interval": "1h",
  465. "field": ["project", "transaction"],
  466. "query": "event.type:transaction",
  467. "trendFunction": "p95()",
  468. },
  469. )
  470. assert response.status_code == 200, response.content
  471. events = response.data["events"]
  472. result_stats = response.data["stats"]
  473. assert len(events["data"]) == 1
  474. self.expected_data.update(
  475. {
  476. "aggregate_range_1": 2000,
  477. "aggregate_range_2": 9200,
  478. "count_percentage": 3.0,
  479. "trend_difference": 7200.0,
  480. "trend_percentage": 4.6,
  481. }
  482. )
  483. self.assert_event(events["data"][0])
  484. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  485. assert [attrs for time, attrs in stats["data"]] == [
  486. [{"count": 2000}],
  487. [{"count": 9200}],
  488. ]
  489. def test_p99(self):
  490. with self.feature(self.features):
  491. response = self.client.get(
  492. self.url,
  493. format="json",
  494. data={
  495. "end": iso_format(self.day_ago + timedelta(hours=2)),
  496. "start": iso_format(self.day_ago),
  497. "interval": "1h",
  498. "field": ["project", "transaction"],
  499. "query": "event.type:transaction",
  500. "trendFunction": "p99()",
  501. },
  502. )
  503. assert response.status_code == 200, response.content
  504. events = response.data["events"]
  505. result_stats = response.data["stats"]
  506. assert len(events["data"]) == 1
  507. self.expected_data.update(
  508. {
  509. "aggregate_range_1": 2000,
  510. "aggregate_range_2": 9840,
  511. "count_percentage": 3.0,
  512. "trend_difference": 7840.0,
  513. "trend_percentage": 4.92,
  514. }
  515. )
  516. self.assert_event(events["data"][0])
  517. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  518. assert [attrs for time, attrs in stats["data"]] == [
  519. [{"count": 2000}],
  520. [{"count": 9840}],
  521. ]
  522. def test_avg_trend_function(self):
  523. with self.feature(self.features):
  524. response = self.client.get(
  525. self.url,
  526. format="json",
  527. data={
  528. "end": iso_format(self.day_ago + timedelta(hours=2)),
  529. "interval": "1h",
  530. "start": iso_format(self.day_ago),
  531. "field": ["project", "transaction"],
  532. "query": "event.type:transaction",
  533. "trendFunction": "avg(transaction.duration)",
  534. "project": [self.project.id],
  535. },
  536. )
  537. assert response.status_code == 200, response.content
  538. events = response.data["events"]
  539. result_stats = response.data["stats"]
  540. assert len(events["data"]) == 1
  541. self.expected_data.update(
  542. {
  543. "aggregate_range_1": 2000,
  544. "aggregate_range_2": 4000,
  545. "count_percentage": 3.0,
  546. "trend_difference": 2000.0,
  547. "trend_percentage": 2.0,
  548. }
  549. )
  550. self.assert_event(events["data"][0])
  551. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  552. assert [attrs for time, attrs in stats["data"]] == [
  553. [{"count": 2000}],
  554. [{"count": 4000}],
  555. ]
  556. def test_alias_in_conditions(self):
  557. query_parts = [
  558. "event.type:transaction",
  559. "count_percentage():>0.25",
  560. "count_percentage():<4",
  561. "trend_percentage():>0%",
  562. ]
  563. queries = [" ".join(query_parts), " AND ".join(query_parts)]
  564. for query in queries:
  565. with self.feature(self.features):
  566. response = self.client.get(
  567. self.url,
  568. format="json",
  569. data={
  570. "end": iso_format(self.day_ago + timedelta(hours=2)),
  571. "interval": "1h",
  572. "start": iso_format(self.day_ago),
  573. "field": ["project", "transaction"],
  574. "query": query,
  575. "trendFunction": "avg(transaction.duration)",
  576. "project": [self.project.id],
  577. },
  578. )
  579. assert response.status_code == 200, response.content
  580. events = response.data["events"]
  581. result_stats = response.data["stats"]
  582. assert len(events["data"]) == 1
  583. self.expected_data.update(
  584. {
  585. "aggregate_range_1": 2000,
  586. "aggregate_range_2": 4000,
  587. "count_percentage": 3.0,
  588. "trend_difference": 2000.0,
  589. "trend_percentage": 2.0,
  590. }
  591. )
  592. self.assert_event(events["data"][0])
  593. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  594. assert [attrs for time, attrs in stats["data"]] == [
  595. [{"count": 2000}],
  596. [{"count": 4000}],
  597. ]
  598. def test_trend_with_middle(self):
  599. with self.feature(self.features):
  600. response = self.client.get(
  601. self.url,
  602. format="json",
  603. data={
  604. "end": iso_format(self.day_ago + timedelta(hours=2)),
  605. "middle": iso_format(self.day_ago + timedelta(hours=1, minutes=31)),
  606. "start": iso_format(self.day_ago),
  607. "interval": "1h",
  608. "field": ["project", "transaction"],
  609. "query": "event.type:transaction",
  610. "trendFunction": "avg(transaction.duration)",
  611. "project": [self.project.id],
  612. },
  613. )
  614. assert response.status_code == 200, response.content
  615. events = response.data["events"]
  616. result_stats = response.data["stats"]
  617. assert len(events["data"]) == 1
  618. self.expected_data.update(
  619. {
  620. "count_range_2": 2,
  621. "count_range_1": 2,
  622. "aggregate_range_1": 1000,
  623. "aggregate_range_2": 6000,
  624. "count_percentage": 1.0,
  625. "trend_difference": 5000.0,
  626. "trend_percentage": 6.0,
  627. }
  628. )
  629. self.assert_event(events["data"][0])
  630. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  631. assert [attrs for time, attrs in stats["data"]] == [
  632. [{"count": 2000}],
  633. [{"count": 4000}],
  634. ]
  635. def test_invalid_middle_date(self):
  636. with self.feature(self.features):
  637. response = self.client.get(
  638. self.url,
  639. format="json",
  640. data={
  641. "start": iso_format(self.day_ago),
  642. "middle": "blah",
  643. "end": iso_format(self.day_ago + timedelta(hours=2)),
  644. "field": ["project", "transaction"],
  645. "query": "event.type:transaction",
  646. "trendFunction": "p50()",
  647. "project": [self.project.id],
  648. },
  649. )
  650. assert response.status_code == 400
  651. response = self.client.get(
  652. self.url,
  653. format="json",
  654. data={
  655. "start": iso_format(self.day_ago),
  656. "middle": iso_format(self.day_ago - timedelta(hours=2)),
  657. "end": iso_format(self.day_ago + timedelta(hours=2)),
  658. "field": ["project", "transaction"],
  659. "query": "event.type:transaction",
  660. "trendFunction": "apdex(450)",
  661. "project": [self.project.id],
  662. },
  663. )
  664. assert response.status_code == 400
  665. response = self.client.get(
  666. self.url,
  667. format="json",
  668. data={
  669. "start": iso_format(self.day_ago),
  670. "middle": iso_format(self.day_ago + timedelta(hours=4)),
  671. "end": iso_format(self.day_ago + timedelta(hours=2)),
  672. "field": ["project", "transaction"],
  673. "query": "event.type:transaction",
  674. "trendFunction": "apdex(450)",
  675. "project": [self.project.id],
  676. },
  677. )
  678. assert response.status_code == 400
  679. def test_invalid_trend_function(self):
  680. with self.feature(self.features):
  681. response = self.client.get(
  682. self.url,
  683. format="json",
  684. data={
  685. "end": iso_format(self.day_ago + timedelta(hours=2)),
  686. "start": iso_format(self.day_ago),
  687. "field": ["project", "transaction"],
  688. "query": "event.type:transaction",
  689. "trendFunction": "apdex(450)",
  690. "project": [self.project.id],
  691. },
  692. )
  693. assert response.status_code == 400
  694. def test_divide_by_zero(self):
  695. with self.feature(self.features):
  696. response = self.client.get(
  697. self.url,
  698. format="json",
  699. data={
  700. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  701. "end": iso_format(self.day_ago + timedelta(hours=2)),
  702. "start": iso_format(self.day_ago - timedelta(hours=2)),
  703. "interval": "1h",
  704. "field": ["project", "transaction"],
  705. "query": "event.type:transaction",
  706. "project": [self.project.id],
  707. },
  708. )
  709. assert response.status_code == 200, response.content
  710. events = response.data["events"]
  711. result_stats = response.data["stats"]
  712. assert len(events["data"]) == 1
  713. self.expected_data.update(
  714. {
  715. "count_range_2": 4,
  716. "count_range_1": 0,
  717. "aggregate_range_1": 0,
  718. "aggregate_range_2": 2000.0,
  719. "count_percentage": None,
  720. "trend_difference": 0,
  721. "trend_percentage": None,
  722. }
  723. )
  724. self.assert_event(events["data"][0])
  725. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  726. assert [attrs for time, attrs in stats["data"]] == [
  727. [{"count": 0}],
  728. [{"count": 0}],
  729. [{"count": 2000}],
  730. [{"count": 2000}],
  731. ]
  732. @region_silo_test
  733. class OrganizationEventsTrendsPagingTest(APITestCase, SnubaTestCase):
  734. def setUp(self):
  735. super().setUp()
  736. self.login_as(user=self.user)
  737. self.url = reverse(
  738. "sentry-api-0-organization-events-trends-stats",
  739. kwargs={"organization_slug": self.project.organization.slug},
  740. )
  741. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  742. self.prototype = load_data("transaction")
  743. self.features = {"organizations:performance-view": True}
  744. # Make 10 transactions for paging
  745. for i in range(10):
  746. for j in range(2):
  747. data = self.prototype.copy()
  748. data["user"] = {"email": "foo@example.com"}
  749. data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
  750. data["timestamp"] = iso_format(
  751. self.day_ago + timedelta(hours=j, minutes=30, seconds=2)
  752. )
  753. if i < 5:
  754. data["transaction"] = f"transaction_1{i}"
  755. else:
  756. data["transaction"] = f"transaction_2{i}"
  757. self.store_event(data, project_id=self.project.id)
  758. def _parse_links(self, header):
  759. # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
  760. links = {}
  761. for url, attrs in parse_link_header(header).items():
  762. links[attrs["rel"]] = attrs
  763. attrs["href"] = url
  764. return links
  765. def test_pagination(self):
  766. with self.feature(self.features):
  767. response = self.client.get(
  768. self.url,
  769. format="json",
  770. data={
  771. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  772. "end": iso_format(self.day_ago + timedelta(hours=2)),
  773. "start": iso_format(self.day_ago - timedelta(hours=2)),
  774. "field": ["project", "transaction"],
  775. "query": "event.type:transaction",
  776. "project": [self.project.id],
  777. },
  778. )
  779. assert response.status_code == 200, response.content
  780. links = self._parse_links(response["Link"])
  781. assert links["previous"]["results"] == "false"
  782. assert links["next"]["results"] == "true"
  783. assert len(response.data["events"]["data"]) == 5
  784. response = self.client.get(links["next"]["href"], format="json")
  785. assert response.status_code == 200, response.content
  786. links = self._parse_links(response["Link"])
  787. assert links["previous"]["results"] == "true"
  788. assert links["next"]["results"] == "false"
  789. assert len(response.data["events"]["data"]) == 5
  790. def test_pagination_with_query(self):
  791. with self.feature(self.features):
  792. response = self.client.get(
  793. self.url,
  794. format="json",
  795. data={
  796. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  797. "end": iso_format(self.day_ago + timedelta(hours=2)),
  798. "start": iso_format(self.day_ago - timedelta(hours=2)),
  799. "field": ["project", "transaction"],
  800. "query": "event.type:transaction transaction:transaction_1*",
  801. "project": [self.project.id],
  802. },
  803. )
  804. assert response.status_code == 200, response.content
  805. links = self._parse_links(response["Link"])
  806. assert links["previous"]["results"] == "false"
  807. assert links["next"]["results"] == "false"
  808. assert len(response.data["events"]["data"]) == 5