index.spec.tsx 37 KB

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