test_organization_events_mep.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220
  1. from unittest import mock
  2. import pytest
  3. from django.urls import reverse
  4. from snuba_sdk.conditions import InvalidConditionError
  5. from sentry.discover.models import TeamKeyTransaction
  6. from sentry.exceptions import IncompatibleMetricsQuery, InvalidSearchQuery
  7. from sentry.models import ProjectTeam
  8. from sentry.search.events import constants
  9. from sentry.testutils import MetricsEnhancedPerformanceTestCase
  10. from sentry.testutils.helpers.datetime import before_now, iso_format
  11. from sentry.utils.samples import load_data
  12. pytestmark = pytest.mark.sentry_metrics
  13. class OrganizationEventsMetricsEnhancedPerformanceEndpointTest(MetricsEnhancedPerformanceTestCase):
  14. viewname = "sentry-api-0-organization-events"
  15. # Poor intentionally omitted for test_measurement_rating_that_does_not_exist
  16. METRIC_STRINGS = [
  17. "foo_transaction",
  18. "bar_transaction",
  19. "baz_transaction",
  20. "staging",
  21. "measurement_rating",
  22. "good",
  23. "meh",
  24. "d:transactions/measurements.something_custom@millisecond",
  25. "d:transactions/measurements.bytes_transfered@byte",
  26. "d:transactions/measurements.datacenter_memory@pebibyte",
  27. ]
  28. def setUp(self):
  29. super().setUp()
  30. self.min_ago = before_now(minutes=1)
  31. self.two_min_ago = before_now(minutes=2)
  32. self.transaction_data = load_data("transaction", timestamp=before_now(minutes=1))
  33. self.features = {
  34. "organizations:performance-use-metrics": True,
  35. }
  36. def do_request(self, query, features=None):
  37. if features is None:
  38. features = {"organizations:discover-basic": True}
  39. features.update(self.features)
  40. self.login_as(user=self.user)
  41. url = reverse(
  42. self.viewname,
  43. kwargs={"organization_slug": self.organization.slug},
  44. )
  45. with self.feature(features):
  46. return self.client.get(url, query, format="json")
  47. def test_no_projects(self):
  48. response = self.do_request(
  49. {
  50. "dataset": "metricsEnhanced",
  51. }
  52. )
  53. assert response.status_code == 200, response.content
  54. def test_invalid_dataset(self):
  55. response = self.do_request(
  56. {
  57. "dataset": "aFakeDataset",
  58. "project": self.project.id,
  59. }
  60. )
  61. assert response.status_code == 400, response.content
  62. assert (
  63. response.data["detail"] == "dataset must be one of: discover, metricsEnhanced, metrics"
  64. )
  65. def test_out_of_retention(self):
  66. self.create_project()
  67. with self.options({"system.event-retention-days": 10}):
  68. query = {
  69. "field": ["id", "timestamp"],
  70. "orderby": ["-timestamp", "-id"],
  71. "query": "event.type:transaction",
  72. "start": iso_format(before_now(days=20)),
  73. "end": iso_format(before_now(days=15)),
  74. "dataset": "metricsEnhanced",
  75. }
  76. response = self.do_request(query)
  77. assert response.status_code == 400, response.content
  78. assert response.data["detail"] == "Invalid date range. Please try a more recent date range."
  79. def test_invalid_search_terms(self):
  80. response = self.do_request(
  81. {
  82. "field": ["epm()"],
  83. "query": "hi \n there",
  84. "project": self.project.id,
  85. "dataset": "metricsEnhanced",
  86. }
  87. )
  88. assert response.status_code == 400, response.content
  89. assert (
  90. response.data["detail"]
  91. == "Parse error at 'hi \n ther' (column 4). This is commonly caused by unmatched parentheses. Enclose any text in double quotes."
  92. )
  93. def test_percentile_with_no_data(self):
  94. response = self.do_request(
  95. {
  96. "field": ["p50()"],
  97. "query": "",
  98. "project": self.project.id,
  99. "dataset": "metricsEnhanced",
  100. }
  101. )
  102. assert response.status_code == 200, response.content
  103. data = response.data["data"]
  104. assert len(data) == 1
  105. assert data[0]["p50()"] == 0
  106. def test_project_name(self):
  107. self.store_transaction_metric(
  108. 1,
  109. tags={"environment": "staging"},
  110. timestamp=self.min_ago,
  111. )
  112. response = self.do_request(
  113. {
  114. "field": ["project.name", "environment", "epm()"],
  115. "query": "event.type:transaction",
  116. "dataset": "metricsEnhanced",
  117. "per_page": 50,
  118. }
  119. )
  120. assert response.status_code == 200, response.content
  121. assert len(response.data["data"]) == 1
  122. data = response.data["data"]
  123. meta = response.data["meta"]
  124. field_meta = meta["fields"]
  125. assert data[0]["project.name"] == self.project.slug
  126. assert "project.id" not in data[0]
  127. assert data[0]["environment"] == "staging"
  128. assert meta["isMetricsData"]
  129. assert field_meta["project.name"] == "string"
  130. assert field_meta["environment"] == "string"
  131. assert field_meta["epm()"] == "number"
  132. def test_title_alias(self):
  133. """title is an alias to transaction name"""
  134. self.store_transaction_metric(
  135. 1,
  136. tags={"transaction": "foo_transaction"},
  137. timestamp=self.min_ago,
  138. )
  139. response = self.do_request(
  140. {
  141. "field": ["title", "p50()"],
  142. "query": "event.type:transaction",
  143. "dataset": "metricsEnhanced",
  144. "per_page": 50,
  145. }
  146. )
  147. assert response.status_code == 200, response.content
  148. assert len(response.data["data"]) == 1
  149. data = response.data["data"]
  150. meta = response.data["meta"]
  151. field_meta = meta["fields"]
  152. assert data[0]["title"] == "foo_transaction"
  153. assert data[0]["p50()"] == 1
  154. assert meta["isMetricsData"]
  155. assert field_meta["title"] == "string"
  156. assert field_meta["p50()"] == "duration"
  157. def test_having_condition(self):
  158. self.store_transaction_metric(
  159. 1,
  160. tags={"environment": "staging", "transaction": "foo_transaction"},
  161. timestamp=self.min_ago,
  162. )
  163. self.store_transaction_metric(
  164. # shouldn't show up
  165. 100,
  166. tags={"environment": "staging", "transaction": "bar_transaction"},
  167. timestamp=self.min_ago,
  168. )
  169. response = self.do_request(
  170. {
  171. "field": ["transaction", "project", "p50(transaction.duration)"],
  172. "query": "event.type:transaction p50(transaction.duration):<50",
  173. "dataset": "metricsEnhanced",
  174. "per_page": 50,
  175. }
  176. )
  177. assert response.status_code == 200, response.content
  178. assert len(response.data["data"]) == 1
  179. data = response.data["data"]
  180. meta = response.data["meta"]
  181. field_meta = meta["fields"]
  182. assert data[0]["transaction"] == "foo_transaction"
  183. assert data[0]["project"] == self.project.slug
  184. assert data[0]["p50(transaction.duration)"] == 1
  185. assert meta["isMetricsData"]
  186. assert field_meta["transaction"] == "string"
  187. assert field_meta["project"] == "string"
  188. assert field_meta["p50(transaction.duration)"] == "duration"
  189. def test_having_condition_with_preventing_aggregates(self):
  190. self.store_transaction_metric(
  191. 1,
  192. tags={"environment": "staging", "transaction": "foo_transaction"},
  193. timestamp=self.min_ago,
  194. )
  195. self.store_transaction_metric(
  196. 100,
  197. tags={"environment": "staging", "transaction": "bar_transaction"},
  198. timestamp=self.min_ago,
  199. )
  200. response = self.do_request(
  201. {
  202. "field": ["transaction", "project", "p50(transaction.duration)"],
  203. "query": "event.type:transaction p50(transaction.duration):<50",
  204. "dataset": "metricsEnhanced",
  205. "preventMetricAggregates": "1",
  206. "per_page": 50,
  207. }
  208. )
  209. assert response.status_code == 200, response.content
  210. assert len(response.data["data"]) == 0
  211. meta = response.data["meta"]
  212. field_meta = meta["fields"]
  213. assert not meta["isMetricsData"]
  214. assert field_meta["transaction"] == "string"
  215. assert field_meta["project"] == "string"
  216. assert field_meta["p50(transaction.duration)"] == "duration"
  217. def test_having_condition_with_preventing_aggregate_metrics_only(self):
  218. """same as the previous test, but with the dataset on explicit metrics
  219. which should throw a 400 error instead"""
  220. response = self.do_request(
  221. {
  222. "field": ["transaction", "project", "p50(transaction.duration)"],
  223. "query": "event.type:transaction p50(transaction.duration):<50",
  224. "dataset": "metrics",
  225. "preventMetricAggregates": "1",
  226. "per_page": 50,
  227. "project": self.project.id,
  228. }
  229. )
  230. assert response.status_code == 400, response.content
  231. def test_having_condition_not_selected(self):
  232. self.store_transaction_metric(
  233. 1,
  234. tags={"environment": "staging", "transaction": "foo_transaction"},
  235. timestamp=self.min_ago,
  236. )
  237. self.store_transaction_metric(
  238. # shouldn't show up
  239. 100,
  240. tags={"environment": "staging", "transaction": "bar_transaction"},
  241. timestamp=self.min_ago,
  242. )
  243. response = self.do_request(
  244. {
  245. "field": ["transaction", "project", "p50(transaction.duration)"],
  246. "query": "event.type:transaction p75(transaction.duration):<50",
  247. "dataset": "metricsEnhanced",
  248. "per_page": 50,
  249. }
  250. )
  251. assert response.status_code == 200, response.content
  252. assert len(response.data["data"]) == 1
  253. data = response.data["data"]
  254. meta = response.data["meta"]
  255. field_meta = meta["fields"]
  256. assert data[0]["transaction"] == "foo_transaction"
  257. assert data[0]["project"] == self.project.slug
  258. assert data[0]["p50(transaction.duration)"] == 1
  259. assert meta["isMetricsData"]
  260. assert field_meta["transaction"] == "string"
  261. assert field_meta["project"] == "string"
  262. assert field_meta["p50(transaction.duration)"] == "duration"
  263. def test_non_metrics_tag_with_implicit_format(self):
  264. self.store_transaction_metric(
  265. 1,
  266. tags={"environment": "staging", "transaction": "foo_transaction"},
  267. timestamp=self.min_ago,
  268. )
  269. response = self.do_request(
  270. {
  271. "field": ["test", "p50(transaction.duration)"],
  272. "query": "event.type:transaction",
  273. "dataset": "metricsEnhanced",
  274. "per_page": 50,
  275. }
  276. )
  277. assert response.status_code == 200, response.content
  278. assert len(response.data["data"]) == 0
  279. assert not response.data["meta"]["isMetricsData"]
  280. def test_non_metrics_tag_with_implicit_format_metrics_dataset(self):
  281. self.store_transaction_metric(
  282. 1,
  283. tags={"environment": "staging", "transaction": "foo_transaction"},
  284. timestamp=self.min_ago,
  285. )
  286. response = self.do_request(
  287. {
  288. "field": ["test", "p50(transaction.duration)"],
  289. "query": "event.type:transaction",
  290. "dataset": "metrics",
  291. "per_page": 50,
  292. }
  293. )
  294. assert response.status_code == 400, response.content
  295. def test_performance_homepage_query(self):
  296. self.store_transaction_metric(
  297. 1,
  298. tags={
  299. "transaction": "foo_transaction",
  300. constants.METRIC_SATISFACTION_TAG_KEY: constants.METRIC_SATISFIED_TAG_VALUE,
  301. },
  302. timestamp=self.min_ago,
  303. )
  304. self.store_transaction_metric(
  305. 1,
  306. "measurements.fcp",
  307. tags={"transaction": "foo_transaction"},
  308. timestamp=self.min_ago,
  309. )
  310. self.store_transaction_metric(
  311. 2,
  312. "measurements.lcp",
  313. tags={"transaction": "foo_transaction"},
  314. timestamp=self.min_ago,
  315. )
  316. self.store_transaction_metric(
  317. 3,
  318. "measurements.fid",
  319. tags={"transaction": "foo_transaction"},
  320. timestamp=self.min_ago,
  321. )
  322. self.store_transaction_metric(
  323. 4,
  324. "measurements.cls",
  325. tags={"transaction": "foo_transaction"},
  326. timestamp=self.min_ago,
  327. )
  328. self.store_transaction_metric(
  329. 1,
  330. "user",
  331. tags={
  332. "transaction": "foo_transaction",
  333. constants.METRIC_SATISFACTION_TAG_KEY: constants.METRIC_FRUSTRATED_TAG_VALUE,
  334. },
  335. timestamp=self.min_ago,
  336. )
  337. for dataset in ["metrics", "metricsEnhanced"]:
  338. response = self.do_request(
  339. {
  340. "field": [
  341. "transaction",
  342. "project",
  343. "tpm()",
  344. "p75(measurements.fcp)",
  345. "p75(measurements.lcp)",
  346. "p75(measurements.fid)",
  347. "p75(measurements.cls)",
  348. "count_unique(user)",
  349. "apdex()",
  350. "count_miserable(user)",
  351. "user_misery()",
  352. ],
  353. "query": "event.type:transaction",
  354. "dataset": dataset,
  355. "per_page": 50,
  356. }
  357. )
  358. assert len(response.data["data"]) == 1
  359. data = response.data["data"][0]
  360. meta = response.data["meta"]
  361. field_meta = meta["fields"]
  362. assert data["transaction"] == "foo_transaction"
  363. assert data["project"] == self.project.slug
  364. assert data["p75(measurements.fcp)"] == 1.0
  365. assert data["p75(measurements.lcp)"] == 2.0
  366. assert data["p75(measurements.fid)"] == 3.0
  367. assert data["p75(measurements.cls)"] == 4.0
  368. assert data["apdex()"] == 1.0
  369. assert data["count_miserable(user)"] == 1.0
  370. assert data["user_misery()"] == 0.058
  371. assert meta["isMetricsData"]
  372. assert field_meta["transaction"] == "string"
  373. assert field_meta["project"] == "string"
  374. assert field_meta["p75(measurements.fcp)"] == "duration"
  375. assert field_meta["p75(measurements.lcp)"] == "duration"
  376. assert field_meta["p75(measurements.fid)"] == "duration"
  377. assert field_meta["p75(measurements.cls)"] == "duration"
  378. assert field_meta["apdex()"] == "number"
  379. assert field_meta["count_miserable(user)"] == "integer"
  380. assert field_meta["user_misery()"] == "number"
  381. def test_no_team_key_transactions(self):
  382. self.store_transaction_metric(
  383. 1, tags={"transaction": "foo_transaction"}, timestamp=self.min_ago
  384. )
  385. self.store_transaction_metric(
  386. 100, tags={"transaction": "bar_transaction"}, timestamp=self.min_ago
  387. )
  388. query = {
  389. "team": "myteams",
  390. "project": [self.project.id],
  391. # TODO sort by transaction here once that's possible for order to match the same test without metrics
  392. "orderby": "p95()",
  393. "field": [
  394. "team_key_transaction",
  395. "transaction",
  396. "transaction.status",
  397. "project",
  398. "epm()",
  399. "failure_rate()",
  400. "p95()",
  401. ],
  402. "per_page": 50,
  403. "dataset": "metricsEnhanced",
  404. }
  405. response = self.do_request(query)
  406. assert response.status_code == 200, response.content
  407. assert len(response.data["data"]) == 2
  408. data = response.data["data"]
  409. meta = response.data["meta"]
  410. field_meta = meta["fields"]
  411. assert data[0]["team_key_transaction"] == 0
  412. assert data[0]["transaction"] == "foo_transaction"
  413. assert data[1]["team_key_transaction"] == 0
  414. assert data[1]["transaction"] == "bar_transaction"
  415. assert meta["isMetricsData"]
  416. assert field_meta["team_key_transaction"] == "boolean"
  417. assert field_meta["transaction"] == "string"
  418. def test_team_key_transactions_my_teams(self):
  419. team1 = self.create_team(organization=self.organization, name="Team A")
  420. self.create_team_membership(team1, user=self.user)
  421. self.project.add_team(team1)
  422. team2 = self.create_team(organization=self.organization, name="Team B")
  423. self.project.add_team(team2)
  424. key_transactions = [
  425. (team1, "foo_transaction"),
  426. (team2, "baz_transaction"),
  427. ]
  428. # Not a key transaction
  429. self.store_transaction_metric(
  430. 100, tags={"transaction": "bar_transaction"}, timestamp=self.min_ago
  431. )
  432. for team, transaction in key_transactions:
  433. self.store_transaction_metric(
  434. 1, tags={"transaction": transaction}, timestamp=self.min_ago
  435. )
  436. TeamKeyTransaction.objects.create(
  437. organization=self.organization,
  438. transaction=transaction,
  439. project_team=ProjectTeam.objects.get(project=self.project, team=team),
  440. )
  441. query = {
  442. "team": "myteams",
  443. "project": [self.project.id],
  444. "field": [
  445. "team_key_transaction",
  446. "transaction",
  447. "transaction.status",
  448. "project",
  449. "epm()",
  450. "failure_rate()",
  451. "p95()",
  452. ],
  453. "per_page": 50,
  454. "dataset": "metricsEnhanced",
  455. }
  456. query["orderby"] = ["team_key_transaction", "p95()"]
  457. response = self.do_request(query)
  458. assert response.status_code == 200, response.content
  459. assert len(response.data["data"]) == 3
  460. data = response.data["data"]
  461. meta = response.data["meta"]
  462. field_meta = meta["fields"]
  463. assert data[0]["team_key_transaction"] == 0
  464. assert data[0]["transaction"] == "baz_transaction"
  465. assert data[1]["team_key_transaction"] == 0
  466. assert data[1]["transaction"] == "bar_transaction"
  467. assert data[2]["team_key_transaction"] == 1
  468. assert data[2]["transaction"] == "foo_transaction"
  469. assert meta["isMetricsData"]
  470. assert field_meta["team_key_transaction"] == "boolean"
  471. assert field_meta["transaction"] == "string"
  472. # not specifying any teams should use my teams
  473. query = {
  474. "project": [self.project.id],
  475. "field": [
  476. "team_key_transaction",
  477. "transaction",
  478. "transaction.status",
  479. "project",
  480. "epm()",
  481. "failure_rate()",
  482. "p95()",
  483. ],
  484. "per_page": 50,
  485. "dataset": "metricsEnhanced",
  486. }
  487. query["orderby"] = ["team_key_transaction", "p95()"]
  488. response = self.do_request(query)
  489. assert response.status_code == 200, response.content
  490. assert len(response.data["data"]) == 3
  491. data = response.data["data"]
  492. meta = response.data["meta"]
  493. field_meta = meta["fields"]
  494. assert data[0]["team_key_transaction"] == 0
  495. assert data[0]["transaction"] == "baz_transaction"
  496. assert data[1]["team_key_transaction"] == 0
  497. assert data[1]["transaction"] == "bar_transaction"
  498. assert data[2]["team_key_transaction"] == 1
  499. assert data[2]["transaction"] == "foo_transaction"
  500. assert meta["isMetricsData"]
  501. assert field_meta["team_key_transaction"] == "boolean"
  502. assert field_meta["transaction"] == "string"
  503. def test_team_key_transactions_orderby(self):
  504. team1 = self.create_team(organization=self.organization, name="Team A")
  505. team2 = self.create_team(organization=self.organization, name="Team B")
  506. key_transactions = [
  507. (team1, "foo_transaction", 1),
  508. (team2, "baz_transaction", 100),
  509. ]
  510. # Not a key transaction
  511. self.store_transaction_metric(
  512. 100, tags={"transaction": "bar_transaction"}, timestamp=self.min_ago
  513. )
  514. for team, transaction, value in key_transactions:
  515. self.store_transaction_metric(
  516. value, tags={"transaction": transaction}, timestamp=self.min_ago
  517. )
  518. self.create_team_membership(team, user=self.user)
  519. self.project.add_team(team)
  520. TeamKeyTransaction.objects.create(
  521. organization=self.organization,
  522. transaction=transaction,
  523. project_team=ProjectTeam.objects.get(project=self.project, team=team),
  524. )
  525. query = {
  526. "team": "myteams",
  527. "project": [self.project.id],
  528. "field": [
  529. "team_key_transaction",
  530. "transaction",
  531. "transaction.status",
  532. "project",
  533. "epm()",
  534. "failure_rate()",
  535. "p95()",
  536. ],
  537. "per_page": 50,
  538. "dataset": "metricsEnhanced",
  539. }
  540. # test ascending order
  541. query["orderby"] = ["team_key_transaction", "p95()"]
  542. response = self.do_request(query)
  543. assert response.status_code == 200, response.content
  544. assert len(response.data["data"]) == 3
  545. data = response.data["data"]
  546. meta = response.data["meta"]
  547. field_meta = meta["fields"]
  548. assert data[0]["team_key_transaction"] == 0
  549. assert data[0]["transaction"] == "bar_transaction"
  550. assert data[1]["team_key_transaction"] == 1
  551. assert data[1]["transaction"] == "foo_transaction"
  552. assert data[2]["team_key_transaction"] == 1
  553. assert data[2]["transaction"] == "baz_transaction"
  554. assert meta["isMetricsData"]
  555. assert field_meta["team_key_transaction"] == "boolean"
  556. assert field_meta["transaction"] == "string"
  557. # test descending order
  558. query["orderby"] = ["-team_key_transaction", "p95()"]
  559. response = self.do_request(query)
  560. assert response.status_code == 200, response.content
  561. assert len(response.data["data"]) == 3
  562. data = response.data["data"]
  563. meta = response.data["meta"]
  564. field_meta = meta["fields"]
  565. assert data[0]["team_key_transaction"] == 1
  566. assert data[0]["transaction"] == "foo_transaction"
  567. assert data[1]["team_key_transaction"] == 1
  568. assert data[1]["transaction"] == "baz_transaction"
  569. assert data[2]["team_key_transaction"] == 0
  570. assert data[2]["transaction"] == "bar_transaction"
  571. assert meta["isMetricsData"]
  572. assert field_meta["team_key_transaction"] == "boolean"
  573. assert field_meta["transaction"] == "string"
  574. def test_team_key_transactions_query(self):
  575. team1 = self.create_team(organization=self.organization, name="Team A")
  576. team2 = self.create_team(organization=self.organization, name="Team B")
  577. key_transactions = [
  578. (team1, "foo_transaction", 1),
  579. (team2, "baz_transaction", 100),
  580. ]
  581. # Not a key transaction
  582. self.store_transaction_metric(
  583. 100, tags={"transaction": "bar_transaction"}, timestamp=self.min_ago
  584. )
  585. for team, transaction, value in key_transactions:
  586. self.store_transaction_metric(
  587. value, tags={"transaction": transaction}, timestamp=self.min_ago
  588. )
  589. self.create_team_membership(team, user=self.user)
  590. self.project.add_team(team)
  591. TeamKeyTransaction.objects.create(
  592. organization=self.organization,
  593. transaction=transaction,
  594. project_team=ProjectTeam.objects.get(project=self.project, team=team),
  595. )
  596. query = {
  597. "team": "myteams",
  598. "project": [self.project.id],
  599. # use the order by to ensure the result order
  600. "orderby": "p95()",
  601. "field": [
  602. "team_key_transaction",
  603. "transaction",
  604. "transaction.status",
  605. "project",
  606. "epm()",
  607. "failure_rate()",
  608. "p95()",
  609. ],
  610. "per_page": 50,
  611. "dataset": "metricsEnhanced",
  612. }
  613. # key transactions
  614. query["query"] = "has:team_key_transaction"
  615. response = self.do_request(query)
  616. assert response.status_code == 200, response.content
  617. assert len(response.data["data"]) == 2
  618. data = response.data["data"]
  619. meta = response.data["meta"]
  620. field_meta = meta["fields"]
  621. assert data[0]["team_key_transaction"] == 1
  622. assert data[0]["transaction"] == "foo_transaction"
  623. assert data[1]["team_key_transaction"] == 1
  624. assert data[1]["transaction"] == "baz_transaction"
  625. assert meta["isMetricsData"]
  626. assert field_meta["team_key_transaction"] == "boolean"
  627. assert field_meta["transaction"] == "string"
  628. # key transactions
  629. query["query"] = "team_key_transaction:true"
  630. response = self.do_request(query)
  631. assert response.status_code == 200, response.content
  632. assert len(response.data["data"]) == 2
  633. data = response.data["data"]
  634. meta = response.data["meta"]
  635. field_meta = meta["fields"]
  636. assert data[0]["team_key_transaction"] == 1
  637. assert data[0]["transaction"] == "foo_transaction"
  638. assert data[1]["team_key_transaction"] == 1
  639. assert data[1]["transaction"] == "baz_transaction"
  640. assert meta["isMetricsData"]
  641. assert field_meta["team_key_transaction"] == "boolean"
  642. assert field_meta["transaction"] == "string"
  643. # not key transactions
  644. query["query"] = "!has:team_key_transaction"
  645. response = self.do_request(query)
  646. assert response.status_code == 200, response.content
  647. assert len(response.data["data"]) == 1
  648. data = response.data["data"]
  649. meta = response.data["meta"]
  650. field_meta = meta["fields"]
  651. assert data[0]["team_key_transaction"] == 0
  652. assert data[0]["transaction"] == "bar_transaction"
  653. assert meta["isMetricsData"]
  654. assert field_meta["team_key_transaction"] == "boolean"
  655. assert field_meta["transaction"] == "string"
  656. # not key transactions
  657. query["query"] = "team_key_transaction:false"
  658. response = self.do_request(query)
  659. assert response.status_code == 200, response.content
  660. assert len(response.data["data"]) == 1
  661. data = response.data["data"]
  662. meta = response.data["meta"]
  663. field_meta = meta["fields"]
  664. assert data[0]["team_key_transaction"] == 0
  665. assert data[0]["transaction"] == "bar_transaction"
  666. assert meta["isMetricsData"]
  667. assert field_meta["team_key_transaction"] == "boolean"
  668. assert field_meta["transaction"] == "string"
  669. def test_too_many_team_key_transactions(self):
  670. MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS = 1
  671. with mock.patch(
  672. "sentry.search.events.fields.MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS",
  673. MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS,
  674. ):
  675. team = self.create_team(organization=self.organization, name="Team A")
  676. self.create_team_membership(team, user=self.user)
  677. self.project.add_team(team)
  678. project_team = ProjectTeam.objects.get(project=self.project, team=team)
  679. transactions = ["foo_transaction", "bar_transaction", "baz_transaction"]
  680. for i in range(MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS + 1):
  681. self.store_transaction_metric(
  682. 100, tags={"transaction": transactions[i]}, timestamp=self.min_ago
  683. )
  684. TeamKeyTransaction.objects.bulk_create(
  685. [
  686. TeamKeyTransaction(
  687. organization=self.organization,
  688. project_team=project_team,
  689. transaction=transactions[i],
  690. )
  691. for i in range(MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS + 1)
  692. ]
  693. )
  694. query = {
  695. "team": "myteams",
  696. "project": [self.project.id],
  697. "orderby": "p95()",
  698. "field": [
  699. "team_key_transaction",
  700. "transaction",
  701. "transaction.status",
  702. "project",
  703. "epm()",
  704. "failure_rate()",
  705. "p95()",
  706. ],
  707. "dataset": "metricsEnhanced",
  708. "per_page": 50,
  709. }
  710. response = self.do_request(query)
  711. assert response.status_code == 200, response.content
  712. assert len(response.data["data"]) == 2
  713. data = response.data["data"]
  714. meta = response.data["meta"]
  715. assert (
  716. sum(row["team_key_transaction"] for row in data)
  717. == MAX_QUERYABLE_TEAM_KEY_TRANSACTIONS
  718. )
  719. assert meta["isMetricsData"]
  720. def test_measurement_rating(self):
  721. self.store_transaction_metric(
  722. 50,
  723. metric="measurements.lcp",
  724. tags={"measurement_rating": "good", "transaction": "foo_transaction"},
  725. timestamp=self.min_ago,
  726. )
  727. self.store_transaction_metric(
  728. 15,
  729. metric="measurements.fp",
  730. tags={"measurement_rating": "good", "transaction": "foo_transaction"},
  731. timestamp=self.min_ago,
  732. )
  733. self.store_transaction_metric(
  734. 1500,
  735. metric="measurements.fcp",
  736. tags={"measurement_rating": "meh", "transaction": "foo_transaction"},
  737. timestamp=self.min_ago,
  738. )
  739. self.store_transaction_metric(
  740. 125,
  741. metric="measurements.fid",
  742. tags={"measurement_rating": "meh", "transaction": "foo_transaction"},
  743. timestamp=self.min_ago,
  744. )
  745. self.store_transaction_metric(
  746. 0.15,
  747. metric="measurements.cls",
  748. tags={"measurement_rating": "good", "transaction": "foo_transaction"},
  749. timestamp=self.min_ago,
  750. )
  751. response = self.do_request(
  752. {
  753. "field": [
  754. "transaction",
  755. "count_web_vitals(measurements.lcp, good)",
  756. "count_web_vitals(measurements.fp, good)",
  757. "count_web_vitals(measurements.fcp, meh)",
  758. "count_web_vitals(measurements.fid, meh)",
  759. "count_web_vitals(measurements.cls, good)",
  760. ],
  761. "query": "event.type:transaction",
  762. "dataset": "metricsEnhanced",
  763. "per_page": 50,
  764. }
  765. )
  766. assert response.status_code == 200, response.content
  767. assert len(response.data["data"]) == 1
  768. data = response.data["data"]
  769. meta = response.data["meta"]
  770. field_meta = meta["fields"]
  771. assert data[0]["count_web_vitals(measurements.lcp, good)"] == 1
  772. assert data[0]["count_web_vitals(measurements.fp, good)"] == 1
  773. assert data[0]["count_web_vitals(measurements.fcp, meh)"] == 1
  774. assert data[0]["count_web_vitals(measurements.fid, meh)"] == 1
  775. assert data[0]["count_web_vitals(measurements.cls, good)"] == 1
  776. assert meta["isMetricsData"]
  777. assert field_meta["count_web_vitals(measurements.lcp, good)"] == "integer"
  778. assert field_meta["count_web_vitals(measurements.fp, good)"] == "integer"
  779. assert field_meta["count_web_vitals(measurements.fcp, meh)"] == "integer"
  780. assert field_meta["count_web_vitals(measurements.fid, meh)"] == "integer"
  781. assert field_meta["count_web_vitals(measurements.cls, good)"] == "integer"
  782. def test_measurement_rating_that_does_not_exist(self):
  783. self.store_transaction_metric(
  784. 1,
  785. metric="measurements.lcp",
  786. tags={"measurement_rating": "good", "transaction": "foo_transaction"},
  787. timestamp=self.min_ago,
  788. )
  789. response = self.do_request(
  790. {
  791. "field": ["transaction", "count_web_vitals(measurements.lcp, poor)"],
  792. "query": "event.type:transaction",
  793. "dataset": "metricsEnhanced",
  794. "per_page": 50,
  795. }
  796. )
  797. assert response.status_code == 200, response.content
  798. assert len(response.data["data"]) == 1
  799. data = response.data["data"]
  800. meta = response.data["meta"]
  801. assert data[0]["count_web_vitals(measurements.lcp, poor)"] == 0
  802. assert meta["isMetricsData"]
  803. assert meta["fields"]["count_web_vitals(measurements.lcp, poor)"] == "integer"
  804. def test_count_web_vitals_invalid_vital(self):
  805. query = {
  806. "field": [
  807. "count_web_vitals(measurements.foo, poor)",
  808. ],
  809. "project": [self.project.id],
  810. "dataset": "metricsEnhanced",
  811. }
  812. response = self.do_request(query)
  813. assert response.status_code == 400, response.content
  814. query = {
  815. "field": [
  816. "count_web_vitals(tags[lcp], poor)",
  817. ],
  818. "project": [self.project.id],
  819. "dataset": "metricsEnhanced",
  820. }
  821. response = self.do_request(query)
  822. assert response.status_code == 400, response.content
  823. query = {
  824. "field": [
  825. "count_web_vitals(transaction.duration, poor)",
  826. ],
  827. "project": [self.project.id],
  828. "dataset": "metricsEnhanced",
  829. }
  830. response = self.do_request(query)
  831. assert response.status_code == 400, response.content
  832. query = {
  833. "field": [
  834. "count_web_vitals(measurements.lcp, bad)",
  835. ],
  836. "project": [self.project.id],
  837. "dataset": "metricsEnhanced",
  838. }
  839. response = self.do_request(query)
  840. assert response.status_code == 400, response.content
  841. @mock.patch("sentry.snuba.metrics_performance.MetricsQueryBuilder")
  842. def test_failed_dry_run_does_not_error(self, mock_builder):
  843. with self.feature("organizations:performance-dry-run-mep"):
  844. mock_builder.side_effect = InvalidSearchQuery("Something bad")
  845. query = {
  846. "field": ["count()"],
  847. "project": [self.project.id],
  848. }
  849. response = self.do_request(query)
  850. assert response.status_code == 200, response.content
  851. assert len(mock_builder.mock_calls) == 1
  852. assert mock_builder.call_args.kwargs["dry_run"]
  853. mock_builder.side_effect = IncompatibleMetricsQuery("Something bad")
  854. query = {
  855. "field": ["count()"],
  856. "project": [self.project.id],
  857. }
  858. response = self.do_request(query)
  859. assert response.status_code == 200, response.content
  860. assert len(mock_builder.mock_calls) == 2
  861. assert mock_builder.call_args.kwargs["dry_run"]
  862. mock_builder.side_effect = InvalidConditionError("Something bad")
  863. query = {
  864. "field": ["count()"],
  865. "project": [self.project.id],
  866. }
  867. response = self.do_request(query)
  868. assert response.status_code == 200, response.content
  869. assert len(mock_builder.mock_calls) == 3
  870. assert mock_builder.call_args.kwargs["dry_run"]
  871. def test_count_unique_user_returns_zero(self):
  872. self.store_transaction_metric(
  873. 50,
  874. metric="user",
  875. tags={"transaction": "foo_transaction"},
  876. timestamp=self.min_ago,
  877. )
  878. self.store_transaction_metric(
  879. 50,
  880. tags={"transaction": "foo_transaction"},
  881. timestamp=self.min_ago,
  882. )
  883. self.store_transaction_metric(
  884. 100,
  885. tags={"transaction": "bar_transaction"},
  886. timestamp=self.min_ago,
  887. )
  888. query = {
  889. "project": [self.project.id],
  890. "orderby": "p50()",
  891. "field": [
  892. "transaction",
  893. "count_unique(user)",
  894. "p50()",
  895. ],
  896. "dataset": "metricsEnhanced",
  897. "per_page": 50,
  898. }
  899. response = self.do_request(query)
  900. assert response.status_code == 200, response.content
  901. assert len(response.data["data"]) == 2
  902. data = response.data["data"]
  903. meta = response.data["meta"]
  904. assert data[0]["transaction"] == "foo_transaction"
  905. assert data[0]["count_unique(user)"] == 1
  906. assert data[1]["transaction"] == "bar_transaction"
  907. assert data[1]["count_unique(user)"] == 0
  908. assert meta["isMetricsData"]
  909. def test_sum_transaction_duration(self):
  910. self.store_transaction_metric(
  911. 50,
  912. tags={"transaction": "foo_transaction"},
  913. timestamp=self.min_ago,
  914. )
  915. self.store_transaction_metric(
  916. 100,
  917. tags={"transaction": "foo_transaction"},
  918. timestamp=self.min_ago,
  919. )
  920. self.store_transaction_metric(
  921. 150,
  922. tags={"transaction": "foo_transaction"},
  923. timestamp=self.min_ago,
  924. )
  925. query = {
  926. "project": [self.project.id],
  927. "orderby": "sum(transaction.duration)",
  928. "field": [
  929. "transaction",
  930. "sum(transaction.duration)",
  931. ],
  932. "dataset": "metricsEnhanced",
  933. "per_page": 50,
  934. }
  935. response = self.do_request(query)
  936. assert response.status_code == 200, response.content
  937. assert len(response.data["data"]) == 1
  938. data = response.data["data"]
  939. meta = response.data["meta"]
  940. assert data[0]["transaction"] == "foo_transaction"
  941. assert data[0]["sum(transaction.duration)"] == 300
  942. assert meta["isMetricsData"]
  943. def test_custom_measurements_simple(self):
  944. self.store_transaction_metric(
  945. 1,
  946. metric="measurements.something_custom",
  947. internal_metric="d:transactions/measurements.something_custom@millisecond",
  948. entity="metrics_distributions",
  949. tags={"transaction": "foo_transaction"},
  950. timestamp=self.min_ago,
  951. )
  952. query = {
  953. "project": [self.project.id],
  954. "orderby": "p50(measurements.something_custom)",
  955. "field": [
  956. "transaction",
  957. "p50(measurements.something_custom)",
  958. ],
  959. "statsPeriod": "24h",
  960. "dataset": "metricsEnhanced",
  961. "per_page": 50,
  962. }
  963. response = self.do_request(query)
  964. assert response.status_code == 200, response.content
  965. assert len(response.data["data"]) == 1
  966. data = response.data["data"]
  967. meta = response.data["meta"]
  968. assert data[0]["transaction"] == "foo_transaction"
  969. assert data[0]["p50(measurements.something_custom)"] == 1
  970. assert meta["isMetricsData"]
  971. assert meta["fields"]["p50(measurements.something_custom)"] == "duration"
  972. assert meta["units"]["p50(measurements.something_custom)"] == "millisecond"
  973. def test_custom_measurement_size_meta_type(self):
  974. self.store_transaction_metric(
  975. 1,
  976. metric="measurements.bytes_transfered",
  977. internal_metric="d:transactions/measurements.bytes_transfered@byte",
  978. entity="metrics_distributions",
  979. tags={"transaction": "foo_transaction"},
  980. timestamp=self.min_ago,
  981. )
  982. self.store_transaction_metric(
  983. 100,
  984. metric="measurements.bytes_transfered",
  985. internal_metric="d:transactions/measurements.datacenter_memory@pebibyte",
  986. entity="metrics_distributions",
  987. tags={"transaction": "foo_transaction"},
  988. timestamp=self.min_ago,
  989. )
  990. query = {
  991. "project": [self.project.id],
  992. "orderby": "p50(measurements.bytes_transfered)",
  993. "field": [
  994. "transaction",
  995. "p50(measurements.bytes_transfered)",
  996. "p99(measurements.bytes_transfered)",
  997. "max(measurements.datacenter_memory)",
  998. ],
  999. "statsPeriod": "24h",
  1000. "dataset": "metricsEnhanced",
  1001. "per_page": 50,
  1002. }
  1003. response = self.do_request(query)
  1004. assert response.status_code == 200, response.content
  1005. assert len(response.data["data"]) == 1
  1006. data = response.data["data"]
  1007. meta = response.data["meta"]
  1008. assert data[0]["transaction"] == "foo_transaction"
  1009. assert data[0]["p50(measurements.bytes_transfered)"] == 1
  1010. assert data[0]["max(measurements.datacenter_memory)"] == 100
  1011. assert meta["isMetricsData"]
  1012. assert meta["fields"]["p50(measurements.bytes_transfered)"] == "size"
  1013. assert meta["units"]["p50(measurements.bytes_transfered)"] == "byte"
  1014. assert meta["fields"]["p99(measurements.bytes_transfered)"] == "size"
  1015. assert meta["units"]["p99(measurements.bytes_transfered)"] == "byte"
  1016. assert meta["fields"]["max(measurements.datacenter_memory)"] == "size"
  1017. assert meta["units"]["max(measurements.datacenter_memory)"] == "pebibyte"
  1018. def test_environment_param(self):
  1019. self.create_environment(self.project, name="staging")
  1020. self.store_transaction_metric(
  1021. 1,
  1022. tags={"transaction": "foo_transaction", "environment": "staging"},
  1023. timestamp=self.min_ago,
  1024. )
  1025. self.store_transaction_metric(
  1026. 100,
  1027. tags={"transaction": "foo_transaction"},
  1028. timestamp=self.min_ago,
  1029. )
  1030. query = {
  1031. "project": [self.project.id],
  1032. "environment": "staging",
  1033. "orderby": "p50(transaction.duration)",
  1034. "field": [
  1035. "transaction",
  1036. "environment",
  1037. "p50(transaction.duration)",
  1038. ],
  1039. "statsPeriod": "24h",
  1040. "dataset": "metricsEnhanced",
  1041. "per_page": 50,
  1042. }
  1043. response = self.do_request(query)
  1044. assert response.status_code == 200, response.content
  1045. assert len(response.data["data"]) == 1
  1046. data = response.data["data"]
  1047. meta = response.data["meta"]
  1048. assert data[0]["transaction"] == "foo_transaction"
  1049. assert data[0]["environment"] == "staging"
  1050. assert data[0]["p50(transaction.duration)"] == 1
  1051. assert meta["isMetricsData"]
  1052. def test_environment_query(self):
  1053. self.create_environment(self.project, name="staging")
  1054. self.store_transaction_metric(
  1055. 1,
  1056. tags={"transaction": "foo_transaction", "environment": "staging"},
  1057. timestamp=self.min_ago,
  1058. )
  1059. self.store_transaction_metric(
  1060. 100,
  1061. tags={"transaction": "foo_transaction"},
  1062. timestamp=self.min_ago,
  1063. )
  1064. query = {
  1065. "project": [self.project.id],
  1066. "orderby": "p50(transaction.duration)",
  1067. "field": [
  1068. "transaction",
  1069. "environment",
  1070. "p50(transaction.duration)",
  1071. ],
  1072. "query": "!has:environment",
  1073. "statsPeriod": "24h",
  1074. "dataset": "metricsEnhanced",
  1075. "per_page": 50,
  1076. }
  1077. response = self.do_request(query)
  1078. assert response.status_code == 200, response.content
  1079. assert len(response.data["data"]) == 1
  1080. data = response.data["data"]
  1081. meta = response.data["meta"]
  1082. assert data[0]["transaction"] == "foo_transaction"
  1083. assert data[0]["environment"] is None
  1084. assert data[0]["p50(transaction.duration)"] == 100
  1085. assert meta["isMetricsData"]