test_organization_events_trends.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  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
  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"] = (self.day_ago + timedelta(minutes=30)).isoformat()
  15. data["user"] = {"email": "foo@example.com"}
  16. data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=2)).isoformat()
  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"] = (
  23. self.day_ago + timedelta(hours=1, minutes=30 + i)
  24. ).isoformat()
  25. data["timestamp"] = (
  26. self.day_ago + timedelta(hours=1, minutes=30 + i, seconds=second[i])
  27. ).isoformat()
  28. data["measurements"]["lcp"]["value"] = second[i] * 1000
  29. data["user"] = {"email": f"foo{i}@example.com"}
  30. self.store_event(data, project_id=self.project.id)
  31. self.expected_data = {
  32. "count_range_1": 1,
  33. "count_range_2": 3,
  34. "transaction": self.prototype["transaction"],
  35. "project": self.project.slug,
  36. }
  37. def assert_event(self, data):
  38. for key, value in self.expected_data.items():
  39. assert data[key] == value, key
  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_id_or_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": (self.day_ago + timedelta(hours=2)).isoformat(),
  55. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  81. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  109. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  135. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  161. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  194. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  212. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  237. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  255. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  282. "start": self.day_ago.isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  298. "start": (self.day_ago - timedelta(hours=2)).isoformat(),
  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": (self.day_ago + timedelta(hours=2)).isoformat(),
  328. "start": (self.day_ago - timedelta(hours=2)).isoformat(),
  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. class OrganizationEventsTrendsStatsEndpointTest(OrganizationEventsTrendsBase):
  350. def setUp(self):
  351. super().setUp()
  352. self.url = reverse(
  353. "sentry-api-0-organization-events-trends-stats",
  354. kwargs={"organization_id_or_slug": self.project.organization.slug},
  355. )
  356. self.features = {"organizations:performance-view": True}
  357. def test_simple(self):
  358. with self.feature(self.features):
  359. response = self.client.get(
  360. self.url,
  361. format="json",
  362. data={
  363. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  364. "start": self.day_ago.isoformat(),
  365. "interval": "1h",
  366. "field": ["project", "transaction"],
  367. "query": "event.type:transaction",
  368. },
  369. )
  370. assert response.status_code == 200, response.content
  371. events = response.data["events"]
  372. result_stats = response.data["stats"]
  373. assert len(events["data"]) == 1
  374. self.expected_data.update(
  375. {
  376. "aggregate_range_1": 2000,
  377. "aggregate_range_2": 2000,
  378. "count_percentage": 3.0,
  379. "trend_difference": 0.0,
  380. "trend_percentage": 1.0,
  381. }
  382. )
  383. self.assert_event(events["data"][0])
  384. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  385. assert [attrs for time, attrs in stats["data"]] == [
  386. [{"count": 2000}],
  387. [{"count": 2000}],
  388. ]
  389. def test_web_vital(self):
  390. with self.feature(self.features):
  391. response = self.client.get(
  392. self.url,
  393. format="json",
  394. data={
  395. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  396. "start": self.day_ago.isoformat(),
  397. "interval": "1h",
  398. "field": ["project", "transaction"],
  399. "query": "event.type:transaction",
  400. "trendFunction": "p50(measurements.lcp)",
  401. },
  402. )
  403. assert response.status_code == 200, response.content
  404. events = response.data["events"]
  405. result_stats = response.data["stats"]
  406. assert len(events["data"]) == 1
  407. self.expected_data.update(
  408. {
  409. "aggregate_range_1": 2000,
  410. "aggregate_range_2": 2000,
  411. "count_percentage": 3.0,
  412. "trend_difference": 0.0,
  413. "trend_percentage": 1.0,
  414. }
  415. )
  416. self.assert_event(events["data"][0])
  417. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  418. assert [attrs for time, attrs in stats["data"]] == [
  419. [{"count": 2000}],
  420. [{"count": 2000}],
  421. ]
  422. def test_p75(self):
  423. with self.feature(self.features):
  424. response = self.client.get(
  425. self.url,
  426. format="json",
  427. data={
  428. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  429. "start": self.day_ago.isoformat(),
  430. "interval": "1h",
  431. "field": ["project", "transaction"],
  432. "query": "event.type:transaction",
  433. "trendFunction": "p75()",
  434. },
  435. )
  436. assert response.status_code == 200, response.content
  437. events = response.data["events"]
  438. result_stats = response.data["stats"]
  439. assert len(events["data"]) == 1
  440. self.expected_data.update(
  441. {
  442. "aggregate_range_1": 2000,
  443. "aggregate_range_2": 6000,
  444. "count_percentage": 3.0,
  445. "trend_difference": 4000.0,
  446. "trend_percentage": 3.0,
  447. }
  448. )
  449. self.assert_event(events["data"][0])
  450. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  451. assert [attrs for time, attrs in stats["data"]] == [
  452. [{"count": 2000}],
  453. [{"count": 6000}],
  454. ]
  455. def test_p95(self):
  456. with self.feature(self.features):
  457. response = self.client.get(
  458. self.url,
  459. format="json",
  460. data={
  461. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  462. "start": self.day_ago.isoformat(),
  463. "interval": "1h",
  464. "field": ["project", "transaction"],
  465. "query": "event.type:transaction",
  466. "trendFunction": "p95()",
  467. },
  468. )
  469. assert response.status_code == 200, response.content
  470. events = response.data["events"]
  471. result_stats = response.data["stats"]
  472. assert len(events["data"]) == 1
  473. self.expected_data.update(
  474. {
  475. "aggregate_range_1": 2000,
  476. "aggregate_range_2": 9200,
  477. "count_percentage": 3.0,
  478. "trend_difference": 7200.0,
  479. "trend_percentage": 4.6,
  480. }
  481. )
  482. self.assert_event(events["data"][0])
  483. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  484. assert [attrs for time, attrs in stats["data"]] == [
  485. [{"count": 2000}],
  486. [{"count": 9200}],
  487. ]
  488. def test_p99(self):
  489. with self.feature(self.features):
  490. response = self.client.get(
  491. self.url,
  492. format="json",
  493. data={
  494. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  495. "start": self.day_ago.isoformat(),
  496. "interval": "1h",
  497. "field": ["project", "transaction"],
  498. "query": "event.type:transaction",
  499. "trendFunction": "p99()",
  500. },
  501. )
  502. assert response.status_code == 200, response.content
  503. events = response.data["events"]
  504. result_stats = response.data["stats"]
  505. assert len(events["data"]) == 1
  506. self.expected_data.update(
  507. {
  508. "aggregate_range_1": 2000,
  509. "aggregate_range_2": 9840,
  510. "count_percentage": 3.0,
  511. "trend_difference": 7840.0,
  512. "trend_percentage": 4.92,
  513. }
  514. )
  515. self.assert_event(events["data"][0])
  516. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  517. assert [attrs for time, attrs in stats["data"]] == [
  518. [{"count": 2000}],
  519. [{"count": 9840}],
  520. ]
  521. def test_avg_trend_function(self):
  522. with self.feature(self.features):
  523. response = self.client.get(
  524. self.url,
  525. format="json",
  526. data={
  527. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  528. "interval": "1h",
  529. "start": self.day_ago.isoformat(),
  530. "field": ["project", "transaction"],
  531. "query": "event.type:transaction",
  532. "trendFunction": "avg(transaction.duration)",
  533. "project": [self.project.id],
  534. },
  535. )
  536. assert response.status_code == 200, response.content
  537. events = response.data["events"]
  538. result_stats = response.data["stats"]
  539. assert len(events["data"]) == 1
  540. self.expected_data.update(
  541. {
  542. "aggregate_range_1": 2000,
  543. "aggregate_range_2": 4000,
  544. "count_percentage": 3.0,
  545. "trend_difference": 2000.0,
  546. "trend_percentage": 2.0,
  547. }
  548. )
  549. self.assert_event(events["data"][0])
  550. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  551. assert [attrs for time, attrs in stats["data"]] == [
  552. [{"count": 2000}],
  553. [{"count": 4000}],
  554. ]
  555. def test_alias_in_conditions(self):
  556. query_parts = [
  557. "event.type:transaction",
  558. "count_percentage():>0.25",
  559. "count_percentage():<4",
  560. "trend_percentage():>0%",
  561. ]
  562. queries = [" ".join(query_parts), " AND ".join(query_parts)]
  563. for query in queries:
  564. with self.feature(self.features):
  565. response = self.client.get(
  566. self.url,
  567. format="json",
  568. data={
  569. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  570. "interval": "1h",
  571. "start": self.day_ago.isoformat(),
  572. "field": ["project", "transaction"],
  573. "query": query,
  574. "trendFunction": "avg(transaction.duration)",
  575. "project": [self.project.id],
  576. },
  577. )
  578. assert response.status_code == 200, response.content
  579. events = response.data["events"]
  580. result_stats = response.data["stats"]
  581. assert len(events["data"]) == 1
  582. self.expected_data.update(
  583. {
  584. "aggregate_range_1": 2000,
  585. "aggregate_range_2": 4000,
  586. "count_percentage": 3.0,
  587. "trend_difference": 2000.0,
  588. "trend_percentage": 2.0,
  589. }
  590. )
  591. self.assert_event(events["data"][0])
  592. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  593. assert [attrs for time, attrs in stats["data"]] == [
  594. [{"count": 2000}],
  595. [{"count": 4000}],
  596. ]
  597. def test_trend_with_middle(self):
  598. with self.feature(self.features):
  599. response = self.client.get(
  600. self.url,
  601. format="json",
  602. data={
  603. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  604. "middle": (self.day_ago + timedelta(hours=1, minutes=31)).isoformat(),
  605. "start": self.day_ago.isoformat(),
  606. "interval": "1h",
  607. "field": ["project", "transaction"],
  608. "query": "event.type:transaction",
  609. "trendFunction": "avg(transaction.duration)",
  610. "project": [self.project.id],
  611. },
  612. )
  613. assert response.status_code == 200, response.content
  614. events = response.data["events"]
  615. result_stats = response.data["stats"]
  616. assert len(events["data"]) == 1
  617. self.expected_data.update(
  618. {
  619. "count_range_2": 2,
  620. "count_range_1": 2,
  621. "aggregate_range_1": 1000,
  622. "aggregate_range_2": 6000,
  623. "count_percentage": 1.0,
  624. "trend_difference": 5000.0,
  625. "trend_percentage": 6.0,
  626. }
  627. )
  628. self.assert_event(events["data"][0])
  629. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  630. assert [attrs for time, attrs in stats["data"]] == [
  631. [{"count": 2000}],
  632. [{"count": 4000}],
  633. ]
  634. def test_invalid_middle_date(self):
  635. with self.feature(self.features):
  636. response = self.client.get(
  637. self.url,
  638. format="json",
  639. data={
  640. "start": self.day_ago.isoformat(),
  641. "middle": "blah",
  642. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  643. "field": ["project", "transaction"],
  644. "query": "event.type:transaction",
  645. "trendFunction": "p50()",
  646. "project": [self.project.id],
  647. },
  648. )
  649. assert response.status_code == 400
  650. response = self.client.get(
  651. self.url,
  652. format="json",
  653. data={
  654. "start": self.day_ago.isoformat(),
  655. "middle": (self.day_ago - timedelta(hours=2)).isoformat(),
  656. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  657. "field": ["project", "transaction"],
  658. "query": "event.type:transaction",
  659. "trendFunction": "apdex(450)",
  660. "project": [self.project.id],
  661. },
  662. )
  663. assert response.status_code == 400
  664. response = self.client.get(
  665. self.url,
  666. format="json",
  667. data={
  668. "start": self.day_ago.isoformat(),
  669. "middle": (self.day_ago + timedelta(hours=4)).isoformat(),
  670. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  671. "field": ["project", "transaction"],
  672. "query": "event.type:transaction",
  673. "trendFunction": "apdex(450)",
  674. "project": [self.project.id],
  675. },
  676. )
  677. assert response.status_code == 400
  678. def test_invalid_trend_function(self):
  679. with self.feature(self.features):
  680. response = self.client.get(
  681. self.url,
  682. format="json",
  683. data={
  684. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  685. "start": self.day_ago.isoformat(),
  686. "field": ["project", "transaction"],
  687. "query": "event.type:transaction",
  688. "trendFunction": "apdex(450)",
  689. "project": [self.project.id],
  690. },
  691. )
  692. assert response.status_code == 400
  693. def test_divide_by_zero(self):
  694. with self.feature(self.features):
  695. response = self.client.get(
  696. self.url,
  697. format="json",
  698. data={
  699. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  700. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  701. "start": (self.day_ago - timedelta(hours=2)).isoformat(),
  702. "interval": "1h",
  703. "field": ["project", "transaction"],
  704. "query": "event.type:transaction",
  705. "project": [self.project.id],
  706. },
  707. )
  708. assert response.status_code == 200, response.content
  709. events = response.data["events"]
  710. result_stats = response.data["stats"]
  711. assert len(events["data"]) == 1
  712. self.expected_data.update(
  713. {
  714. "count_range_2": 4,
  715. "count_range_1": 0,
  716. "aggregate_range_1": 0,
  717. "aggregate_range_2": 2000.0,
  718. "count_percentage": None,
  719. "trend_difference": 0,
  720. "trend_percentage": None,
  721. }
  722. )
  723. self.assert_event(events["data"][0])
  724. stats = result_stats[f"{self.project.slug},{self.prototype['transaction']}"]
  725. assert [attrs for time, attrs in stats["data"]] == [
  726. [{"count": 0}],
  727. [{"count": 0}],
  728. [{"count": 2000}],
  729. [{"count": 2000}],
  730. ]
  731. class OrganizationEventsTrendsPagingTest(APITestCase, SnubaTestCase):
  732. def setUp(self):
  733. super().setUp()
  734. self.login_as(user=self.user)
  735. self.url = reverse(
  736. "sentry-api-0-organization-events-trends-stats",
  737. kwargs={"organization_id_or_slug": self.project.organization.slug},
  738. )
  739. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  740. self.prototype = load_data("transaction")
  741. self.features = {"organizations:performance-view": True}
  742. # Make 10 transactions for paging
  743. for i in range(10):
  744. for j in range(2):
  745. data = self.prototype.copy()
  746. data["user"] = {"email": "foo@example.com"}
  747. data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat()
  748. data["timestamp"] = (
  749. self.day_ago + timedelta(hours=j, minutes=30, seconds=2)
  750. ).isoformat()
  751. if i < 5:
  752. data["transaction"] = f"transaction_1{i}"
  753. else:
  754. data["transaction"] = f"transaction_2{i}"
  755. self.store_event(data, project_id=self.project.id)
  756. def _parse_links(self, header):
  757. # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
  758. links = {}
  759. for url, attrs in parse_link_header(header).items():
  760. links[attrs["rel"]] = attrs
  761. attrs["href"] = url
  762. return links
  763. def test_pagination(self):
  764. with self.feature(self.features):
  765. response = self.client.get(
  766. self.url,
  767. format="json",
  768. data={
  769. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  770. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  771. "start": (self.day_ago - timedelta(hours=2)).isoformat(),
  772. "field": ["project", "transaction"],
  773. "query": "event.type:transaction",
  774. "project": [self.project.id],
  775. },
  776. )
  777. assert response.status_code == 200, response.content
  778. links = self._parse_links(response["Link"])
  779. assert links["previous"]["results"] == "false"
  780. assert links["next"]["results"] == "true"
  781. assert len(response.data["events"]["data"]) == 5
  782. response = self.client.get(links["next"]["href"], format="json")
  783. assert response.status_code == 200, response.content
  784. links = self._parse_links(response["Link"])
  785. assert links["previous"]["results"] == "true"
  786. assert links["next"]["results"] == "false"
  787. assert len(response.data["events"]["data"]) == 5
  788. def test_pagination_with_query(self):
  789. with self.feature(self.features):
  790. response = self.client.get(
  791. self.url,
  792. format="json",
  793. data={
  794. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  795. "end": (self.day_ago + timedelta(hours=2)).isoformat(),
  796. "start": (self.day_ago - timedelta(hours=2)).isoformat(),
  797. "field": ["project", "transaction"],
  798. "query": "event.type:transaction transaction:transaction_1*",
  799. "project": [self.project.id],
  800. },
  801. )
  802. assert response.status_code == 200, response.content
  803. links = self._parse_links(response["Link"])
  804. assert links["previous"]["results"] == "false"
  805. assert links["next"]["results"] == "false"
  806. assert len(response.data["events"]["data"]) == 5