transactionSummary.spec.tsx 36 KB

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