test_organization_events_trends.py 33 KB

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