test_organization_events_stats_mep.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. from datetime import timedelta
  2. from unittest import mock
  3. import pytest
  4. from django.urls import reverse
  5. from sentry.testutils import MetricsEnhancedPerformanceTestCase
  6. from sentry.testutils.helpers.datetime import before_now, iso_format
  7. pytestmark = pytest.mark.sentry_metrics
  8. class OrganizationEventsStatsMetricsEnhancedPerformanceEndpointTest(
  9. MetricsEnhancedPerformanceTestCase
  10. ):
  11. endpoint = "sentry-api-0-organization-events-stats"
  12. METRIC_STRINGS = [
  13. "foo_transaction",
  14. "d:transactions/measurements.datacenter_memory@pebibyte",
  15. ]
  16. def setUp(self):
  17. super().setUp()
  18. self.login_as(user=self.user)
  19. self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  20. self.DEFAULT_METRIC_TIMESTAMP = self.day_ago
  21. self.url = reverse(
  22. "sentry-api-0-organization-events-stats",
  23. kwargs={"organization_slug": self.project.organization.slug},
  24. )
  25. self.features = {
  26. "organizations:performance-use-metrics": True,
  27. }
  28. def do_request(self, data, url=None, features=None):
  29. if features is None:
  30. features = {"organizations:discover-basic": True}
  31. features.update(self.features)
  32. with self.feature(features):
  33. return self.client.get(self.url if url is None else url, data=data, format="json")
  34. # These throughput tests should roughly match the ones in OrganizationEventsStatsEndpointTest
  35. def test_throughput_epm_hour_rollup(self):
  36. # Each of these denotes how many events to create in each hour
  37. event_counts = [6, 0, 6, 3, 0, 3]
  38. for hour, count in enumerate(event_counts):
  39. for minute in range(count):
  40. self.store_transaction_metric(
  41. 1, timestamp=self.day_ago + timedelta(hours=hour, minutes=minute)
  42. )
  43. for axis in ["epm()", "tpm()"]:
  44. response = self.do_request(
  45. data={
  46. "start": iso_format(self.day_ago),
  47. "end": iso_format(self.day_ago + timedelta(hours=6)),
  48. "interval": "1h",
  49. "yAxis": axis,
  50. "project": self.project.id,
  51. "dataset": "metricsEnhanced",
  52. },
  53. )
  54. assert response.status_code == 200, response.content
  55. data = response.data["data"]
  56. assert len(data) == 6
  57. assert response.data["isMetricsData"]
  58. rows = data[0:6]
  59. for test in zip(event_counts, rows):
  60. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  61. def test_throughput_epm_day_rollup(self):
  62. # Each of these denotes how many events to create in each minute
  63. event_counts = [6, 0, 6, 3, 0, 3]
  64. for hour, count in enumerate(event_counts):
  65. for minute in range(count):
  66. self.store_transaction_metric(
  67. 1, timestamp=self.day_ago + timedelta(hours=hour, minutes=minute)
  68. )
  69. for axis in ["epm()", "tpm()"]:
  70. response = self.do_request(
  71. data={
  72. "start": iso_format(self.day_ago),
  73. "end": iso_format(self.day_ago + timedelta(hours=24)),
  74. "interval": "24h",
  75. "yAxis": axis,
  76. "project": self.project.id,
  77. "dataset": "metricsEnhanced",
  78. },
  79. )
  80. assert response.status_code == 200, response.content
  81. data = response.data["data"]
  82. assert len(data) == 2
  83. assert response.data["isMetricsData"]
  84. assert data[0][1][0]["count"] == sum(event_counts) / (86400.0 / 60.0)
  85. def test_throughput_epm_hour_rollup_offset_of_hour(self):
  86. # Each of these denotes how many events to create in each hour
  87. event_counts = [6, 0, 6, 3, 0, 3]
  88. for hour, count in enumerate(event_counts):
  89. for minute in range(count):
  90. self.store_transaction_metric(
  91. 1, timestamp=self.day_ago + timedelta(hours=hour, minutes=minute + 30)
  92. )
  93. for axis in ["tpm()", "epm()"]:
  94. response = self.do_request(
  95. data={
  96. "start": iso_format(self.day_ago + timedelta(minutes=30)),
  97. "end": iso_format(self.day_ago + timedelta(hours=6, minutes=30)),
  98. "interval": "1h",
  99. "yAxis": axis,
  100. "project": self.project.id,
  101. "dataset": "metricsEnhanced",
  102. },
  103. )
  104. assert response.status_code == 200, response.content
  105. data = response.data["data"]
  106. assert len(data) == 6
  107. assert response.data["isMetricsData"]
  108. rows = data[0:6]
  109. for test in zip(event_counts, rows):
  110. assert test[1][1][0]["count"] == test[0] / (3600.0 / 60.0)
  111. def test_throughput_eps_minute_rollup(self):
  112. # Each of these denotes how many events to create in each minute
  113. event_counts = [6, 0, 6, 3, 0, 3]
  114. for minute, count in enumerate(event_counts):
  115. for second in range(count):
  116. self.store_transaction_metric(
  117. 1, timestamp=self.day_ago + timedelta(minutes=minute, seconds=second)
  118. )
  119. for axis in ["eps()", "tps()"]:
  120. response = self.do_request(
  121. data={
  122. "start": iso_format(self.day_ago),
  123. "end": iso_format(self.day_ago + timedelta(minutes=6)),
  124. "interval": "1m",
  125. "yAxis": axis,
  126. "project": self.project.id,
  127. "dataset": "metricsEnhanced",
  128. },
  129. )
  130. assert response.status_code == 200, response.content
  131. data = response.data["data"]
  132. assert len(data) == 6
  133. assert response.data["isMetricsData"]
  134. rows = data[0:6]
  135. for test in zip(event_counts, rows):
  136. assert test[1][1][0]["count"] == test[0] / 60.0
  137. def test_failure_rate(self):
  138. for hour in range(6):
  139. timestamp = self.day_ago + timedelta(hours=hour, minutes=30)
  140. self.store_transaction_metric(1, tags={"transaction.status": "ok"}, timestamp=timestamp)
  141. if hour < 3:
  142. self.store_transaction_metric(
  143. 1, tags={"transaction.status": "internal_error"}, timestamp=timestamp
  144. )
  145. response = self.do_request(
  146. data={
  147. "start": iso_format(self.day_ago),
  148. "end": iso_format(self.day_ago + timedelta(hours=6)),
  149. "interval": "1h",
  150. "yAxis": ["failure_rate()"],
  151. "project": self.project.id,
  152. "dataset": "metricsEnhanced",
  153. },
  154. )
  155. assert response.status_code == 200, response.content
  156. data = response.data["data"]
  157. assert len(data) == 6
  158. assert response.data["isMetricsData"]
  159. assert [attrs for time, attrs in response.data["data"]] == [
  160. [{"count": 0.5}],
  161. [{"count": 0.5}],
  162. [{"count": 0.5}],
  163. [{"count": 0}],
  164. [{"count": 0}],
  165. [{"count": 0}],
  166. ]
  167. def test_percentiles_multi_axis(self):
  168. for hour in range(6):
  169. timestamp = self.day_ago + timedelta(hours=hour, minutes=30)
  170. self.store_transaction_metric(111, timestamp=timestamp)
  171. self.store_transaction_metric(222, metric="measurements.lcp", timestamp=timestamp)
  172. response = self.do_request(
  173. data={
  174. "start": iso_format(self.day_ago),
  175. "end": iso_format(self.day_ago + timedelta(hours=6)),
  176. "interval": "1h",
  177. "yAxis": ["p75(measurements.lcp)", "p75(transaction.duration)"],
  178. "project": self.project.id,
  179. "dataset": "metricsEnhanced",
  180. },
  181. )
  182. assert response.status_code == 200, response.content
  183. lcp = response.data["p75(measurements.lcp)"]
  184. duration = response.data["p75(transaction.duration)"]
  185. assert len(duration["data"]) == 6
  186. assert duration["isMetricsData"]
  187. assert len(lcp["data"]) == 6
  188. assert lcp["isMetricsData"]
  189. for item in duration["data"]:
  190. assert item[1][0]["count"] == 111
  191. for item in lcp["data"]:
  192. assert item[1][0]["count"] == 222
  193. @mock.patch("sentry.snuba.metrics_enhanced_performance.timeseries_query", return_value={})
  194. def test_multiple_yaxis_only_one_query(self, mock_query):
  195. self.do_request(
  196. data={
  197. "project": self.project.id,
  198. "start": iso_format(self.day_ago),
  199. "end": iso_format(self.day_ago + timedelta(hours=2)),
  200. "interval": "1h",
  201. "yAxis": ["epm()", "eps()", "tpm()", "p50(transaction.duration)"],
  202. "dataset": "metricsEnhanced",
  203. },
  204. )
  205. assert mock_query.call_count == 1
  206. def test_aggregate_function_user_count(self):
  207. self.store_transaction_metric(
  208. 1, metric="user", timestamp=self.day_ago + timedelta(minutes=30)
  209. )
  210. self.store_transaction_metric(
  211. 1, metric="user", timestamp=self.day_ago + timedelta(hours=1, minutes=30)
  212. )
  213. response = self.do_request(
  214. data={
  215. "start": iso_format(self.day_ago),
  216. "end": iso_format(self.day_ago + timedelta(hours=2)),
  217. "interval": "1h",
  218. "yAxis": "count_unique(user)",
  219. "dataset": "metricsEnhanced",
  220. },
  221. )
  222. assert response.status_code == 200, response.content
  223. assert response.data["isMetricsData"]
  224. assert [attrs for time, attrs in response.data["data"]] == [[{"count": 1}], [{"count": 1}]]
  225. meta = response.data["meta"]
  226. assert meta["isMetricsData"] == response.data["isMetricsData"]
  227. def test_non_mep_query_fallsback(self):
  228. def get_mep(query):
  229. response = self.do_request(
  230. data={
  231. "project": self.project.id,
  232. "start": iso_format(self.day_ago),
  233. "end": iso_format(self.day_ago + timedelta(hours=2)),
  234. "interval": "1h",
  235. "query": query,
  236. "yAxis": ["epm()"],
  237. "dataset": "metricsEnhanced",
  238. },
  239. )
  240. assert response.status_code == 200, response.content
  241. return response.data["isMetricsData"]
  242. assert get_mep(""), "empty query"
  243. assert get_mep("event.type:transaction"), "event type transaction"
  244. assert not get_mep("event.type:error"), "event type error"
  245. assert not get_mep("transaction.duration:<15min"), "outlier filter"
  246. assert get_mep("epm():>0.01"), "throughput filter"
  247. assert not get_mep(
  248. "event.type:transaction OR event.type:error"
  249. ), "boolean with non-mep filter"
  250. assert get_mep(
  251. "event.type:transaction OR transaction:foo_transaction"
  252. ), "boolean with mep filter"
  253. def test_having_condition_with_preventing_aggregates(self):
  254. response = self.do_request(
  255. data={
  256. "project": self.project.id,
  257. "start": iso_format(self.day_ago),
  258. "end": iso_format(self.day_ago + timedelta(hours=2)),
  259. "interval": "1h",
  260. "query": "p95():<5s",
  261. "yAxis": ["epm()"],
  262. "dataset": "metricsEnhanced",
  263. "preventMetricAggregates": "1",
  264. },
  265. )
  266. assert response.status_code == 200, response.content
  267. assert not response.data["isMetricsData"]
  268. meta = response.data["meta"]
  269. assert meta["isMetricsData"] == response.data["isMetricsData"]
  270. def test_explicit_not_mep(self):
  271. response = self.do_request(
  272. data={
  273. "project": self.project.id,
  274. "start": iso_format(self.day_ago),
  275. "end": iso_format(self.day_ago + timedelta(hours=2)),
  276. "interval": "1h",
  277. # Should be a mep able query
  278. "query": "",
  279. "yAxis": ["epm()"],
  280. "metricsEnhanced": "0",
  281. },
  282. )
  283. assert response.status_code == 200, response.content
  284. return not response.data["isMetricsData"]
  285. meta = response.data["meta"]
  286. assert meta["isMetricsData"] == response.data["isMetricsData"]
  287. def test_sum_transaction_duration(self):
  288. self.store_transaction_metric(123, timestamp=self.day_ago + timedelta(minutes=30))
  289. self.store_transaction_metric(456, timestamp=self.day_ago + timedelta(hours=1, minutes=30))
  290. self.store_transaction_metric(789, timestamp=self.day_ago + timedelta(hours=1, minutes=30))
  291. response = self.do_request(
  292. data={
  293. "start": iso_format(self.day_ago),
  294. "end": iso_format(self.day_ago + timedelta(hours=2)),
  295. "interval": "1h",
  296. "yAxis": "sum(transaction.duration)",
  297. "dataset": "metricsEnhanced",
  298. },
  299. )
  300. assert response.status_code == 200, response.content
  301. assert response.data["isMetricsData"]
  302. assert [attrs for time, attrs in response.data["data"]] == [
  303. [{"count": 123}],
  304. [{"count": 1245}],
  305. ]
  306. meta = response.data["meta"]
  307. assert meta["isMetricsData"] == response.data["isMetricsData"]
  308. assert meta["fields"] == {"time": "date", "sum_transaction_duration": "duration"}
  309. assert meta["units"] == {"time": None, "sum_transaction_duration": "millisecond"}
  310. def test_custom_measurement(self):
  311. self.store_transaction_metric(
  312. 123,
  313. metric="measurements.bytes_transfered",
  314. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  315. entity="metrics_distributions",
  316. tags={"transaction": "foo_transaction"},
  317. timestamp=self.day_ago + timedelta(minutes=30),
  318. )
  319. self.store_transaction_metric(
  320. 456,
  321. metric="measurements.bytes_transfered",
  322. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  323. entity="metrics_distributions",
  324. tags={"transaction": "foo_transaction"},
  325. timestamp=self.day_ago + timedelta(hours=1, minutes=30),
  326. )
  327. self.store_transaction_metric(
  328. 789,
  329. metric="measurements.bytes_transfered",
  330. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  331. entity="metrics_distributions",
  332. tags={"transaction": "foo_transaction"},
  333. timestamp=self.day_ago + timedelta(hours=1, minutes=30),
  334. )
  335. response = self.do_request(
  336. data={
  337. "start": iso_format(self.day_ago),
  338. "end": iso_format(self.day_ago + timedelta(hours=2)),
  339. "interval": "1h",
  340. "yAxis": "sum(measurements.datacenter_memory)",
  341. "dataset": "metricsEnhanced",
  342. },
  343. )
  344. assert response.status_code == 200, response.content
  345. assert response.data["isMetricsData"]
  346. assert [attrs for time, attrs in response.data["data"]] == [
  347. [{"count": 123}],
  348. [{"count": 1245}],
  349. ]
  350. meta = response.data["meta"]
  351. assert meta["isMetricsData"] == response.data["isMetricsData"]
  352. assert meta["fields"] == {"time": "date", "sum_measurements_datacenter_memory": "size"}
  353. assert meta["units"] == {"time": None, "sum_measurements_datacenter_memory": "pebibyte"}
  354. def test_does_not_fallback_if_custom_metric_is_out_of_request_time_range(self):
  355. self.store_transaction_metric(
  356. 123,
  357. timestamp=self.day_ago + timedelta(hours=1),
  358. internal_metric="d:transactions/measurements.custom@kibibyte",
  359. entity="metrics_distributions",
  360. )
  361. response = self.do_request(
  362. data={
  363. "start": iso_format(self.day_ago),
  364. "end": iso_format(self.day_ago + timedelta(hours=2)),
  365. "interval": "1h",
  366. "yAxis": "p99(measurements.custom)",
  367. "dataset": "metricsEnhanced",
  368. },
  369. )
  370. meta = response.data["meta"]
  371. assert response.status_code == 200, response.content
  372. assert response.data["isMetricsData"]
  373. assert meta["isMetricsData"]
  374. assert meta["fields"] == {"time": "date", "p99_measurements_custom": "size"}
  375. assert meta["units"] == {"time": None, "p99_measurements_custom": "kibibyte"}
  376. def test_multi_yaxis_custom_measurement(self):
  377. self.store_transaction_metric(
  378. 123,
  379. metric="measurements.bytes_transfered",
  380. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  381. entity="metrics_distributions",
  382. tags={"transaction": "foo_transaction"},
  383. timestamp=self.day_ago + timedelta(minutes=30),
  384. )
  385. self.store_transaction_metric(
  386. 456,
  387. metric="measurements.bytes_transfered",
  388. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  389. entity="metrics_distributions",
  390. tags={"transaction": "foo_transaction"},
  391. timestamp=self.day_ago + timedelta(hours=1, minutes=30),
  392. )
  393. self.store_transaction_metric(
  394. 789,
  395. metric="measurements.bytes_transfered",
  396. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  397. entity="metrics_distributions",
  398. tags={"transaction": "foo_transaction"},
  399. timestamp=self.day_ago + timedelta(hours=1, minutes=30),
  400. )
  401. response = self.do_request(
  402. data={
  403. "start": iso_format(self.day_ago),
  404. "end": iso_format(self.day_ago + timedelta(hours=2)),
  405. "interval": "1h",
  406. "yAxis": [
  407. "sum(measurements.datacenter_memory)",
  408. "p50(measurements.datacenter_memory)",
  409. ],
  410. "dataset": "metricsEnhanced",
  411. },
  412. )
  413. assert response.status_code == 200, response.content
  414. sum_data = response.data["sum(measurements.datacenter_memory)"]
  415. p50_data = response.data["p50(measurements.datacenter_memory)"]
  416. assert sum_data["isMetricsData"]
  417. assert p50_data["isMetricsData"]
  418. assert [attrs for time, attrs in sum_data["data"]] == [
  419. [{"count": 123}],
  420. [{"count": 1245}],
  421. ]
  422. assert [attrs for time, attrs in p50_data["data"]] == [
  423. [{"count": 123}],
  424. [{"count": 622.5}],
  425. ]
  426. sum_meta = sum_data["meta"]
  427. assert sum_meta["isMetricsData"] == sum_data["isMetricsData"]
  428. assert sum_meta["fields"] == {
  429. "time": "date",
  430. "sum_measurements_datacenter_memory": "size",
  431. "p50_measurements_datacenter_memory": "size",
  432. }
  433. assert sum_meta["units"] == {
  434. "time": None,
  435. "sum_measurements_datacenter_memory": "pebibyte",
  436. "p50_measurements_datacenter_memory": "pebibyte",
  437. }
  438. p50_meta = p50_data["meta"]
  439. assert p50_meta["isMetricsData"] == p50_data["isMetricsData"]
  440. assert p50_meta["fields"] == {
  441. "time": "date",
  442. "sum_measurements_datacenter_memory": "size",
  443. "p50_measurements_datacenter_memory": "size",
  444. }
  445. assert p50_meta["units"] == {
  446. "time": None,
  447. "sum_measurements_datacenter_memory": "pebibyte",
  448. "p50_measurements_datacenter_memory": "pebibyte",
  449. }
  450. def test_dataset_metrics_does_not_fallback(self):
  451. self.store_transaction_metric(123, timestamp=self.day_ago + timedelta(minutes=30))
  452. self.store_transaction_metric(456, timestamp=self.day_ago + timedelta(hours=1, minutes=30))
  453. self.store_transaction_metric(789, timestamp=self.day_ago + timedelta(hours=1, minutes=30))
  454. response = self.do_request(
  455. data={
  456. "start": iso_format(self.day_ago),
  457. "end": iso_format(self.day_ago + timedelta(hours=2)),
  458. "interval": "1h",
  459. "query": "transaction.duration:<5s",
  460. "yAxis": "sum(transaction.duration)",
  461. "dataset": "metrics",
  462. },
  463. )
  464. assert response.status_code == 400, response.content
  465. def test_title_filter(self):
  466. self.store_transaction_metric(
  467. 123,
  468. tags={"transaction": "foo_transaction"},
  469. timestamp=self.day_ago + timedelta(minutes=30),
  470. )
  471. response = self.do_request(
  472. data={
  473. "start": iso_format(self.day_ago),
  474. "end": iso_format(self.day_ago + timedelta(hours=2)),
  475. "interval": "1h",
  476. "query": "title:foo_transaction",
  477. "yAxis": [
  478. "sum(transaction.duration)",
  479. ],
  480. "dataset": "metricsEnhanced",
  481. },
  482. )
  483. assert response.status_code == 200, response.content
  484. data = response.data["data"]
  485. assert [attrs for time, attrs in data] == [
  486. [{"count": 123}],
  487. [{"count": 0}],
  488. ]
  489. def test_search_query_if_environment_does_not_exist_on_indexer(self):
  490. self.create_environment(self.project, name="prod")
  491. self.create_environment(self.project, name="dev")
  492. self.store_transaction_metric(
  493. 123,
  494. tags={"transaction": "foo_transaction"},
  495. timestamp=self.day_ago + timedelta(minutes=30),
  496. )
  497. response = self.do_request(
  498. data={
  499. "start": iso_format(self.day_ago),
  500. "end": iso_format(self.day_ago + timedelta(hours=2)),
  501. "interval": "1h",
  502. "yAxis": [
  503. "sum(transaction.duration)",
  504. ],
  505. "environment": ["prod", "dev"],
  506. "dataset": "metricsEnhanced",
  507. },
  508. )
  509. assert response.status_code == 200, response.content
  510. data = response.data["data"]
  511. assert [attrs for time, attrs in data] == [
  512. [{"count": 0}],
  513. [{"count": 0}],
  514. ]
  515. assert not response.data["isMetricsData"]