index.spec.tsx 38 KB

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