index.spec.tsx 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {TeamFixture} from 'sentry-fixture/team';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. findByLabelText,
  7. render,
  8. renderGlobalModal,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. } from 'sentry-test/reactTestingLibrary';
  13. import OrganizationStore from 'sentry/stores/organizationStore';
  14. import ProjectsStore from 'sentry/stores/projectsStore';
  15. import TeamStore from 'sentry/stores/teamStore';
  16. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  17. import type {Project} from 'sentry/types/project';
  18. import {browserHistory} from 'sentry/utils/browserHistory';
  19. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  20. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  21. import {
  22. MEPSetting,
  23. MEPState,
  24. } from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  25. import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview';
  26. const teams = [
  27. TeamFixture({id: '1', slug: 'team1', name: 'Team 1'}),
  28. TeamFixture({id: '2', slug: 'team2', name: 'Team 2'}),
  29. ];
  30. function initializeData({
  31. features: additionalFeatures = [],
  32. query = {},
  33. project: prj,
  34. projects,
  35. }: {
  36. features?: string[];
  37. project?: Project;
  38. projects?: Project[];
  39. query?: Record<string, any>;
  40. } = {}) {
  41. const features = ['discover-basic', 'performance-view', ...additionalFeatures];
  42. const project = prj ?? ProjectFixture({teams});
  43. const organization = OrganizationFixture({
  44. features,
  45. });
  46. const initialData = initializeOrg({
  47. organization,
  48. projects: projects ? projects : [project],
  49. router: {
  50. location: {
  51. pathname: '/',
  52. query: {
  53. transaction: '/performance',
  54. project: project.id,
  55. transactionCursor: '1:0:0',
  56. ...query,
  57. },
  58. },
  59. },
  60. });
  61. ProjectsStore.loadInitialData(initialData.projects);
  62. TeamStore.loadInitialData(teams, false, null);
  63. return initialData;
  64. }
  65. function TestComponent({
  66. ...props
  67. }: React.ComponentProps<typeof TransactionSummary> & {
  68. router: InjectedRouter<Record<string, string>, any>;
  69. }) {
  70. if (!props.organization) {
  71. throw new Error('Missing organization');
  72. }
  73. return (
  74. <MetricsCardinalityProvider
  75. organization={props.organization}
  76. location={props.location}
  77. >
  78. <TransactionSummary {...props} />
  79. </MetricsCardinalityProvider>
  80. );
  81. }
  82. describe('Performance > TransactionSummary', function () {
  83. let eventStatsMock: jest.Mock;
  84. beforeEach(function () {
  85. // eslint-disable-next-line no-console
  86. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  87. MockApiClient.clearMockResponses();
  88. MockApiClient.addMockResponse({
  89. url: '/organizations/org-slug/projects/',
  90. body: [],
  91. });
  92. MockApiClient.addMockResponse({
  93. url: '/organizations/org-slug/tags/',
  94. body: [],
  95. });
  96. MockApiClient.addMockResponse({
  97. url: '/organizations/org-slug/tags/user.email/values/',
  98. body: [],
  99. });
  100. eventStatsMock = MockApiClient.addMockResponse({
  101. url: '/organizations/org-slug/events-stats/',
  102. body: {data: [[123, []]]},
  103. });
  104. MockApiClient.addMockResponse({
  105. url: '/organizations/org-slug/releases/stats/',
  106. body: [],
  107. });
  108. MockApiClient.addMockResponse({
  109. url: '/organizations/org-slug/issues/?limit=5&project=2&query=is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d',
  110. body: [],
  111. });
  112. MockApiClient.addMockResponse({
  113. url: '/organizations/org-slug/users/',
  114. body: [],
  115. });
  116. MockApiClient.addMockResponse({
  117. url: '/organizations/org-slug/recent-searches/',
  118. body: [],
  119. });
  120. MockApiClient.addMockResponse({
  121. url: '/organizations/org-slug/recent-searches/',
  122. method: 'POST',
  123. body: [],
  124. });
  125. MockApiClient.addMockResponse({
  126. url: '/organizations/org-slug/sdk-updates/',
  127. body: [],
  128. });
  129. MockApiClient.addMockResponse({
  130. url: '/organizations/org-slug/prompts-activity/',
  131. body: {},
  132. });
  133. MockApiClient.addMockResponse({
  134. url: '/organizations/org-slug/events-facets-performance/',
  135. body: {},
  136. });
  137. // Events Mock totals for the sidebar and other summary data
  138. MockApiClient.addMockResponse({
  139. url: '/organizations/org-slug/events/',
  140. body: {
  141. meta: {
  142. fields: {
  143. 'count()': 'number',
  144. 'apdex()': 'number',
  145. 'count_miserable_user()': 'number',
  146. 'user_misery()': 'number',
  147. 'count_unique_user()': 'number',
  148. 'p95()': 'number',
  149. 'failure_rate()': 'number',
  150. 'tpm()': 'number',
  151. project_threshold_config: 'string',
  152. },
  153. },
  154. data: [
  155. {
  156. 'count()': 2,
  157. 'apdex()': 0.6,
  158. 'count_miserable_user()': 122,
  159. 'user_misery()': 0.114,
  160. 'count_unique_user()': 1,
  161. 'p95()': 750.123,
  162. 'failure_rate()': 1,
  163. 'tpm()': 1,
  164. project_threshold_config: ['duration', 300],
  165. },
  166. ],
  167. },
  168. match: [
  169. (_url, options) => {
  170. return options.query?.field?.includes('p95()');
  171. },
  172. ],
  173. });
  174. // [Metrics Enhanced] Events Mock totals for the sidebar and other summary data
  175. MockApiClient.addMockResponse({
  176. url: '/organizations/org-slug/events/',
  177. body: {
  178. meta: {
  179. fields: {
  180. 'count()': 'number',
  181. 'apdex()': 'number',
  182. 'count_miserable_user()': 'number',
  183. 'user_misery()': 'number',
  184. 'count_unique_user()': 'number',
  185. 'p95()': 'number',
  186. 'failure_rate()': 'number',
  187. 'tpm()': 'number',
  188. project_threshold_config: 'string',
  189. },
  190. isMetricsData: true,
  191. },
  192. data: [
  193. {
  194. 'count()': 200,
  195. 'apdex()': 0.5,
  196. 'count_miserable_user()': 120,
  197. 'user_misery()': 0.1,
  198. 'count_unique_user()': 100,
  199. 'p95()': 731.3132,
  200. 'failure_rate()': 1,
  201. 'tpm()': 100,
  202. project_threshold_config: ['duration', 300],
  203. },
  204. ],
  205. },
  206. match: [
  207. (_url, options) => {
  208. const isMetricsEnhanced =
  209. options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
  210. return options.query?.field?.includes('p95()') && isMetricsEnhanced;
  211. },
  212. ],
  213. });
  214. // Events Mock unfiltered totals for percentage calculations
  215. MockApiClient.addMockResponse({
  216. url: '/organizations/org-slug/events/',
  217. body: {
  218. meta: {
  219. fields: {
  220. 'tpm()': 'number',
  221. },
  222. },
  223. data: [
  224. {
  225. 'tpm()': 1,
  226. },
  227. ],
  228. },
  229. match: [
  230. (_url, options) => {
  231. return (
  232. options.query?.field?.includes('tpm()') &&
  233. !options.query?.field?.includes('p95()')
  234. );
  235. },
  236. ],
  237. });
  238. // Events Mock count totals for histogram percentage calculations
  239. MockApiClient.addMockResponse({
  240. url: '/organizations/org-slug/events/',
  241. body: {
  242. meta: {
  243. fields: {
  244. 'count()': 'number',
  245. },
  246. },
  247. data: [
  248. {
  249. 'count()': 2,
  250. },
  251. ],
  252. },
  253. match: [
  254. (_url, options) => {
  255. return (
  256. options.query?.field?.length === 1 && options.query?.field[0] === 'count()'
  257. );
  258. },
  259. ],
  260. });
  261. // Events Transaction list response
  262. MockApiClient.addMockResponse({
  263. url: '/organizations/org-slug/events/',
  264. headers: {
  265. Link:
  266. '<http://localhost/api/0/organizations/org-slug/events/?cursor=2:0:0>; rel="next"; results="true"; cursor="2:0:0",' +
  267. '<http://localhost/api/0/organizations/org-slug/events/?cursor=1:0:0>; rel="previous"; results="false"; cursor="1:0:0"',
  268. },
  269. body: {
  270. meta: {
  271. fields: {
  272. id: 'string',
  273. 'user.display': 'string',
  274. 'transaction.duration': 'duration',
  275. 'project.id': 'integer',
  276. timestamp: 'date',
  277. },
  278. },
  279. data: [
  280. {
  281. id: 'deadbeef',
  282. 'user.display': 'uhoh@example.com',
  283. 'transaction.duration': 400,
  284. 'project.id': 2,
  285. timestamp: '2020-05-21T15:31:18+00:00',
  286. },
  287. ],
  288. },
  289. match: [
  290. (_url, options) => {
  291. return options.query?.field?.includes('user.display');
  292. },
  293. ],
  294. });
  295. // Events Mock totals for status breakdown
  296. MockApiClient.addMockResponse({
  297. url: '/organizations/org-slug/events/',
  298. body: {
  299. meta: {
  300. fields: {
  301. 'transaction.status': 'string',
  302. 'count()': 'number',
  303. },
  304. },
  305. data: [
  306. {
  307. 'count()': 2,
  308. 'transaction.status': 'ok',
  309. },
  310. ],
  311. },
  312. match: [
  313. (_url, options) => {
  314. return options.query?.field?.includes('transaction.status');
  315. },
  316. ],
  317. });
  318. MockApiClient.addMockResponse({
  319. url: '/organizations/org-slug/events-facets/',
  320. body: [
  321. {
  322. key: 'release',
  323. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  324. },
  325. {
  326. key: 'environment',
  327. topValues: [
  328. {count: 2, value: 'dev', name: 'dev'},
  329. {count: 1, value: 'prod', name: 'prod'},
  330. ],
  331. },
  332. {
  333. key: 'foo',
  334. topValues: [
  335. {count: 2, value: 'bar', name: 'bar'},
  336. {count: 1, value: 'baz', name: 'baz'},
  337. ],
  338. },
  339. {
  340. key: 'user',
  341. topValues: [
  342. {count: 2, value: 'id:100', name: '100'},
  343. {count: 1, value: 'id:101', name: '101'},
  344. ],
  345. },
  346. ],
  347. });
  348. MockApiClient.addMockResponse({
  349. url: '/organizations/org-slug/project-transaction-threshold-override/',
  350. method: 'GET',
  351. body: {
  352. threshold: '800',
  353. metric: 'lcp',
  354. },
  355. });
  356. MockApiClient.addMockResponse({
  357. url: '/organizations/org-slug/events-vitals/',
  358. body: {
  359. 'measurements.fcp': {
  360. poor: 3,
  361. meh: 100,
  362. good: 47,
  363. total: 150,
  364. p75: 1500,
  365. },
  366. 'measurements.lcp': {
  367. poor: 2,
  368. meh: 38,
  369. good: 40,
  370. total: 80,
  371. p75: 2750,
  372. },
  373. 'measurements.fid': {
  374. poor: 2,
  375. meh: 53,
  376. good: 5,
  377. total: 60,
  378. p75: 1000,
  379. },
  380. 'measurements.cls': {
  381. poor: 3,
  382. meh: 10,
  383. good: 4,
  384. total: 17,
  385. p75: 0.2,
  386. },
  387. },
  388. });
  389. MockApiClient.addMockResponse({
  390. method: 'GET',
  391. url: `/organizations/org-slug/key-transactions-list/`,
  392. body: teams.map(({id}) => ({
  393. team: id,
  394. count: 0,
  395. keyed: [],
  396. })),
  397. });
  398. MockApiClient.addMockResponse({
  399. url: '/organizations/org-slug/events-has-measurements/',
  400. body: {measurements: false},
  401. });
  402. MockApiClient.addMockResponse({
  403. url: '/organizations/org-slug/events-spans-performance/',
  404. body: [
  405. {
  406. op: 'ui.long-task',
  407. group: 'c777169faad84eb4',
  408. description: 'Main UI thread blocked',
  409. frequency: 713,
  410. count: 9040,
  411. avgOccurrences: null,
  412. sumExclusiveTime: 1743893.9822921753,
  413. p50ExclusiveTime: null,
  414. p75ExclusiveTime: 244.9998779296875,
  415. p95ExclusiveTime: null,
  416. p99ExclusiveTime: null,
  417. },
  418. ],
  419. });
  420. MockApiClient.addMockResponse({
  421. method: 'GET',
  422. url: `/organizations/org-slug/metrics-compatibility/`,
  423. body: {
  424. compatible_projects: [],
  425. incompatible_projecs: [],
  426. },
  427. });
  428. MockApiClient.addMockResponse({
  429. method: 'GET',
  430. url: `/organizations/org-slug/metrics-compatibility-sums/`,
  431. body: {
  432. sum: {
  433. metrics: 100,
  434. metrics_null: 0,
  435. metrics_unparam: 0,
  436. },
  437. },
  438. });
  439. // Events Mock slowest functions
  440. MockApiClient.addMockResponse({
  441. url: '/organizations/org-slug/events/',
  442. body: {
  443. meta: {
  444. fields: {
  445. function: 'string',
  446. package: 'string',
  447. 'p75()': 'duration',
  448. 'count()': 'integer',
  449. 'sum()': 'duration',
  450. 'all_examples()': 'string',
  451. },
  452. },
  453. data: [],
  454. },
  455. match: [
  456. (_url, options) => {
  457. return options.query?.field?.indexOf('all_examples()') !== -1;
  458. },
  459. ],
  460. });
  461. // Flamegraph mock
  462. MockApiClient.addMockResponse({
  463. url: '/organizations/org-slug/profiling/flamegraph/',
  464. body: {
  465. activeProfileIndex: 0,
  466. metadata: {
  467. deviceClassification: '',
  468. deviceLocale: '',
  469. deviceManufacturer: '',
  470. deviceModel: '',
  471. deviceOSName: '',
  472. deviceOSVersion: '',
  473. durationNS: 0,
  474. organizationID: 0,
  475. platform: '',
  476. profileID: '',
  477. projectID: 0,
  478. received: '0001-01-01T00:00:00Z',
  479. sampled: false,
  480. timestamp: '0001-01-01T00:00:00Z',
  481. traceID: '',
  482. transactionID: '',
  483. transactionName: '',
  484. version: '',
  485. },
  486. platform: '',
  487. profiles: [
  488. {
  489. endValue: 0,
  490. isMainThread: true,
  491. name: '',
  492. samples: [],
  493. startValue: 0,
  494. threadID: 0,
  495. type: 'sampled',
  496. unit: 'count',
  497. weights: [],
  498. sample_durations_ns: null,
  499. },
  500. ],
  501. projectID: 0,
  502. shared: {
  503. frames: [],
  504. },
  505. transactionName: '',
  506. metrics: [],
  507. },
  508. });
  509. jest.spyOn(MEPSetting, 'get').mockImplementation(() => MEPState.AUTO);
  510. });
  511. afterEach(function () {
  512. MockApiClient.clearMockResponses();
  513. ProjectsStore.reset();
  514. jest.clearAllMocks();
  515. });
  516. describe('with events', function () {
  517. it('renders basic UI elements', async function () {
  518. const {organization, router} = initializeData();
  519. render(
  520. <TestComponent
  521. organization={organization}
  522. router={router}
  523. location={router.location}
  524. />,
  525. {
  526. router,
  527. organization,
  528. }
  529. );
  530. // It shows the header
  531. await screen.findByText('Transaction Summary');
  532. expect(screen.getByText('/performance')).toBeInTheDocument();
  533. // It shows a chart
  534. expect(
  535. screen.getByRole('button', {name: 'Display Duration Breakdown'})
  536. ).toBeInTheDocument();
  537. // It shows a searchbar
  538. expect(screen.getByLabelText('Search events')).toBeInTheDocument();
  539. // It shows a table
  540. expect(screen.getByTestId('transactions-table')).toBeInTheDocument();
  541. // Ensure open in discover button exists.
  542. expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument();
  543. // Ensure open issues button exists.
  544. expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument();
  545. // Ensure transaction filter button exists
  546. expect(
  547. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  548. ).toBeInTheDocument();
  549. // Ensure create alert from discover is hidden without metric alert
  550. expect(
  551. screen.queryByRole('button', {name: 'Create Alert'})
  552. ).not.toBeInTheDocument();
  553. // Ensure status breakdown exists
  554. expect(screen.getByText('Status Breakdown')).toBeInTheDocument();
  555. });
  556. it('renders feature flagged UI elements', function () {
  557. const {organization, router} = initializeData({
  558. features: ['incidents'],
  559. });
  560. render(
  561. <TestComponent
  562. organization={organization}
  563. router={router}
  564. location={router.location}
  565. />,
  566. {
  567. router,
  568. organization,
  569. }
  570. );
  571. // Ensure create alert from discover is shown with metric alerts
  572. expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument();
  573. });
  574. it('renders Web Vitals widget', async function () {
  575. const {organization, router} = initializeData({
  576. project: ProjectFixture({teams, platform: 'javascript'}),
  577. query: {
  578. query:
  579. 'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/',
  580. },
  581. });
  582. render(
  583. <TestComponent
  584. organization={organization}
  585. router={router}
  586. location={router.location}
  587. />,
  588. {
  589. router,
  590. organization,
  591. }
  592. );
  593. // It renders the web vitals widget
  594. await screen.findByRole('heading', {name: 'Web Vitals'});
  595. await waitFor(() => {
  596. expect(screen.getAllByTestId('vital-status')).toHaveLength(3);
  597. });
  598. const vitalStatues = screen.getAllByTestId('vital-status');
  599. expect(vitalStatues[0]).toHaveTextContent('31%');
  600. expect(vitalStatues[1]).toHaveTextContent('65%');
  601. expect(vitalStatues[2]).toHaveTextContent('3%');
  602. });
  603. it('renders sidebar widgets', async function () {
  604. const {organization, router} = initializeData({});
  605. render(
  606. <TestComponent
  607. organization={organization}
  608. router={router}
  609. location={router.location}
  610. />,
  611. {
  612. router,
  613. organization,
  614. }
  615. );
  616. // Renders Apdex widget
  617. await screen.findByRole('heading', {name: 'Apdex'});
  618. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  619. // Renders Failure Rate widget
  620. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  621. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  622. });
  623. it('renders project picker modal when no url does not have project id', async function () {
  624. MockApiClient.addMockResponse({
  625. url: '/organizations/org-slug/events/',
  626. body: {
  627. meta: {
  628. fields: {
  629. project: 'string',
  630. 'count()': 'number',
  631. },
  632. },
  633. data: [
  634. {
  635. 'count()': 2,
  636. project: 'proj-slug-1',
  637. },
  638. {
  639. 'count()': 3,
  640. project: 'proj-slug-2',
  641. },
  642. ],
  643. },
  644. match: [
  645. (_url, options) => {
  646. return options.query?.field?.includes('project');
  647. },
  648. ],
  649. });
  650. const projects = [
  651. ProjectFixture({
  652. slug: 'proj-slug-1',
  653. id: '1',
  654. name: 'Project Name 1',
  655. }),
  656. ProjectFixture({
  657. slug: 'proj-slug-2',
  658. id: '2',
  659. name: 'Project Name 2',
  660. }),
  661. ];
  662. OrganizationStore.onUpdate(OrganizationFixture({slug: 'org-slug'}), {
  663. replace: true,
  664. });
  665. const {organization, router} = initializeData({projects});
  666. const spy = jest.spyOn(router, 'replace');
  667. // Ensure project id is not in path
  668. delete router.location.query.project;
  669. render(
  670. <TestComponent
  671. organization={organization}
  672. router={router}
  673. location={router.location}
  674. />,
  675. {router, organization}
  676. );
  677. renderGlobalModal();
  678. const firstProjectOption = await screen.findByText('proj-slug-1');
  679. expect(firstProjectOption).toBeInTheDocument();
  680. expect(screen.getByText('proj-slug-2')).toBeInTheDocument();
  681. expect(screen.getByText('My Projects')).toBeInTheDocument();
  682. await userEvent.click(firstProjectOption);
  683. expect(spy).toHaveBeenCalledWith(
  684. '/organizations/org-slug/performance/summary/?transaction=/performance&statsPeriod=14d&referrer=performance-transaction-summary&transactionCursor=1:0:0&project=1'
  685. );
  686. });
  687. it('fetches transaction threshold', function () {
  688. const {organization, router} = initializeData();
  689. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  690. url: '/organizations/org-slug/project-transaction-threshold-override/',
  691. method: 'GET',
  692. body: {
  693. threshold: '800',
  694. metric: 'lcp',
  695. },
  696. });
  697. const getProjectThresholdMock = MockApiClient.addMockResponse({
  698. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  699. method: 'GET',
  700. body: {
  701. threshold: '200',
  702. metric: 'duration',
  703. },
  704. });
  705. render(
  706. <TestComponent
  707. organization={organization}
  708. router={router}
  709. location={router.location}
  710. />,
  711. {
  712. router,
  713. organization,
  714. }
  715. );
  716. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  717. expect(getProjectThresholdMock).not.toHaveBeenCalled();
  718. });
  719. it('fetches project transaction threshdold', async function () {
  720. const {organization, router} = initializeData();
  721. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  722. url: '/organizations/org-slug/project-transaction-threshold-override/',
  723. method: 'GET',
  724. statusCode: 404,
  725. });
  726. const getProjectThresholdMock = MockApiClient.addMockResponse({
  727. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  728. method: 'GET',
  729. body: {
  730. threshold: '200',
  731. metric: 'duration',
  732. },
  733. });
  734. render(
  735. <TestComponent
  736. organization={organization}
  737. router={router}
  738. location={router.location}
  739. />,
  740. {
  741. router,
  742. organization,
  743. }
  744. );
  745. await screen.findByText('Transaction Summary');
  746. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  747. expect(getProjectThresholdMock).toHaveBeenCalledTimes(1);
  748. });
  749. it('triggers a navigation on search', async function () {
  750. const {organization, router} = initializeData();
  751. render(
  752. <TestComponent
  753. organization={organization}
  754. router={router}
  755. location={router.location}
  756. />,
  757. {
  758. router,
  759. organization,
  760. }
  761. );
  762. // Fill out the search box, and submit it.
  763. await userEvent.type(
  764. screen.getByLabelText('Search events'),
  765. 'user.email:uhoh*{enter}'
  766. );
  767. // Check the navigation.
  768. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  769. expect(browserHistory.push).toHaveBeenCalledWith({
  770. pathname: '/',
  771. query: {
  772. transaction: '/performance',
  773. project: '2',
  774. statsPeriod: '14d',
  775. query: 'user.email:uhoh*',
  776. transactionCursor: '1:0:0',
  777. },
  778. });
  779. });
  780. it('can mark a transaction as key', async function () {
  781. const {organization, router} = initializeData();
  782. render(
  783. <TestComponent
  784. organization={organization}
  785. router={router}
  786. location={router.location}
  787. />,
  788. {
  789. router,
  790. organization,
  791. }
  792. );
  793. const mockUpdate = MockApiClient.addMockResponse({
  794. url: `/organizations/org-slug/key-transactions/`,
  795. method: 'POST',
  796. body: {},
  797. });
  798. await screen.findByRole('button', {name: 'Star for Team'});
  799. // Click the key transaction button
  800. await userEvent.click(screen.getByRole('button', {name: 'Star for Team'}));
  801. await userEvent.click(screen.getByRole('option', {name: '#team1'}));
  802. // Ensure request was made.
  803. expect(mockUpdate).toHaveBeenCalled();
  804. });
  805. it('triggers a navigation on transaction filter', async function () {
  806. const {organization, router} = initializeData();
  807. render(
  808. <TestComponent
  809. organization={organization}
  810. router={router}
  811. location={router.location}
  812. />,
  813. {
  814. router,
  815. organization,
  816. }
  817. );
  818. await screen.findByText('Transaction Summary');
  819. await waitFor(() => {
  820. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  821. });
  822. // Open the transaction filter dropdown
  823. await userEvent.click(
  824. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  825. );
  826. await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]);
  827. // Check the navigation.
  828. expect(browserHistory.push).toHaveBeenCalledWith({
  829. pathname: '/',
  830. query: {
  831. transaction: '/performance',
  832. project: '2',
  833. showTransactions: 'slow',
  834. transactionCursor: undefined,
  835. },
  836. });
  837. });
  838. it('renders pagination buttons', async function () {
  839. const {organization, router} = initializeData();
  840. render(
  841. <TestComponent
  842. organization={organization}
  843. router={router}
  844. location={router.location}
  845. />,
  846. {
  847. router,
  848. organization,
  849. }
  850. );
  851. await screen.findByText('Transaction Summary');
  852. const pagination = await screen.findByTestId('pagination');
  853. expect(await findByLabelText(pagination, 'Previous')).toBeInTheDocument();
  854. expect(await findByLabelText(pagination, 'Next')).toBeInTheDocument();
  855. // Click the 'next' button
  856. await userEvent.click(await findByLabelText(pagination, 'Next'));
  857. // Check the navigation.
  858. expect(browserHistory.push).toHaveBeenCalledWith({
  859. pathname: '/',
  860. query: {
  861. transaction: '/performance',
  862. project: '2',
  863. transactionCursor: '2:0:0',
  864. },
  865. });
  866. });
  867. it('forwards conditions to related issues', async function () {
  868. const issueGet = MockApiClient.addMockResponse({
  869. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d',
  870. body: [],
  871. });
  872. const {organization, router} = initializeData({
  873. query: {query: 'tag:value'},
  874. });
  875. render(
  876. <TestComponent
  877. organization={organization}
  878. router={router}
  879. location={router.location}
  880. />,
  881. {
  882. router,
  883. organization,
  884. }
  885. );
  886. await screen.findByText('Transaction Summary');
  887. expect(issueGet).toHaveBeenCalled();
  888. });
  889. it('does not forward event type to related issues', async function () {
  890. const issueGet = MockApiClient.addMockResponse({
  891. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d',
  892. body: [],
  893. match: [
  894. (_, options) => {
  895. // event.type must NOT be in the query params
  896. return !options.query?.query?.includes('event.type');
  897. },
  898. ],
  899. });
  900. const {organization, router} = initializeData({
  901. query: {query: 'tag:value event.type:transaction'},
  902. });
  903. render(
  904. <TestComponent
  905. organization={organization}
  906. router={router}
  907. location={router.location}
  908. />,
  909. {
  910. router,
  911. organization,
  912. }
  913. );
  914. await screen.findByText('Transaction Summary');
  915. expect(issueGet).toHaveBeenCalled();
  916. });
  917. it('renders the suspect spans table if the feature is enabled', async function () {
  918. MockApiClient.addMockResponse({
  919. url: '/organizations/org-slug/events-spans-performance/',
  920. body: [],
  921. });
  922. const {organization, router} = initializeData();
  923. render(
  924. <TestComponent
  925. organization={organization}
  926. router={router}
  927. location={router.location}
  928. />,
  929. {
  930. router,
  931. organization,
  932. }
  933. );
  934. expect(await screen.findByText('Suspect Spans')).toBeInTheDocument();
  935. });
  936. it('adds search condition on transaction status when clicking on status breakdown', async function () {
  937. const {organization, router} = initializeData();
  938. render(
  939. <TestComponent
  940. organization={organization}
  941. router={router}
  942. location={router.location}
  943. />,
  944. {
  945. router,
  946. organization,
  947. }
  948. );
  949. await screen.findByTestId('status-ok');
  950. await userEvent.click(screen.getByTestId('status-ok'));
  951. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  952. expect(browserHistory.push).toHaveBeenCalledWith(
  953. expect.objectContaining({
  954. query: expect.objectContaining({
  955. query: expect.stringContaining('transaction.status:ok'),
  956. }),
  957. })
  958. );
  959. });
  960. it('appends tag value to existing query when clicked', async function () {
  961. const {organization, router} = initializeData();
  962. render(
  963. <TestComponent
  964. organization={organization}
  965. router={router}
  966. location={router.location}
  967. />,
  968. {
  969. router,
  970. organization,
  971. }
  972. );
  973. await screen.findByText('Tag Summary');
  974. // Expand environment tag
  975. await userEvent.click(await screen.findByText('environment'));
  976. // Select dev
  977. await userEvent.click(
  978. await screen.findByLabelText(
  979. 'environment, dev, 100% of all events. View events with this tag value.'
  980. )
  981. );
  982. // Expand foo tag
  983. await userEvent.click(await screen.findByText('foo'));
  984. // Select bar
  985. await userEvent.click(
  986. await screen.findByLabelText(
  987. 'foo, bar, 100% of all events. View events with this tag value.'
  988. )
  989. );
  990. expect(router.push).toHaveBeenCalledTimes(2);
  991. expect(router.push).toHaveBeenNthCalledWith(1, {
  992. pathname: '/',
  993. query: {
  994. project: '2',
  995. query: 'tags[environment]:dev',
  996. transaction: '/performance',
  997. transactionCursor: '1:0:0',
  998. },
  999. });
  1000. expect(router.push).toHaveBeenNthCalledWith(2, {
  1001. pathname: '/',
  1002. query: {
  1003. project: '2',
  1004. query: 'foo:bar',
  1005. transaction: '/performance',
  1006. transactionCursor: '1:0:0',
  1007. },
  1008. });
  1009. });
  1010. it('does not use MEP dataset for stats query without features', async function () {
  1011. const {organization, router} = initializeData({
  1012. query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
  1013. features: [''], // No 'dynamic-sampling' feature to indicate it can use metrics dataset or metrics enhanced.
  1014. });
  1015. render(
  1016. <TestComponent
  1017. organization={organization}
  1018. router={router}
  1019. location={router.location}
  1020. />,
  1021. {
  1022. router,
  1023. organization,
  1024. }
  1025. );
  1026. await screen.findByText('Transaction Summary');
  1027. await screen.findByRole('heading', {name: 'Apdex'});
  1028. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  1029. expect(eventStatsMock).toHaveBeenNthCalledWith(
  1030. 1,
  1031. expect.anything(),
  1032. expect.objectContaining({
  1033. query: expect.objectContaining({
  1034. environment: [],
  1035. interval: '30m',
  1036. partial: '1',
  1037. project: [2],
  1038. query:
  1039. 'transaction.op:pageload event.type:transaction transaction:/performance',
  1040. referrer: 'api.performance.transaction-summary.duration-chart',
  1041. statsPeriod: '14d',
  1042. yAxis: [
  1043. 'p50(transaction.duration)',
  1044. 'p75(transaction.duration)',
  1045. 'p95(transaction.duration)',
  1046. 'p99(transaction.duration)',
  1047. 'p100(transaction.duration)',
  1048. 'avg(transaction.duration)',
  1049. ],
  1050. }),
  1051. })
  1052. );
  1053. });
  1054. it('uses MEP dataset for stats query', async function () {
  1055. const {organization, router} = initializeData({
  1056. query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
  1057. features: ['dynamic-sampling', 'mep-rollout-flag'],
  1058. });
  1059. render(
  1060. <TestComponent
  1061. organization={organization}
  1062. router={router}
  1063. location={router.location}
  1064. />,
  1065. {
  1066. router,
  1067. organization,
  1068. }
  1069. );
  1070. await screen.findByText('Transaction Summary');
  1071. // Renders Apdex widget
  1072. await screen.findByRole('heading', {name: 'Apdex'});
  1073. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
  1074. expect(eventStatsMock).toHaveBeenNthCalledWith(
  1075. 1,
  1076. expect.anything(),
  1077. expect.objectContaining({
  1078. query: expect.objectContaining({
  1079. query:
  1080. 'transaction.op:pageload event.type:transaction transaction:/performance',
  1081. dataset: 'metricsEnhanced',
  1082. }),
  1083. })
  1084. );
  1085. // Renders Failure Rate widget
  1086. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  1087. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  1088. expect(
  1089. screen.queryByTestId('search-metrics-fallback-warning')
  1090. ).not.toBeInTheDocument();
  1091. });
  1092. it('does not use MEP dataset for stats query if cardinality fallback fails', async function () {
  1093. MockApiClient.addMockResponse({
  1094. method: 'GET',
  1095. url: `/organizations/org-slug/metrics-compatibility-sums/`,
  1096. body: {
  1097. sum: {
  1098. metrics: 100,
  1099. metrics_null: 100,
  1100. metrics_unparam: 0,
  1101. },
  1102. },
  1103. });
  1104. const {organization, router} = initializeData({
  1105. query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
  1106. features: ['dynamic-sampling', 'mep-rollout-flag'],
  1107. });
  1108. render(
  1109. <TestComponent
  1110. organization={organization}
  1111. router={router}
  1112. location={router.location}
  1113. />,
  1114. {
  1115. router,
  1116. organization,
  1117. }
  1118. );
  1119. await screen.findByText('Transaction Summary');
  1120. // Renders Apdex widget
  1121. await screen.findByRole('heading', {name: 'Apdex'});
  1122. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  1123. expect(eventStatsMock).toHaveBeenNthCalledWith(
  1124. 1,
  1125. expect.anything(),
  1126. expect.objectContaining({
  1127. query: expect.objectContaining({
  1128. query:
  1129. 'transaction.op:pageload event.type:transaction transaction:/performance',
  1130. }),
  1131. })
  1132. );
  1133. });
  1134. it('uses MEP dataset for stats query and shows fallback warning', async function () {
  1135. MockApiClient.addMockResponse({
  1136. url: '/organizations/org-slug/issues/?limit=5&project=2&query=has%3Anot-compatible%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d',
  1137. body: [],
  1138. });
  1139. MockApiClient.addMockResponse({
  1140. url: '/organizations/org-slug/events/',
  1141. body: {
  1142. meta: {
  1143. fields: {
  1144. 'count()': 'number',
  1145. 'apdex()': 'number',
  1146. 'count_miserable_user()': 'number',
  1147. 'user_misery()': 'number',
  1148. 'count_unique_user()': 'number',
  1149. 'p95()': 'number',
  1150. 'failure_rate()': 'number',
  1151. 'tpm()': 'number',
  1152. project_threshold_config: 'string',
  1153. },
  1154. isMetricsData: false, // The total response is setting the metrics fallback behaviour.
  1155. },
  1156. data: [
  1157. {
  1158. 'count()': 200,
  1159. 'apdex()': 0.5,
  1160. 'count_miserable_user()': 120,
  1161. 'user_misery()': 0.1,
  1162. 'count_unique_user()': 100,
  1163. 'p95()': 731.3132,
  1164. 'failure_rate()': 1,
  1165. 'tpm()': 100,
  1166. project_threshold_config: ['duration', 300],
  1167. },
  1168. ],
  1169. },
  1170. match: [
  1171. (_url, options) => {
  1172. const isMetricsEnhanced =
  1173. options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
  1174. return (
  1175. options.query?.field?.includes('p95()') &&
  1176. isMetricsEnhanced &&
  1177. options.query?.query?.includes('not-compatible')
  1178. );
  1179. },
  1180. ],
  1181. });
  1182. const {organization, router} = initializeData({
  1183. query: {query: 'transaction.op:pageload has:not-compatible'}, // Adds incompatible w/ metrics tag
  1184. features: ['dynamic-sampling', 'mep-rollout-flag'],
  1185. });
  1186. render(
  1187. <TestComponent
  1188. organization={organization}
  1189. router={router}
  1190. location={router.location}
  1191. />,
  1192. {
  1193. router,
  1194. organization,
  1195. }
  1196. );
  1197. await screen.findByText('Transaction Summary');
  1198. // Renders Apdex widget
  1199. await screen.findByRole('heading', {name: 'Apdex'});
  1200. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
  1201. expect(eventStatsMock).toHaveBeenNthCalledWith(
  1202. 1,
  1203. expect.anything(),
  1204. expect.objectContaining({
  1205. query: expect.objectContaining({
  1206. query:
  1207. 'transaction.op:pageload has:not-compatible event.type:transaction transaction:/performance',
  1208. dataset: 'metricsEnhanced',
  1209. }),
  1210. })
  1211. );
  1212. // Renders Failure Rate widget
  1213. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  1214. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  1215. expect(screen.getByTestId('search-metrics-fallback-warning')).toBeInTheDocument();
  1216. });
  1217. });
  1218. });