test_organization_events_trends.py 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. from datetime import timedelta
  2. from django.urls import reverse
  3. from sentry.api.endpoints.organization_events_trends import OrganizationEventsTrendsEndpointBase
  4. from sentry.search.events.filter import get_filter
  5. from sentry.testutils import APITestCase, SnubaTestCase
  6. from sentry.testutils.cases import TestCase
  7. from sentry.testutils.helpers import parse_link_header
  8. from sentry.testutils.helpers.datetime import before_now, iso_format
  9. from sentry.utils.samples import load_data
  10. class OrganizationEventsTrendsBase(APITestCase, SnubaTestCase):
  11. def setUp(self):
  12. super().setUp()
  13. self.login_as(user=self.user)
  14. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  15. self.prototype = load_data("transaction")
  16. data = self.prototype.copy()
  17. data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
  18. data["user"] = {"email": "foo@example.com"}
  19. data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=2))
  20. data["measurements"]["lcp"]["value"] = 2000
  21. self.store_event(data, project_id=self.project.id)
  22. second = [0, 2, 10]
  23. for i in range(3):
  24. data = self.prototype.copy()
  25. data["start_timestamp"] = iso_format(self.day_ago + timedelta(hours=1, minutes=30 + i))
  26. data["timestamp"] = iso_format(
  27. self.day_ago + timedelta(hours=1, minutes=30 + i, seconds=second[i])
  28. )
  29. data["measurements"]["lcp"]["value"] = second[i] * 1000
  30. data["user"] = {"email": f"foo{i}@example.com"}
  31. self.store_event(data, project_id=self.project.id)
  32. self.expected_data = {
  33. "count_range_1": 1,
  34. "count_range_2": 3,
  35. "transaction": self.prototype["transaction"],
  36. "project": self.project.slug,
  37. }
  38. def assert_event(self, data):
  39. for key, value in self.expected_data.items():
  40. assert data[key] == value, key
  41. class OrganizationEventsTrendsEndpointTest(OrganizationEventsTrendsBase):
  42. def setUp(self):
  43. super().setUp()
  44. self.url = reverse(
  45. "sentry-api-0-organization-events-trends",
  46. kwargs={"organization_slug": self.project.organization.slug},
  47. )
  48. self.features = {"organizations:performance-view": True}
  49. def test_simple(self):
  50. with self.feature(self.features):
  51. response = self.client.get(
  52. self.url,
  53. format="json",
  54. data={
  55. "end": iso_format(self.day_ago + timedelta(hours=2)),
  56. "start": iso_format(self.day_ago),
  57. "field": ["project", "transaction"],
  58. "query": "event.type:transaction",
  59. "trendType": "regression",
  60. },
  61. )
  62. assert response.status_code == 200, response.content
  63. events = response.data
  64. assert len(events["data"]) == 1
  65. self.expected_data.update(
  66. {
  67. "aggregate_range_1": 2000,
  68. "aggregate_range_2": 2000,
  69. "count_percentage": 3.0,
  70. "trend_difference": 0.0,
  71. "trend_percentage": 1.0,
  72. }
  73. )
  74. self.assert_event(events["data"][0])
  75. def test_web_vital(self):
  76. with self.feature(self.features):
  77. response = self.client.get(
  78. self.url,
  79. format="json",
  80. data={
  81. "end": iso_format(self.day_ago + timedelta(hours=2)),
  82. "start": iso_format(self.day_ago),
  83. "field": ["project", "transaction"],
  84. "query": "event.type:transaction",
  85. "trendType": "regression",
  86. "trendFunction": "p50(measurements.lcp)",
  87. },
  88. )
  89. assert response.status_code == 200, response.content
  90. events = response.data
  91. assert len(events["data"]) == 1
  92. # LCP values are identical to duration
  93. self.expected_data.update(
  94. {
  95. "aggregate_range_1": 2000,
  96. "aggregate_range_2": 2000,
  97. "count_percentage": 3.0,
  98. "trend_difference": 0.0,
  99. "trend_percentage": 1.0,
  100. }
  101. )
  102. self.assert_event(events["data"][0])
  103. def test_p75(self):
  104. with self.feature(self.features):
  105. response = self.client.get(
  106. self.url,
  107. format="json",
  108. data={
  109. "end": iso_format(self.day_ago + timedelta(hours=2)),
  110. "start": iso_format(self.day_ago),
  111. "field": ["project", "transaction"],
  112. "query": "event.type:transaction",
  113. "trendFunction": "p75()",
  114. },
  115. )
  116. assert response.status_code == 200, response.content
  117. events = response.data
  118. assert len(events["data"]) == 1
  119. self.expected_data.update(
  120. {
  121. "aggregate_range_1": 2000,
  122. "aggregate_range_2": 6000,
  123. "count_percentage": 3.0,
  124. "trend_difference": 4000.0,
  125. "trend_percentage": 3.0,
  126. }
  127. )
  128. self.assert_event(events["data"][0])
  129. def test_p95(self):
  130. with self.feature(self.features):
  131. response = self.client.get(
  132. self.url,
  133. format="json",
  134. data={
  135. "end": iso_format(self.day_ago + timedelta(hours=2)),
  136. "start": iso_format(self.day_ago),
  137. "field": ["project", "transaction"],
  138. "query": "event.type:transaction",
  139. "trendFunction": "p95()",
  140. },
  141. )
  142. assert response.status_code == 200, response.content
  143. events = response.data
  144. assert len(events["data"]) == 1
  145. self.expected_data.update(
  146. {
  147. "aggregate_range_1": 2000,
  148. "aggregate_range_2": 9200,
  149. "count_percentage": 3.0,
  150. "trend_difference": 7200.0,
  151. "trend_percentage": 4.6,
  152. }
  153. )
  154. self.assert_event(events["data"][0])
  155. def test_p99(self):
  156. with self.feature(self.features):
  157. response = self.client.get(
  158. self.url,
  159. format="json",
  160. data={
  161. "end": iso_format(self.day_ago + timedelta(hours=2)),
  162. "start": iso_format(self.day_ago),
  163. "field": ["project", "transaction"],
  164. "query": "event.type:transaction",
  165. "trendFunction": "p99()",
  166. },
  167. )
  168. assert response.status_code == 200, response.content
  169. events = response.data
  170. assert len(events["data"]) == 1
  171. self.expected_data.update(
  172. {
  173. "aggregate_range_1": 2000,
  174. "aggregate_range_2": 9840,
  175. "count_percentage": 3.0,
  176. "trend_difference": 7840.0,
  177. "trend_percentage": 4.92,
  178. }
  179. )
  180. self.assert_event(events["data"][0])
  181. def test_trend_percentage_query_alias(self):
  182. queries = [
  183. ("trend_percentage():>0%", "regression", 1),
  184. ("trend_percentage():392%", "regression", 1),
  185. ("trend_percentage():>0%", "improved", 0),
  186. ("trend_percentage():392%", "improved", 0),
  187. ]
  188. for query_data in queries:
  189. with self.feature(self.features):
  190. response = self.client.get(
  191. self.url,
  192. format="json",
  193. data={
  194. "end": iso_format(self.day_ago + timedelta(hours=2)),
  195. "start": iso_format(self.day_ago),
  196. "field": ["project", "transaction"],
  197. "query": f"event.type:transaction {query_data[0]}",
  198. "trendType": query_data[1],
  199. # Use p99 since it has the most significant change
  200. "trendFunction": "p99()",
  201. },
  202. )
  203. assert response.status_code == 200, response.content
  204. events = response.data
  205. assert len(events["data"]) == query_data[2], query_data
  206. def test_trend_percentage_query_alias_as_sort(self):
  207. with self.feature(self.features):
  208. response = self.client.get(
  209. self.url,
  210. format="json",
  211. data={
  212. "end": iso_format(self.day_ago + timedelta(hours=2)),
  213. "start": iso_format(self.day_ago),
  214. "field": ["project", "transaction"],
  215. "query": "event.type:transaction",
  216. "trendType": "improved",
  217. "trendFunction": "p50()",
  218. "sort": "trend_percentage()",
  219. },
  220. )
  221. assert response.status_code == 200, response.content
  222. events = response.data
  223. assert len(events["data"]) == 1
  224. def test_trend_difference_query_alias(self):
  225. queries = [
  226. ("trend_difference():>7s", "regression", 1),
  227. ("trend_difference():7.84s", "regression", 1),
  228. ("trend_difference():>7s", "improved", 0),
  229. ("trend_difference():7.84s", "improved", 0),
  230. ]
  231. for query_data in queries:
  232. with self.feature(self.features):
  233. response = self.client.get(
  234. self.url,
  235. format="json",
  236. data={
  237. "end": iso_format(self.day_ago + timedelta(hours=2)),
  238. "start": iso_format(self.day_ago),
  239. "field": ["project", "transaction"],
  240. "query": f"event.type:transaction {query_data[0]}",
  241. "trendType": query_data[1],
  242. # Use p99 since it has the most significant change
  243. "trendFunction": "p99()",
  244. },
  245. )
  246. assert response.status_code == 200, response.content
  247. events = response.data
  248. assert len(events["data"]) == query_data[2], query_data
  249. def test_avg_trend_function(self):
  250. with self.feature(self.features):
  251. response = self.client.get(
  252. self.url,
  253. format="json",
  254. data={
  255. "end": iso_format(self.day_ago + timedelta(hours=2)),
  256. "start": iso_format(self.day_ago),
  257. "field": ["project", "transaction"],
  258. "query": "event.type:transaction",
  259. "trendFunction": "avg(transaction.duration)",
  260. "project": [self.project.id],
  261. },
  262. )
  263. assert response.status_code == 200, response.content
  264. events = response.data
  265. assert len(events["data"]) == 1
  266. self.expected_data.update(
  267. {
  268. "aggregate_range_1": 2000,
  269. "aggregate_range_2": 4000,
  270. "count_percentage": 3.0,
  271. "trend_difference": 2000.0,
  272. "trend_percentage": 2.0,
  273. }
  274. )
  275. self.assert_event(events["data"][0])
  276. def test_invalid_trend_function(self):
  277. with self.feature(self.features):
  278. response = self.client.get(
  279. self.url,
  280. format="json",
  281. data={
  282. "end": iso_format(self.day_ago + timedelta(hours=2)),
  283. "start": iso_format(self.day_ago),
  284. "field": ["project", "transaction"],
  285. "query": "event.type:transaction",
  286. "trendFunction": "apdex(450)",
  287. "project": [self.project.id],
  288. },
  289. )
  290. assert response.status_code == 400
  291. def test_divide_by_zero(self):
  292. with self.feature(self.features):
  293. response = self.client.get(
  294. self.url,
  295. format="json",
  296. data={
  297. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  298. "end": iso_format(self.day_ago + timedelta(hours=2)),
  299. "start": iso_format(self.day_ago - timedelta(hours=2)),
  300. "field": ["project", "transaction"],
  301. "query": "event.type:transaction",
  302. "project": [self.project.id],
  303. },
  304. )
  305. assert response.status_code == 200, response.content
  306. events = response.data
  307. assert len(events["data"]) == 1
  308. self.expected_data.update(
  309. {
  310. "count_range_2": 4,
  311. "count_range_1": 0,
  312. "aggregate_range_1": 0,
  313. "aggregate_range_2": 2000.0,
  314. "count_percentage": None,
  315. "trend_difference": 0,
  316. "trend_percentage": None,
  317. }
  318. )
  319. self.assert_event(events["data"][0])
  320. def test_auto_aggregation(self):
  321. # absolute_correlation is automatically added, and not a part of data otherwise
  322. with self.feature(self.features):
  323. response = self.client.get(
  324. self.url,
  325. format="json",
  326. data={
  327. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  328. "end": iso_format(self.day_ago + timedelta(hours=2)),
  329. "start": iso_format(self.day_ago - timedelta(hours=2)),
  330. "field": ["project", "transaction"],
  331. "query": "event.type:transaction absolute_correlation():>0.2",
  332. "project": [self.project.id],
  333. },
  334. )
  335. assert response.status_code == 200, response.content
  336. events = response.data
  337. assert len(events["data"]) == 1
  338. self.expected_data.update(
  339. {
  340. "count_range_2": 4,
  341. "count_range_1": 0,
  342. "aggregate_range_1": 0,
  343. "aggregate_range_2": 2000.0,
  344. "count_percentage": None,
  345. "trend_difference": 0,
  346. "trend_percentage": None,
  347. }
  348. )
  349. self.assert_event(events["data"][0])
  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. class OrganizationEventsTrendsPagingTest(APITestCase, SnubaTestCase):
  733. def setUp(self):
  734. super().setUp()
  735. self.login_as(user=self.user)
  736. self.url = reverse(
  737. "sentry-api-0-organization-events-trends-stats",
  738. kwargs={"organization_slug": self.project.organization.slug},
  739. )
  740. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  741. self.prototype = load_data("transaction")
  742. self.features = {"organizations:performance-view": True}
  743. # Make 10 transactions for paging
  744. for i in range(10):
  745. for j in range(2):
  746. data = self.prototype.copy()
  747. data["user"] = {"email": "foo@example.com"}
  748. data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30))
  749. data["timestamp"] = iso_format(
  750. self.day_ago + timedelta(hours=j, minutes=30, seconds=2)
  751. )
  752. if i < 5:
  753. data["transaction"] = f"transaction_1{i}"
  754. else:
  755. data["transaction"] = f"transaction_2{i}"
  756. self.store_event(data, project_id=self.project.id)
  757. def _parse_links(self, header):
  758. # links come in {url: {...attrs}}, but we need {rel: {...attrs}}
  759. links = {}
  760. for url, attrs in parse_link_header(header).items():
  761. links[attrs["rel"]] = attrs
  762. attrs["href"] = url
  763. return links
  764. def test_pagination(self):
  765. with self.feature(self.features):
  766. response = self.client.get(
  767. self.url,
  768. format="json",
  769. data={
  770. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  771. "end": iso_format(self.day_ago + timedelta(hours=2)),
  772. "start": iso_format(self.day_ago - timedelta(hours=2)),
  773. "field": ["project", "transaction"],
  774. "query": "event.type:transaction",
  775. "project": [self.project.id],
  776. },
  777. )
  778. assert response.status_code == 200, response.content
  779. links = self._parse_links(response["Link"])
  780. assert links["previous"]["results"] == "false"
  781. assert links["next"]["results"] == "true"
  782. assert len(response.data["events"]["data"]) == 5
  783. response = self.client.get(links["next"]["href"], format="json")
  784. assert response.status_code == 200, response.content
  785. links = self._parse_links(response["Link"])
  786. assert links["previous"]["results"] == "true"
  787. assert links["next"]["results"] == "false"
  788. assert len(response.data["events"]["data"]) == 5
  789. def test_pagination_with_query(self):
  790. with self.feature(self.features):
  791. response = self.client.get(
  792. self.url,
  793. format="json",
  794. data={
  795. # Set the timeframe to where the second range has no transactions so all the counts/percentile are 0
  796. "end": iso_format(self.day_ago + timedelta(hours=2)),
  797. "start": iso_format(self.day_ago - timedelta(hours=2)),
  798. "field": ["project", "transaction"],
  799. "query": "event.type:transaction transaction:transaction_1*",
  800. "project": [self.project.id],
  801. },
  802. )
  803. assert response.status_code == 200, response.content
  804. links = self._parse_links(response["Link"])
  805. assert links["previous"]["results"] == "false"
  806. assert links["next"]["results"] == "false"
  807. assert len(response.data["events"]["data"]) == 5
  808. class OrganizationEventsTrendsAliasTest(TestCase):
  809. def setUp(self):
  810. self.improved_aliases = OrganizationEventsTrendsEndpointBase.get_function_aliases(
  811. "improved"
  812. )
  813. self.regression_aliases = OrganizationEventsTrendsEndpointBase.get_function_aliases(
  814. "regression"
  815. )
  816. def test_simple(self):
  817. result = get_filter(
  818. "trend_percentage():>0% trend_difference():>0", {"aliases": self.improved_aliases}
  819. )
  820. assert result.having == [
  821. ["trend_percentage", "<", 1.0],
  822. ["trend_difference", "<", 0.0],
  823. ]
  824. result = get_filter(
  825. "trend_percentage():>0% trend_difference():>0", {"aliases": self.regression_aliases}
  826. )
  827. assert result.having == [
  828. ["trend_percentage", ">", 1.0],
  829. ["trend_difference", ">", 0.0],
  830. ]
  831. def test_and_query(self):
  832. result = get_filter(
  833. "trend_percentage():>0% AND trend_percentage():<100%",
  834. {"aliases": self.improved_aliases},
  835. )
  836. assert result.having == [["trend_percentage", "<", 1.0], ["trend_percentage", ">", 0.0]]
  837. result = get_filter(
  838. "trend_percentage():>0% AND trend_percentage():<100%",
  839. {"aliases": self.regression_aliases},
  840. )
  841. assert result.having == [["trend_percentage", ">", 1.0], ["trend_percentage", "<", 2.0]]
  842. def test_or_query(self):
  843. result = get_filter(
  844. "trend_percentage():>0% OR trend_percentage():<100%",
  845. {"aliases": self.improved_aliases},
  846. )
  847. assert result.having == [
  848. [
  849. [
  850. "or",
  851. [["less", ["trend_percentage", 1.0]], ["greater", ["trend_percentage", 0.0]]],
  852. ],
  853. "=",
  854. 1,
  855. ]
  856. ]
  857. result = get_filter(
  858. "trend_percentage():>0% OR trend_percentage():<100%",
  859. {"aliases": self.regression_aliases},
  860. )
  861. assert result.having == [
  862. [
  863. [
  864. "or",
  865. [["greater", ["trend_percentage", 1.0]], ["less", ["trend_percentage", 2.0]]],
  866. ],
  867. "=",
  868. 1,
  869. ]
  870. ]
  871. def test_greater_than(self):
  872. result = get_filter("trend_difference():>=0", {"aliases": self.improved_aliases})
  873. assert result.having == [["trend_difference", "<=", 0.0]]
  874. result = get_filter("trend_difference():>=0", {"aliases": self.regression_aliases})
  875. assert result.having == [["trend_difference", ">=", 0.0]]
  876. def test_negation(self):
  877. result = get_filter("!trend_difference():>=0", {"aliases": self.improved_aliases})
  878. assert result.having == [["trend_difference", ">", 0.0]]
  879. result = get_filter("!trend_difference():>=0", {"aliases": self.regression_aliases})
  880. assert result.having == [["trend_difference", "<", 0.0]]
  881. def test_confidence(self):
  882. result = get_filter("confidence():>6", {"aliases": self.improved_aliases})
  883. assert result.having == [["t_test", ">", 6.0]]
  884. result = get_filter("confidence():>6", {"aliases": self.regression_aliases})
  885. assert result.having == [["t_test", "<", -6.0]]
  886. class OrganizationEventsTrendsEndpointTestWithSnql(OrganizationEventsTrendsEndpointTest):
  887. def setUp(self):
  888. super().setUp()
  889. self.features = {
  890. "organizations:performance-view": True,
  891. "organizations:trends-use-snql": True,
  892. }
  893. class OrganizationEventsTrendsStatsEndpointTestWithSnql(OrganizationEventsTrendsStatsEndpointTest):
  894. def setUp(self):
  895. super().setUp()
  896. self.features = {
  897. "organizations:performance-view": True,
  898. "organizations:trends-use-snql": True,
  899. }
  900. class OrganizationEventsTrendsPagingTestWithSnql(OrganizationEventsTrendsPagingTest):
  901. def setUp(self):
  902. super().setUp()
  903. self.features = {
  904. "organizations:performance-view": True,
  905. "organizations:trends-use-snql": True,
  906. }