index.spec.tsx 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298
  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, waitFor} 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 Mock unfiltered totals for percentage calculations
  244. MockApiClient.addMockResponse({
  245. url: '/organizations/org-slug/events/',
  246. body: {
  247. meta: {
  248. fields: {
  249. 'tpm()': 'number',
  250. },
  251. },
  252. data: [
  253. {
  254. 'tpm()': 1,
  255. },
  256. ],
  257. },
  258. match: [
  259. (_url, options) => {
  260. return (
  261. options.query?.field?.includes('tpm()') &&
  262. !options.query?.field?.includes('p95()')
  263. );
  264. },
  265. ],
  266. });
  267. // Events Mock count totals for histogram percentage calculations
  268. MockApiClient.addMockResponse({
  269. url: '/organizations/org-slug/events/',
  270. body: {
  271. meta: {
  272. fields: {
  273. 'count()': 'number',
  274. },
  275. },
  276. data: [
  277. {
  278. 'count()': 2,
  279. },
  280. ],
  281. },
  282. match: [
  283. (_url, options) => {
  284. return (
  285. options.query?.field?.includes('count()') &&
  286. !options.query?.field?.includes('p95()')
  287. );
  288. },
  289. ],
  290. });
  291. // Events Transaction list response
  292. MockApiClient.addMockResponse({
  293. url: '/organizations/org-slug/events/',
  294. headers: {
  295. Link:
  296. '<http://localhost/api/0/organizations/org-slug/events/?cursor=2:0:0>; rel="next"; results="true"; cursor="2:0:0",' +
  297. '<http://localhost/api/0/organizations/org-slug/events/?cursor=1:0:0>; rel="previous"; results="false"; cursor="1:0:0"',
  298. },
  299. body: {
  300. meta: {
  301. fields: {
  302. id: 'string',
  303. 'user.display': 'string',
  304. 'transaction.duration': 'duration',
  305. 'project.id': 'integer',
  306. timestamp: 'date',
  307. },
  308. },
  309. data: [
  310. {
  311. id: 'deadbeef',
  312. 'user.display': 'uhoh@example.com',
  313. 'transaction.duration': 400,
  314. 'project.id': 2,
  315. timestamp: '2020-05-21T15:31:18+00:00',
  316. },
  317. ],
  318. },
  319. match: [
  320. (_url, options) => {
  321. return options.query?.field?.includes('user.display');
  322. },
  323. ],
  324. });
  325. // Events Mock totals for status breakdown
  326. MockApiClient.addMockResponse({
  327. url: '/organizations/org-slug/events/',
  328. body: {
  329. meta: {
  330. fields: {
  331. 'transaction.status': 'string',
  332. 'count()': 'number',
  333. },
  334. },
  335. data: [
  336. {
  337. 'count()': 2,
  338. 'transaction.status': 'ok',
  339. },
  340. ],
  341. },
  342. match: [
  343. (_url, options) => {
  344. return options.query?.field?.includes('transaction.status');
  345. },
  346. ],
  347. });
  348. MockApiClient.addMockResponse({
  349. url: '/organizations/org-slug/events-facets/',
  350. body: [
  351. {
  352. key: 'release',
  353. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  354. },
  355. {
  356. key: 'environment',
  357. topValues: [
  358. {count: 2, value: 'dev', name: 'dev'},
  359. {count: 1, value: 'prod', name: 'prod'},
  360. ],
  361. },
  362. {
  363. key: 'foo',
  364. topValues: [
  365. {count: 2, value: 'bar', name: 'bar'},
  366. {count: 1, value: 'baz', name: 'baz'},
  367. ],
  368. },
  369. {
  370. key: 'user',
  371. topValues: [
  372. {count: 2, value: 'id:100', name: '100'},
  373. {count: 1, value: 'id:101', name: '101'},
  374. ],
  375. },
  376. ],
  377. });
  378. MockApiClient.addMockResponse({
  379. url: '/organizations/org-slug/project-transaction-threshold-override/',
  380. method: 'GET',
  381. body: {
  382. threshold: '800',
  383. metric: 'lcp',
  384. },
  385. });
  386. MockApiClient.addMockResponse({
  387. url: '/organizations/org-slug/events-vitals/',
  388. body: {
  389. 'measurements.fcp': {
  390. poor: 3,
  391. meh: 100,
  392. good: 47,
  393. total: 150,
  394. p75: 1500,
  395. },
  396. 'measurements.lcp': {
  397. poor: 2,
  398. meh: 38,
  399. good: 40,
  400. total: 80,
  401. p75: 2750,
  402. },
  403. 'measurements.fid': {
  404. poor: 2,
  405. meh: 53,
  406. good: 5,
  407. total: 60,
  408. p75: 1000,
  409. },
  410. 'measurements.cls': {
  411. poor: 3,
  412. meh: 10,
  413. good: 4,
  414. total: 17,
  415. p75: 0.2,
  416. },
  417. },
  418. });
  419. MockApiClient.addMockResponse({
  420. method: 'GET',
  421. url: `/organizations/org-slug/key-transactions-list/`,
  422. body: teams.map(({id}) => ({
  423. team: id,
  424. count: 0,
  425. keyed: [],
  426. })),
  427. });
  428. MockApiClient.addMockResponse({
  429. url: '/organizations/org-slug/events-has-measurements/',
  430. body: {measurements: false},
  431. });
  432. MockApiClient.addMockResponse({
  433. url: '/organizations/org-slug/events-spans-performance/',
  434. body: [
  435. {
  436. op: 'ui.long-task',
  437. group: 'c777169faad84eb4',
  438. description: 'Main UI thread blocked',
  439. frequency: 713,
  440. count: 9040,
  441. avgOccurrences: null,
  442. sumExclusiveTime: 1743893.9822921753,
  443. p50ExclusiveTime: null,
  444. p75ExclusiveTime: 244.9998779296875,
  445. p95ExclusiveTime: null,
  446. p99ExclusiveTime: null,
  447. },
  448. ],
  449. });
  450. MockApiClient.addMockResponse({
  451. url: `/projects/org-slug/project-slug/profiling/functions/`,
  452. body: {functions: []},
  453. });
  454. jest.spyOn(MEPSetting, 'get').mockImplementation(() => MEPState.auto);
  455. });
  456. afterEach(function () {
  457. MockApiClient.clearMockResponses();
  458. ProjectsStore.reset();
  459. jest.clearAllMocks();
  460. jest.restoreAllMocks();
  461. });
  462. describe('with discover', function () {
  463. it('renders basic UI elements', async function () {
  464. const {organization, router, routerContext} = initializeData();
  465. render(<TestComponent router={router} location={router.location} />, {
  466. context: routerContext,
  467. organization,
  468. });
  469. // It shows the header
  470. await screen.findByText('Transaction Summary');
  471. expect(screen.getByRole('heading', {name: '/performance'})).toBeInTheDocument();
  472. // It shows a chart
  473. expect(
  474. screen.getByRole('button', {name: 'Display Duration Breakdown'})
  475. ).toBeInTheDocument();
  476. // It shows a searchbar
  477. expect(screen.getByLabelText('Search events')).toBeInTheDocument();
  478. // It shows a table
  479. expect(screen.getByTestId('transactions-table')).toBeInTheDocument();
  480. // Ensure open in discover button exists.
  481. expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument();
  482. // Ensure open issues button exists.
  483. expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument();
  484. // Ensure transaction filter button exists
  485. expect(
  486. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  487. ).toBeInTheDocument();
  488. // Ensure ops breakdown filter exists
  489. expect(
  490. screen.getByRole('button', {name: 'Filter by operation'})
  491. ).toBeInTheDocument();
  492. // Ensure create alert from discover is hidden without metric alert
  493. expect(
  494. screen.queryByRole('button', {name: 'Create Alert'})
  495. ).not.toBeInTheDocument();
  496. // Ensure status breakdown exists
  497. expect(screen.getByText('Status Breakdown')).toBeInTheDocument();
  498. });
  499. it('renders feature flagged UI elements', function () {
  500. const {organization, router, routerContext} = initializeData({
  501. features: ['incidents'],
  502. });
  503. render(<TestComponent router={router} location={router.location} />, {
  504. context: routerContext,
  505. organization,
  506. });
  507. // Ensure create alert from discover is shown with metric alerts
  508. expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument();
  509. });
  510. it('renders Web Vitals widget', async function () {
  511. const {organization, router, routerContext} = initializeData({
  512. project: TestStubs.Project({teams, platform: 'javascript'}),
  513. query: {
  514. query:
  515. 'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/',
  516. },
  517. });
  518. render(<TestComponent router={router} location={router.location} />, {
  519. context: routerContext,
  520. organization,
  521. });
  522. // It renders the web vitals widget
  523. await screen.findByRole('heading', {name: 'Web Vitals'});
  524. const vitalStatues = screen.getAllByTestId('vital-status');
  525. expect(vitalStatues).toHaveLength(3);
  526. expect(vitalStatues[0]).toHaveTextContent('31%');
  527. expect(vitalStatues[1]).toHaveTextContent('65%');
  528. expect(vitalStatues[2]).toHaveTextContent('3%');
  529. });
  530. it('renders sidebar widgets', async function () {
  531. const {organization, router, routerContext} = initializeData();
  532. render(<TestComponent router={router} location={router.location} />, {
  533. context: routerContext,
  534. organization,
  535. });
  536. // Renders Apdex widget
  537. await screen.findByRole('heading', {name: 'Apdex'});
  538. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  539. // Renders Failure Rate widget
  540. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  541. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  542. // Renders TPM widget
  543. expect(screen.getByRole('heading', {name: 'TPM'})).toBeInTheDocument();
  544. expect(screen.getByTestId('tpm-summary-value')).toHaveTextContent('100%');
  545. });
  546. it('fetches transaction threshold', function () {
  547. const {organization, router, routerContext} = initializeData();
  548. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  549. url: '/organizations/org-slug/project-transaction-threshold-override/',
  550. method: 'GET',
  551. body: {
  552. threshold: '800',
  553. metric: 'lcp',
  554. },
  555. });
  556. const getProjectThresholdMock = MockApiClient.addMockResponse({
  557. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  558. method: 'GET',
  559. body: {
  560. threshold: '200',
  561. metric: 'duration',
  562. },
  563. });
  564. render(<TestComponent router={router} location={router.location} />, {
  565. context: routerContext,
  566. organization,
  567. });
  568. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  569. expect(getProjectThresholdMock).not.toHaveBeenCalled();
  570. });
  571. it('fetches project transaction threshdold', async function () {
  572. const {organization, router, routerContext} = initializeData();
  573. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  574. url: '/organizations/org-slug/project-transaction-threshold-override/',
  575. method: 'GET',
  576. statusCode: 404,
  577. });
  578. const getProjectThresholdMock = MockApiClient.addMockResponse({
  579. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  580. method: 'GET',
  581. body: {
  582. threshold: '200',
  583. metric: 'duration',
  584. },
  585. });
  586. render(<TestComponent router={router} location={router.location} />, {
  587. context: routerContext,
  588. organization,
  589. });
  590. await screen.findByText('Transaction Summary');
  591. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  592. expect(getProjectThresholdMock).toHaveBeenCalledTimes(1);
  593. });
  594. it('triggers a navigation on search', async function () {
  595. const {organization, router, routerContext} = initializeData();
  596. render(<TestComponent router={router} location={router.location} />, {
  597. context: routerContext,
  598. organization,
  599. });
  600. // Fill out the search box, and submit it.
  601. await userEvent.type(
  602. screen.getByLabelText('Search events'),
  603. 'user.email:uhoh*{enter}'
  604. );
  605. // Check the navigation.
  606. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  607. expect(browserHistory.push).toHaveBeenCalledWith({
  608. pathname: undefined,
  609. query: {
  610. transaction: '/performance',
  611. project: '2',
  612. statsPeriod: '14d',
  613. query: 'user.email:uhoh*',
  614. transactionCursor: '1:0:0',
  615. },
  616. });
  617. });
  618. it('can mark a transaction as key', async function () {
  619. const {organization, router, routerContext} = initializeData();
  620. render(<TestComponent router={router} location={router.location} />, {
  621. context: routerContext,
  622. organization,
  623. });
  624. const mockUpdate = MockApiClient.addMockResponse({
  625. url: `/organizations/org-slug/key-transactions/`,
  626. method: 'POST',
  627. body: {},
  628. });
  629. await screen.findByRole('button', {name: 'Star for Team'});
  630. // Click the key transaction button
  631. await userEvent.click(screen.getByRole('button', {name: 'Star for Team'}));
  632. await userEvent.click(screen.getByText('team1'));
  633. // Ensure request was made.
  634. expect(mockUpdate).toHaveBeenCalled();
  635. });
  636. it('triggers a navigation on transaction filter', async function () {
  637. const {organization, router, routerContext} = initializeData();
  638. render(<TestComponent router={router} location={router.location} />, {
  639. context: routerContext,
  640. organization,
  641. });
  642. await screen.findByText('Transaction Summary');
  643. await waitFor(() => {
  644. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  645. });
  646. // Open the transaction filter dropdown
  647. await userEvent.click(
  648. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  649. );
  650. await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]);
  651. // Check the navigation.
  652. expect(browserHistory.push).toHaveBeenCalledWith({
  653. pathname: undefined,
  654. query: {
  655. transaction: '/performance',
  656. project: '2',
  657. showTransactions: 'slow',
  658. transactionCursor: undefined,
  659. },
  660. });
  661. });
  662. it('renders pagination buttons', async function () {
  663. const {organization, router, routerContext} = initializeData();
  664. render(<TestComponent router={router} location={router.location} />, {
  665. context: routerContext,
  666. organization,
  667. });
  668. await screen.findByText('Transaction Summary');
  669. expect(await screen.findByLabelText('Previous')).toBeInTheDocument();
  670. // Click the 'next' button
  671. await userEvent.click(screen.getByLabelText('Next'));
  672. // Check the navigation.
  673. expect(browserHistory.push).toHaveBeenCalledWith({
  674. pathname: undefined,
  675. query: {
  676. transaction: '/performance',
  677. project: '2',
  678. transactionCursor: '2:0:0',
  679. },
  680. });
  681. });
  682. it('forwards conditions to related issues', async function () {
  683. const issueGet = MockApiClient.addMockResponse({
  684. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  685. body: [],
  686. });
  687. const {organization, router, routerContext} = initializeData({
  688. query: {query: 'tag:value'},
  689. });
  690. render(<TestComponent router={router} location={router.location} />, {
  691. context: routerContext,
  692. organization,
  693. });
  694. await screen.findByText('Transaction Summary');
  695. expect(issueGet).toHaveBeenCalled();
  696. });
  697. it('does not forward event type to related issues', async function () {
  698. const issueGet = MockApiClient.addMockResponse({
  699. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  700. body: [],
  701. match: [
  702. (_, options) => {
  703. // event.type must NOT be in the query params
  704. return !options.query?.query?.includes('event.type');
  705. },
  706. ],
  707. });
  708. const {organization, router, routerContext} = initializeData({
  709. query: {query: 'tag:value event.type:transaction'},
  710. });
  711. render(<TestComponent router={router} location={router.location} />, {
  712. context: routerContext,
  713. organization,
  714. });
  715. await screen.findByText('Transaction Summary');
  716. expect(issueGet).toHaveBeenCalled();
  717. });
  718. it('renders the suspect spans table if the feature is enabled', async function () {
  719. MockApiClient.addMockResponse({
  720. url: '/organizations/org-slug/events-spans-performance/',
  721. body: [],
  722. });
  723. const {organization, router, routerContext} = initializeData({});
  724. render(<TestComponent router={router} location={router.location} />, {
  725. context: routerContext,
  726. organization,
  727. });
  728. expect(await screen.findByText('Suspect Spans')).toBeInTheDocument();
  729. });
  730. it('adds search condition on transaction status when clicking on status breakdown', async function () {
  731. const {organization, router, routerContext} = initializeData();
  732. render(<TestComponent router={router} location={router.location} />, {
  733. context: routerContext,
  734. organization,
  735. });
  736. await screen.findByTestId('status-ok');
  737. await userEvent.click(screen.getByTestId('status-ok'));
  738. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  739. expect(browserHistory.push).toHaveBeenCalledWith(
  740. expect.objectContaining({
  741. query: expect.objectContaining({
  742. query: expect.stringContaining('transaction.status:ok'),
  743. }),
  744. })
  745. );
  746. });
  747. it('appends tag value to existing query when clicked', async function () {
  748. const {organization, router, routerContext} = initializeData();
  749. render(<TestComponent router={router} location={router.location} />, {
  750. context: routerContext,
  751. organization,
  752. });
  753. await screen.findByText('Tag Summary');
  754. await userEvent.click(
  755. await screen.findByLabelText(
  756. 'environment, dev, 100% of all events. View events with this tag value.'
  757. )
  758. );
  759. await userEvent.click(
  760. await screen.findByLabelText(
  761. 'foo, bar, 100% of all events. View events with this tag value.'
  762. )
  763. );
  764. await userEvent.click(
  765. await screen.findByLabelText(
  766. 'user, id:100, 100% of all events. View events with this tag value.'
  767. )
  768. );
  769. expect(router.push).toHaveBeenCalledTimes(3);
  770. expect(router.push).toHaveBeenNthCalledWith(1, {
  771. query: {
  772. project: '2',
  773. query: 'tags[environment]:dev',
  774. transaction: '/performance',
  775. transactionCursor: '1:0:0',
  776. },
  777. });
  778. expect(router.push).toHaveBeenNthCalledWith(2, {
  779. query: {
  780. project: '2',
  781. query: 'foo:bar',
  782. transaction: '/performance',
  783. transactionCursor: '1:0:0',
  784. },
  785. });
  786. expect(router.push).toHaveBeenNthCalledWith(3, {
  787. query: {
  788. project: '2',
  789. query: 'user:"id:100"',
  790. transaction: '/performance',
  791. transactionCursor: '1:0:0',
  792. },
  793. });
  794. });
  795. });
  796. describe('with events', function () {
  797. it('renders basic UI elements', async function () {
  798. const {organization, router, routerContext} = initializeData();
  799. render(<TestComponent router={router} location={router.location} />, {
  800. context: routerContext,
  801. organization,
  802. });
  803. // It shows the header
  804. await screen.findByText('Transaction Summary');
  805. expect(screen.getByRole('heading', {name: '/performance'})).toBeInTheDocument();
  806. // It shows a chart
  807. expect(
  808. screen.getByRole('button', {name: 'Display Duration Breakdown'})
  809. ).toBeInTheDocument();
  810. // It shows a searchbar
  811. expect(screen.getByLabelText('Search events')).toBeInTheDocument();
  812. // It shows a table
  813. expect(screen.getByTestId('transactions-table')).toBeInTheDocument();
  814. // Ensure open in discover button exists.
  815. expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument();
  816. // Ensure open issues button exists.
  817. expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument();
  818. // Ensure transaction filter button exists
  819. expect(
  820. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  821. ).toBeInTheDocument();
  822. // Ensure create alert from discover is hidden without metric alert
  823. expect(
  824. screen.queryByRole('button', {name: 'Create Alert'})
  825. ).not.toBeInTheDocument();
  826. // Ensure status breakdown exists
  827. expect(screen.getByText('Status Breakdown')).toBeInTheDocument();
  828. });
  829. it('renders feature flagged UI elements', function () {
  830. const {organization, router, routerContext} = initializeData({
  831. features: ['incidents'],
  832. });
  833. render(<TestComponent router={router} location={router.location} />, {
  834. context: routerContext,
  835. organization,
  836. });
  837. // Ensure create alert from discover is shown with metric alerts
  838. expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument();
  839. });
  840. it('renders Web Vitals widget', async function () {
  841. const {organization, router, routerContext} = initializeData({
  842. project: TestStubs.Project({teams, platform: 'javascript'}),
  843. query: {
  844. query:
  845. 'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/',
  846. },
  847. });
  848. render(<TestComponent router={router} location={router.location} />, {
  849. context: routerContext,
  850. organization,
  851. });
  852. // It renders the web vitals widget
  853. await screen.findByRole('heading', {name: 'Web Vitals'});
  854. const vitalStatues = screen.getAllByTestId('vital-status');
  855. expect(vitalStatues).toHaveLength(3);
  856. expect(vitalStatues[0]).toHaveTextContent('31%');
  857. expect(vitalStatues[1]).toHaveTextContent('65%');
  858. expect(vitalStatues[2]).toHaveTextContent('3%');
  859. });
  860. it('renders sidebar widgets', async function () {
  861. const {organization, router, routerContext} = initializeData({});
  862. render(<TestComponent router={router} location={router.location} />, {
  863. context: routerContext,
  864. organization,
  865. });
  866. // Renders Apdex widget
  867. await screen.findByRole('heading', {name: 'Apdex'});
  868. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  869. // Renders Failure Rate widget
  870. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  871. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  872. // Renders TPM widget
  873. expect(screen.getByRole('heading', {name: 'TPM'})).toBeInTheDocument();
  874. expect(screen.getByTestId('tpm-summary-value')).toHaveTextContent('100%');
  875. });
  876. it('fetches transaction threshold', function () {
  877. const {organization, router, routerContext} = initializeData();
  878. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  879. url: '/organizations/org-slug/project-transaction-threshold-override/',
  880. method: 'GET',
  881. body: {
  882. threshold: '800',
  883. metric: 'lcp',
  884. },
  885. });
  886. const getProjectThresholdMock = MockApiClient.addMockResponse({
  887. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  888. method: 'GET',
  889. body: {
  890. threshold: '200',
  891. metric: 'duration',
  892. },
  893. });
  894. render(<TestComponent router={router} location={router.location} />, {
  895. context: routerContext,
  896. organization,
  897. });
  898. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  899. expect(getProjectThresholdMock).not.toHaveBeenCalled();
  900. });
  901. it('fetches project transaction threshdold', async function () {
  902. const {organization, router, routerContext} = initializeData();
  903. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  904. url: '/organizations/org-slug/project-transaction-threshold-override/',
  905. method: 'GET',
  906. statusCode: 404,
  907. });
  908. const getProjectThresholdMock = MockApiClient.addMockResponse({
  909. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  910. method: 'GET',
  911. body: {
  912. threshold: '200',
  913. metric: 'duration',
  914. },
  915. });
  916. render(<TestComponent router={router} location={router.location} />, {
  917. context: routerContext,
  918. organization,
  919. });
  920. await screen.findByText('Transaction Summary');
  921. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  922. expect(getProjectThresholdMock).toHaveBeenCalledTimes(1);
  923. });
  924. it('triggers a navigation on search', async function () {
  925. const {organization, router, routerContext} = initializeData();
  926. render(<TestComponent router={router} location={router.location} />, {
  927. context: routerContext,
  928. organization,
  929. });
  930. // Fill out the search box, and submit it.
  931. await userEvent.type(
  932. screen.getByLabelText('Search events'),
  933. 'user.email:uhoh*{enter}'
  934. );
  935. // Check the navigation.
  936. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  937. expect(browserHistory.push).toHaveBeenCalledWith({
  938. pathname: undefined,
  939. query: {
  940. transaction: '/performance',
  941. project: '2',
  942. statsPeriod: '14d',
  943. query: 'user.email:uhoh*',
  944. transactionCursor: '1:0:0',
  945. },
  946. });
  947. });
  948. it('can mark a transaction as key', async function () {
  949. const {organization, router, routerContext} = initializeData();
  950. render(<TestComponent router={router} location={router.location} />, {
  951. context: routerContext,
  952. organization,
  953. });
  954. const mockUpdate = MockApiClient.addMockResponse({
  955. url: `/organizations/org-slug/key-transactions/`,
  956. method: 'POST',
  957. body: {},
  958. });
  959. await screen.findByRole('button', {name: 'Star for Team'});
  960. // Click the key transaction button
  961. await userEvent.click(screen.getByRole('button', {name: 'Star for Team'}));
  962. await userEvent.click(screen.getByText('team1'));
  963. // Ensure request was made.
  964. expect(mockUpdate).toHaveBeenCalled();
  965. });
  966. it('triggers a navigation on transaction filter', async function () {
  967. const {organization, router, routerContext} = initializeData();
  968. render(<TestComponent router={router} location={router.location} />, {
  969. context: routerContext,
  970. organization,
  971. });
  972. await screen.findByText('Transaction Summary');
  973. await waitFor(() => {
  974. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  975. });
  976. // Open the transaction filter dropdown
  977. await userEvent.click(
  978. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  979. );
  980. await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]);
  981. // Check the navigation.
  982. expect(browserHistory.push).toHaveBeenCalledWith({
  983. pathname: undefined,
  984. query: {
  985. transaction: '/performance',
  986. project: '2',
  987. showTransactions: 'slow',
  988. transactionCursor: undefined,
  989. },
  990. });
  991. });
  992. it('renders pagination buttons', async function () {
  993. const {organization, router, routerContext} = initializeData();
  994. render(<TestComponent router={router} location={router.location} />, {
  995. context: routerContext,
  996. organization,
  997. });
  998. await screen.findByText('Transaction Summary');
  999. expect(await screen.findByLabelText('Previous')).toBeInTheDocument();
  1000. // Click the 'next' button
  1001. await userEvent.click(screen.getByLabelText('Next'));
  1002. // Check the navigation.
  1003. expect(browserHistory.push).toHaveBeenCalledWith({
  1004. pathname: undefined,
  1005. query: {
  1006. transaction: '/performance',
  1007. project: '2',
  1008. transactionCursor: '2:0:0',
  1009. },
  1010. });
  1011. });
  1012. it('forwards conditions to related issues', async function () {
  1013. const issueGet = MockApiClient.addMockResponse({
  1014. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  1015. body: [],
  1016. });
  1017. const {organization, router, routerContext} = initializeData({
  1018. query: {query: 'tag:value'},
  1019. });
  1020. render(<TestComponent router={router} location={router.location} />, {
  1021. context: routerContext,
  1022. organization,
  1023. });
  1024. await screen.findByText('Transaction Summary');
  1025. expect(issueGet).toHaveBeenCalled();
  1026. });
  1027. it('does not forward event type to related issues', async function () {
  1028. const issueGet = MockApiClient.addMockResponse({
  1029. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  1030. body: [],
  1031. match: [
  1032. (_, options) => {
  1033. // event.type must NOT be in the query params
  1034. return !options.query?.query?.includes('event.type');
  1035. },
  1036. ],
  1037. });
  1038. const {organization, router, routerContext} = initializeData({
  1039. query: {query: 'tag:value event.type:transaction'},
  1040. });
  1041. render(<TestComponent router={router} location={router.location} />, {
  1042. context: routerContext,
  1043. organization,
  1044. });
  1045. await screen.findByText('Transaction Summary');
  1046. expect(issueGet).toHaveBeenCalled();
  1047. });
  1048. it('renders the suspect spans table if the feature is enabled', async function () {
  1049. MockApiClient.addMockResponse({
  1050. url: '/organizations/org-slug/events-spans-performance/',
  1051. body: [],
  1052. });
  1053. const {organization, router, routerContext} = initializeData({
  1054. features: ['performance-suspect-spans-view'],
  1055. });
  1056. render(<TestComponent router={router} location={router.location} />, {
  1057. context: routerContext,
  1058. organization,
  1059. });
  1060. expect(await screen.findByText('Suspect Spans')).toBeInTheDocument();
  1061. });
  1062. it('adds search condition on transaction status when clicking on status breakdown', async function () {
  1063. const {organization, router, routerContext} = initializeData();
  1064. render(<TestComponent router={router} location={router.location} />, {
  1065. context: routerContext,
  1066. organization,
  1067. });
  1068. await screen.findByTestId('status-ok');
  1069. await userEvent.click(screen.getByTestId('status-ok'));
  1070. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  1071. expect(browserHistory.push).toHaveBeenCalledWith(
  1072. expect.objectContaining({
  1073. query: expect.objectContaining({
  1074. query: expect.stringContaining('transaction.status:ok'),
  1075. }),
  1076. })
  1077. );
  1078. });
  1079. it('appends tag value to existing query when clicked', async function () {
  1080. const {organization, router, routerContext} = initializeData();
  1081. render(<TestComponent router={router} location={router.location} />, {
  1082. context: routerContext,
  1083. organization,
  1084. });
  1085. await screen.findByText('Tag Summary');
  1086. await userEvent.click(
  1087. await screen.findByLabelText(
  1088. 'environment, dev, 100% of all events. View events with this tag value.'
  1089. )
  1090. );
  1091. await userEvent.click(
  1092. await screen.findByLabelText(
  1093. 'foo, bar, 100% of all events. View events with this tag value.'
  1094. )
  1095. );
  1096. expect(router.push).toHaveBeenCalledTimes(2);
  1097. expect(router.push).toHaveBeenNthCalledWith(1, {
  1098. query: {
  1099. project: '2',
  1100. query: 'tags[environment]:dev',
  1101. transaction: '/performance',
  1102. transactionCursor: '1:0:0',
  1103. },
  1104. });
  1105. expect(router.push).toHaveBeenNthCalledWith(2, {
  1106. query: {
  1107. project: '2',
  1108. query: 'foo:bar',
  1109. transaction: '/performance',
  1110. transactionCursor: '1:0:0',
  1111. },
  1112. });
  1113. });
  1114. });
  1115. });