widgetContainer.spec.tsx 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  1. import {
  2. initializeData as _initializeData,
  3. InitializeDataSettings,
  4. } from 'sentry-test/performance/initializePerformanceData';
  5. import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  6. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  7. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  8. import {
  9. PageErrorAlert,
  10. PageErrorProvider,
  11. } from 'sentry/utils/performance/contexts/pageError';
  12. import {PerformanceDisplayProvider} from 'sentry/utils/performance/contexts/performanceDisplayContext';
  13. import {OrganizationContext} from 'sentry/views/organizationContext';
  14. import WidgetContainer from 'sentry/views/performance/landing/widgets/components/widgetContainer';
  15. import {PerformanceWidgetSetting} from 'sentry/views/performance/landing/widgets/widgetDefinitions';
  16. import {ProjectPerformanceType} from 'sentry/views/performance/utils';
  17. import {QUERY_LIMIT_PARAM} from '../utils';
  18. const initializeData = (query = {}, rest: InitializeDataSettings = {}) => {
  19. const data = _initializeData({
  20. query: {statsPeriod: '7d', environment: ['prod'], project: [-42], ...query},
  21. ...rest,
  22. });
  23. data.eventView.additionalConditions.addFilterValues('transaction.op', ['pageload']);
  24. return data;
  25. };
  26. function WrappedComponent({data, withStaticFilters = false, ...rest}) {
  27. return (
  28. <OrganizationContext.Provider value={data.organization}>
  29. <MetricsCardinalityProvider
  30. location={data.router.location}
  31. organization={data.organization}
  32. >
  33. <MEPSettingProvider forceTransactions>
  34. <PerformanceDisplayProvider
  35. value={{performanceType: ProjectPerformanceType.ANY}}
  36. >
  37. <WidgetContainer
  38. chartHeight={100}
  39. allowedCharts={[
  40. PerformanceWidgetSetting.TPM_AREA,
  41. PerformanceWidgetSetting.FAILURE_RATE_AREA,
  42. PerformanceWidgetSetting.USER_MISERY_AREA,
  43. PerformanceWidgetSetting.DURATION_HISTOGRAM,
  44. ]}
  45. rowChartSettings={[]}
  46. withStaticFilters={withStaticFilters}
  47. forceDefaultChartSetting
  48. {...data}
  49. {...rest}
  50. />
  51. </PerformanceDisplayProvider>
  52. </MEPSettingProvider>
  53. </MetricsCardinalityProvider>
  54. </OrganizationContext.Provider>
  55. );
  56. }
  57. const issuesPredicate = (url, options) =>
  58. url.includes('events') && options.query?.query.includes('error');
  59. describe('Performance > Widgets > WidgetContainer', function () {
  60. let wrapper;
  61. let eventStatsMock;
  62. let eventsTrendsStats;
  63. let eventsMock;
  64. let issuesListMock;
  65. beforeEach(function () {
  66. eventStatsMock = MockApiClient.addMockResponse({
  67. method: 'GET',
  68. url: `/organizations/org-slug/events-stats/`,
  69. body: [],
  70. });
  71. eventsMock = MockApiClient.addMockResponse({
  72. method: 'GET',
  73. url: `/organizations/org-slug/events/`,
  74. body: {
  75. data: [{}],
  76. meta: {},
  77. },
  78. match: [(...args) => !issuesPredicate(...args)],
  79. });
  80. issuesListMock = MockApiClient.addMockResponse({
  81. method: 'GET',
  82. url: `/organizations/org-slug/events/`,
  83. body: {
  84. data: [
  85. {
  86. 'issue.id': 123,
  87. transaction: '/issue/:id/',
  88. title: 'Error: Something is broken.',
  89. 'project.id': 1,
  90. count: 3100,
  91. issue: 'JAVASCRIPT-ABCD',
  92. },
  93. ],
  94. },
  95. match: [(...args) => issuesPredicate(...args)],
  96. });
  97. eventsTrendsStats = MockApiClient.addMockResponse({
  98. method: 'GET',
  99. url: '/organizations/org-slug/events-trends-stats/',
  100. body: [],
  101. });
  102. MockApiClient.addMockResponse({
  103. method: 'GET',
  104. url: `/organizations/org-slug/metrics-compatibility/`,
  105. body: [],
  106. });
  107. MockApiClient.addMockResponse({
  108. method: 'GET',
  109. url: `/organizations/org-slug/metrics-compatibility-sums/`,
  110. body: [],
  111. });
  112. });
  113. afterEach(function () {
  114. if (wrapper) {
  115. wrapper.unmount();
  116. wrapper = undefined;
  117. }
  118. });
  119. it('Check requests when changing widget props', function () {
  120. const data = initializeData();
  121. wrapper = render(
  122. <WrappedComponent
  123. data={data}
  124. defaultChartSetting={PerformanceWidgetSetting.TPM_AREA}
  125. />
  126. );
  127. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  128. // Change eventView reference
  129. data.eventView = data.eventView.clone();
  130. wrapper.rerender(
  131. <WrappedComponent
  132. data={data}
  133. defaultChartSetting={PerformanceWidgetSetting.TPM_AREA}
  134. />
  135. );
  136. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  137. // Change eventView statsperiod
  138. const modifiedData = initializeData({
  139. statsPeriod: '14d',
  140. });
  141. wrapper.rerender(
  142. <WrappedComponent
  143. data={modifiedData}
  144. defaultChartSetting={PerformanceWidgetSetting.TPM_AREA}
  145. />
  146. );
  147. expect(eventStatsMock).toHaveBeenCalledTimes(2);
  148. expect(eventStatsMock).toHaveBeenNthCalledWith(
  149. 2,
  150. expect.anything(),
  151. expect.objectContaining({
  152. query: expect.objectContaining({
  153. interval: '1h',
  154. partial: '1',
  155. query: 'transaction.op:pageload',
  156. statsPeriod: '28d',
  157. yAxis: 'tpm()',
  158. }),
  159. })
  160. );
  161. });
  162. it('Check requests when changing widget props for GenericDiscoverQuery based widget', function () {
  163. const data = initializeData();
  164. wrapper = render(
  165. <MEPSettingProvider forceTransactions>
  166. <WrappedComponent
  167. data={data}
  168. defaultChartSetting={PerformanceWidgetSetting.MOST_IMPROVED}
  169. />
  170. </MEPSettingProvider>
  171. );
  172. expect(eventsTrendsStats).toHaveBeenCalledTimes(1);
  173. // Change eventView reference
  174. data.eventView = data.eventView.clone();
  175. wrapper.rerender(
  176. <MEPSettingProvider forceTransactions>
  177. <WrappedComponent
  178. data={data}
  179. defaultChartSetting={PerformanceWidgetSetting.MOST_IMPROVED}
  180. />
  181. </MEPSettingProvider>
  182. );
  183. expect(eventsTrendsStats).toHaveBeenCalledTimes(1);
  184. // Change eventView statsperiod
  185. const modifiedData = initializeData({
  186. statsPeriod: '14d',
  187. });
  188. wrapper.rerender(
  189. <MEPSettingProvider forceTransactions>
  190. <WrappedComponent
  191. data={modifiedData}
  192. defaultChartSetting={PerformanceWidgetSetting.MOST_IMPROVED}
  193. />
  194. </MEPSettingProvider>
  195. );
  196. expect(eventsTrendsStats).toHaveBeenCalledTimes(2);
  197. expect(eventsTrendsStats).toHaveBeenNthCalledWith(
  198. 2,
  199. expect.anything(),
  200. expect.objectContaining({
  201. query: expect.objectContaining({
  202. cursor: '0:0:1',
  203. environment: ['prod'],
  204. field: ['transaction', 'project'],
  205. interval: undefined,
  206. middle: undefined,
  207. noPagination: true,
  208. per_page: QUERY_LIMIT_PARAM,
  209. project: ['-42'],
  210. query:
  211. 'transaction.op:pageload tpm():>0.01 count_percentage():>0.25 count_percentage():<4 trend_percentage():>0% confidence():>6',
  212. sort: 'trend_percentage()',
  213. statsPeriod: '14d',
  214. trendFunction: 'p50(transaction.duration)',
  215. trendType: 'improved',
  216. }),
  217. })
  218. );
  219. });
  220. it('should call PageError Provider when errors are present', async function () {
  221. const data = initializeData();
  222. eventStatsMock = MockApiClient.addMockResponse({
  223. method: 'GET',
  224. url: `/organizations/org-slug/events-stats/`,
  225. statusCode: 400,
  226. body: {
  227. detail: 'Request did not work :(',
  228. },
  229. });
  230. wrapper = render(
  231. <PageErrorProvider>
  232. <PageErrorAlert />
  233. <WrappedComponent
  234. data={data}
  235. defaultChartSetting={PerformanceWidgetSetting.TPM_AREA}
  236. />
  237. </PageErrorProvider>
  238. );
  239. // Provider update is after request promise.
  240. await act(async () => {});
  241. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  242. 'Transactions Per Minute'
  243. );
  244. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  245. expect(await screen.findByTestId('page-error-alert')).toHaveTextContent(
  246. 'Request did not work :('
  247. );
  248. });
  249. it('TPM Widget', async function () {
  250. const data = initializeData();
  251. wrapper = render(
  252. <WrappedComponent
  253. data={data}
  254. defaultChartSetting={PerformanceWidgetSetting.TPM_AREA}
  255. />
  256. );
  257. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  258. 'Transactions Per Minute'
  259. );
  260. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  261. expect(eventStatsMock).toHaveBeenNthCalledWith(
  262. 1,
  263. expect.anything(),
  264. expect.objectContaining({
  265. query: expect.objectContaining({
  266. interval: '1h',
  267. partial: '1',
  268. query: 'transaction.op:pageload',
  269. statsPeriod: '14d',
  270. yAxis: 'tpm()',
  271. }),
  272. })
  273. );
  274. });
  275. it('Failure Rate Widget', async function () {
  276. const data = initializeData();
  277. wrapper = render(
  278. <WrappedComponent
  279. data={data}
  280. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  281. />
  282. );
  283. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  284. 'Failure Rate'
  285. );
  286. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  287. expect(eventStatsMock).toHaveBeenNthCalledWith(
  288. 1,
  289. expect.anything(),
  290. expect.objectContaining({
  291. query: expect.objectContaining({
  292. interval: '1h',
  293. partial: '1',
  294. query: 'transaction.op:pageload',
  295. statsPeriod: '14d',
  296. yAxis: 'failure_rate()',
  297. }),
  298. })
  299. );
  300. });
  301. it('Widget with MEP enabled and metric meta set to true', async function () {
  302. const data = initializeData(
  303. {},
  304. {
  305. features: ['performance-use-metrics'],
  306. }
  307. );
  308. eventStatsMock = MockApiClient.addMockResponse({
  309. method: 'GET',
  310. url: `/organizations/org-slug/events-stats/`,
  311. body: {
  312. data: [],
  313. isMetricsData: true,
  314. },
  315. });
  316. eventsMock = MockApiClient.addMockResponse({
  317. method: 'GET',
  318. url: `/organizations/org-slug/events/`,
  319. body: {
  320. data: [{}],
  321. meta: {isMetricsData: true},
  322. },
  323. });
  324. wrapper = render(
  325. <WrappedComponent
  326. data={data}
  327. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  328. />
  329. );
  330. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  331. expect(eventStatsMock).toHaveBeenNthCalledWith(
  332. 1,
  333. expect.anything(),
  334. expect.objectContaining({
  335. query: expect.objectContaining({dataset: 'metrics'}),
  336. })
  337. );
  338. expect(eventsMock).toHaveBeenCalledTimes(1);
  339. expect(eventsMock).toHaveBeenCalledWith(
  340. expect.anything(),
  341. expect.objectContaining({
  342. query: expect.objectContaining({dataset: 'metrics'}),
  343. })
  344. );
  345. expect(await screen.findByTestId('has-metrics-data-tag')).toHaveTextContent(
  346. 'processed'
  347. );
  348. });
  349. it('Widget with MEP enabled and metric meta set to undefined', async function () {
  350. const data = initializeData(
  351. {},
  352. {
  353. features: ['performance-use-metrics'],
  354. }
  355. );
  356. eventStatsMock = MockApiClient.addMockResponse({
  357. method: 'GET',
  358. url: `/organizations/org-slug/events-stats/`,
  359. body: {
  360. data: [],
  361. isMetricsData: undefined,
  362. },
  363. });
  364. wrapper = render(
  365. <WrappedComponent
  366. data={data}
  367. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  368. />
  369. );
  370. expect(await screen.findByTestId('no-metrics-data-tag')).toBeInTheDocument();
  371. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  372. expect(eventStatsMock).toHaveBeenNthCalledWith(
  373. 1,
  374. expect.anything(),
  375. expect.objectContaining({
  376. query: expect.objectContaining({dataset: 'metrics'}),
  377. })
  378. );
  379. });
  380. it('Widget with MEP enabled and metric meta set to false', async function () {
  381. const data = initializeData(
  382. {},
  383. {
  384. features: ['performance-use-metrics'],
  385. }
  386. );
  387. eventStatsMock = MockApiClient.addMockResponse({
  388. method: 'GET',
  389. url: `/organizations/org-slug/events-stats/`,
  390. body: {
  391. data: [],
  392. isMetricsData: false,
  393. },
  394. });
  395. eventsMock = MockApiClient.addMockResponse({
  396. method: 'GET',
  397. url: `/organizations/org-slug/events/`,
  398. body: {
  399. data: [{}],
  400. meta: {isMetricsData: false},
  401. },
  402. });
  403. wrapper = render(
  404. <WrappedComponent
  405. data={data}
  406. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  407. />
  408. );
  409. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  410. expect(eventStatsMock).toHaveBeenNthCalledWith(
  411. 1,
  412. expect.anything(),
  413. expect.objectContaining({
  414. query: expect.objectContaining({dataset: 'metrics'}),
  415. })
  416. );
  417. expect(eventsMock).toHaveBeenCalledTimes(1);
  418. expect(eventsMock).toHaveBeenCalledWith(
  419. expect.anything(),
  420. expect.objectContaining({
  421. query: expect.objectContaining({dataset: 'metrics'}),
  422. })
  423. );
  424. expect(await screen.findByTestId('has-metrics-data-tag')).toHaveTextContent(
  425. 'indexed'
  426. );
  427. });
  428. it('User misery Widget', async function () {
  429. const data = initializeData();
  430. wrapper = render(
  431. <WrappedComponent
  432. data={data}
  433. defaultChartSetting={PerformanceWidgetSetting.USER_MISERY_AREA}
  434. />
  435. );
  436. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  437. 'User Misery'
  438. );
  439. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  440. expect(eventStatsMock).toHaveBeenNthCalledWith(
  441. 1,
  442. expect.anything(),
  443. expect.objectContaining({
  444. query: expect.objectContaining({
  445. interval: '1h',
  446. partial: '1',
  447. query: 'transaction.op:pageload',
  448. statsPeriod: '14d',
  449. yAxis: 'user_misery()',
  450. }),
  451. })
  452. );
  453. });
  454. it('Worst LCP widget', async function () {
  455. const data = initializeData();
  456. wrapper = render(
  457. <WrappedComponent
  458. data={data}
  459. defaultChartSetting={PerformanceWidgetSetting.WORST_LCP_VITALS}
  460. />
  461. );
  462. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  463. 'Worst LCP Web Vitals'
  464. );
  465. expect(await screen.findByTestId('view-all-button')).toHaveTextContent('View All');
  466. expect(eventsMock).toHaveBeenCalledTimes(1);
  467. expect(eventsMock).toHaveBeenNthCalledWith(
  468. 1,
  469. expect.anything(),
  470. expect.objectContaining({
  471. query: expect.objectContaining({
  472. environment: ['prod'],
  473. field: [
  474. 'transaction',
  475. 'title',
  476. 'project.id',
  477. 'count_web_vitals(measurements.lcp, poor)',
  478. 'count_web_vitals(measurements.lcp, meh)',
  479. 'count_web_vitals(measurements.lcp, good)',
  480. ],
  481. per_page: 4,
  482. project: ['-42'],
  483. query: 'transaction.op:pageload',
  484. sort: '-count_web_vitals(measurements.lcp, poor)',
  485. statsPeriod: '7d',
  486. }),
  487. })
  488. );
  489. });
  490. it('Worst LCP widget - MEP', async function () {
  491. const data = initializeData(
  492. {},
  493. {
  494. features: ['performance-use-metrics'],
  495. }
  496. );
  497. wrapper = render(
  498. <WrappedComponent
  499. data={data}
  500. defaultChartSetting={PerformanceWidgetSetting.WORST_LCP_VITALS}
  501. />
  502. );
  503. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  504. 'Worst LCP Web Vitals'
  505. );
  506. expect(await screen.findByTestId('view-all-button')).toHaveTextContent('View All');
  507. expect(eventsMock).toHaveBeenCalledTimes(1);
  508. expect(eventsMock).toHaveBeenNthCalledWith(
  509. 1,
  510. expect.anything(),
  511. expect.objectContaining({
  512. query: expect.objectContaining({
  513. environment: ['prod'],
  514. field: [
  515. 'transaction',
  516. 'title',
  517. 'project.id',
  518. 'count_web_vitals(measurements.lcp, poor)',
  519. 'count_web_vitals(measurements.lcp, meh)',
  520. 'count_web_vitals(measurements.lcp, good)',
  521. ],
  522. per_page: 4,
  523. project: ['-42'],
  524. query: 'transaction.op:pageload !transaction:"<< unparameterized >>"',
  525. sort: '-count_web_vitals(measurements.lcp, poor)',
  526. statsPeriod: '7d',
  527. }),
  528. })
  529. );
  530. });
  531. it('Worst FCP widget', async function () {
  532. const data = initializeData();
  533. wrapper = render(
  534. <WrappedComponent
  535. data={data}
  536. defaultChartSetting={PerformanceWidgetSetting.WORST_FCP_VITALS}
  537. />
  538. );
  539. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  540. 'Worst FCP Web Vitals'
  541. );
  542. expect(await screen.findByTestId('view-all-button')).toHaveTextContent('View All');
  543. expect(eventsMock).toHaveBeenCalledTimes(1);
  544. expect(eventsMock).toHaveBeenNthCalledWith(
  545. 1,
  546. expect.anything(),
  547. expect.objectContaining({
  548. query: expect.objectContaining({
  549. environment: ['prod'],
  550. field: [
  551. 'transaction',
  552. 'title',
  553. 'project.id',
  554. 'count_web_vitals(measurements.fcp, poor)',
  555. 'count_web_vitals(measurements.fcp, meh)',
  556. 'count_web_vitals(measurements.fcp, good)',
  557. ],
  558. per_page: 4,
  559. project: ['-42'],
  560. query: 'transaction.op:pageload',
  561. sort: '-count_web_vitals(measurements.fcp, poor)',
  562. statsPeriod: '7d',
  563. }),
  564. })
  565. );
  566. });
  567. it('Worst FID widget', async function () {
  568. const data = initializeData();
  569. wrapper = render(
  570. <WrappedComponent
  571. data={data}
  572. defaultChartSetting={PerformanceWidgetSetting.WORST_FID_VITALS}
  573. />
  574. );
  575. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  576. 'Worst FID Web Vitals'
  577. );
  578. expect(await screen.findByTestId('view-all-button')).toHaveTextContent('View All');
  579. expect(eventsMock).toHaveBeenCalledTimes(1);
  580. expect(eventsMock).toHaveBeenNthCalledWith(
  581. 1,
  582. expect.anything(),
  583. expect.objectContaining({
  584. query: expect.objectContaining({
  585. environment: ['prod'],
  586. field: [
  587. 'transaction',
  588. 'title',
  589. 'project.id',
  590. 'count_web_vitals(measurements.fid, poor)',
  591. 'count_web_vitals(measurements.fid, meh)',
  592. 'count_web_vitals(measurements.fid, good)',
  593. ],
  594. per_page: 4,
  595. project: ['-42'],
  596. query: 'transaction.op:pageload',
  597. sort: '-count_web_vitals(measurements.fid, poor)',
  598. statsPeriod: '7d',
  599. }),
  600. })
  601. );
  602. });
  603. it('LCP Histogram Widget', async function () {
  604. const data = initializeData();
  605. wrapper = render(
  606. <WrappedComponent
  607. data={data}
  608. defaultChartSetting={PerformanceWidgetSetting.LCP_HISTOGRAM}
  609. />
  610. );
  611. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  612. 'LCP Distribution'
  613. );
  614. // TODO(k-fish): Add histogram mock
  615. });
  616. it('FCP Histogram Widget', async function () {
  617. const data = initializeData();
  618. wrapper = render(
  619. <WrappedComponent
  620. data={data}
  621. defaultChartSetting={PerformanceWidgetSetting.FCP_HISTOGRAM}
  622. />
  623. );
  624. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  625. 'FCP Distribution'
  626. );
  627. // TODO(k-fish): Add histogram mock
  628. });
  629. it('Most errors widget', async function () {
  630. const data = initializeData();
  631. wrapper = render(
  632. <WrappedComponent
  633. data={data}
  634. defaultChartSetting={PerformanceWidgetSetting.MOST_RELATED_ERRORS}
  635. />
  636. );
  637. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  638. 'Most Related Errors'
  639. );
  640. expect(eventsMock).toHaveBeenCalledTimes(1);
  641. expect(eventsMock).toHaveBeenNthCalledWith(
  642. 1,
  643. expect.anything(),
  644. expect.objectContaining({
  645. query: expect.objectContaining({
  646. environment: ['prod'],
  647. field: ['transaction', 'project.id', 'failure_count()'],
  648. per_page: QUERY_LIMIT_PARAM,
  649. project: ['-42'],
  650. query: 'transaction.op:pageload failure_count():>0',
  651. sort: '-failure_count()',
  652. statsPeriod: '7d',
  653. }),
  654. })
  655. );
  656. });
  657. it('Most related issues widget', async function () {
  658. const data = initializeData();
  659. wrapper = render(
  660. <WrappedComponent
  661. data={data}
  662. defaultChartSetting={PerformanceWidgetSetting.MOST_RELATED_ISSUES}
  663. />
  664. );
  665. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  666. 'Most Related Issues'
  667. );
  668. expect(issuesListMock).toHaveBeenCalledTimes(1);
  669. expect(issuesListMock).toHaveBeenNthCalledWith(
  670. 1,
  671. expect.anything(),
  672. expect.objectContaining({
  673. query: expect.objectContaining({
  674. environment: ['prod'],
  675. field: ['issue', 'transaction', 'title', 'project.id', 'count()'],
  676. per_page: QUERY_LIMIT_PARAM,
  677. project: ['-42'],
  678. query: 'event.type:error !tags[transaction]:"" count():>0',
  679. sort: '-count()',
  680. statsPeriod: '7d',
  681. }),
  682. })
  683. );
  684. });
  685. it('Switching from issues to errors widget', async function () {
  686. const data = initializeData();
  687. wrapper = render(
  688. <WrappedComponent
  689. data={data}
  690. defaultChartSetting={PerformanceWidgetSetting.MOST_RELATED_ISSUES}
  691. />
  692. );
  693. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  694. 'Most Related Issues'
  695. );
  696. expect(issuesListMock).toHaveBeenCalledTimes(1);
  697. wrapper.rerender(
  698. <WrappedComponent
  699. data={data}
  700. defaultChartSetting={PerformanceWidgetSetting.MOST_RELATED_ERRORS}
  701. />
  702. );
  703. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  704. 'Most Related Errors'
  705. );
  706. expect(eventsMock).toHaveBeenCalledTimes(1);
  707. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  708. });
  709. it('Most improved trends widget', async function () {
  710. const data = initializeData();
  711. wrapper = render(
  712. <MEPSettingProvider forceTransactions>
  713. <WrappedComponent
  714. data={data}
  715. defaultChartSetting={PerformanceWidgetSetting.MOST_IMPROVED}
  716. />
  717. </MEPSettingProvider>
  718. );
  719. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  720. 'Most Improved'
  721. );
  722. expect(eventsTrendsStats).toHaveBeenCalledTimes(1);
  723. expect(eventsTrendsStats).toHaveBeenNthCalledWith(
  724. 1,
  725. expect.anything(),
  726. expect.objectContaining({
  727. query: expect.objectContaining({
  728. environment: ['prod'],
  729. field: ['transaction', 'project'],
  730. interval: undefined,
  731. middle: undefined,
  732. per_page: QUERY_LIMIT_PARAM,
  733. project: ['-42'],
  734. query:
  735. 'transaction.op:pageload tpm():>0.01 count_percentage():>0.25 count_percentage():<4 trend_percentage():>0% confidence():>6',
  736. sort: 'trend_percentage()',
  737. statsPeriod: '7d',
  738. trendFunction: 'p50(transaction.duration)',
  739. trendType: 'improved',
  740. }),
  741. })
  742. );
  743. });
  744. it('Most time spent in db queries widget', async function () {
  745. const data = initializeData();
  746. wrapper = render(
  747. <MEPSettingProvider forceTransactions>
  748. <WrappedComponent
  749. data={data}
  750. defaultChartSetting={PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES}
  751. />
  752. </MEPSettingProvider>
  753. );
  754. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  755. 'Most Time-Consuming Queries'
  756. );
  757. expect(eventsMock).toHaveBeenCalledTimes(1);
  758. expect(eventsMock).toHaveBeenNthCalledWith(
  759. 1,
  760. expect.anything(),
  761. expect.objectContaining({
  762. query: expect.objectContaining({
  763. dataset: 'spansMetrics',
  764. environment: ['prod'],
  765. field: [
  766. 'span.op',
  767. 'span.group',
  768. 'project.id',
  769. 'span.description',
  770. 'sum(span.self_time)',
  771. 'avg(span.self_time)',
  772. 'time_spent_percentage()',
  773. ],
  774. per_page: QUERY_LIMIT_PARAM,
  775. project: ['-42'],
  776. query: 'has:span.description span.module:db transaction.op:pageload',
  777. sort: '-time_spent_percentage()',
  778. statsPeriod: '7d',
  779. }),
  780. })
  781. );
  782. });
  783. it('Most time consuming resources widget', async function () {
  784. const data = initializeData();
  785. wrapper = render(
  786. <MEPSettingProvider forceTransactions>
  787. <WrappedComponent
  788. data={data}
  789. defaultChartSetting={PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES}
  790. />
  791. </MEPSettingProvider>
  792. );
  793. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  794. 'Most Time Consuming Resources'
  795. );
  796. expect(eventsMock).toHaveBeenCalledTimes(1);
  797. expect(eventsMock).toHaveBeenNthCalledWith(
  798. 1,
  799. expect.anything(),
  800. expect.objectContaining({
  801. query: expect.objectContaining({
  802. dataset: 'spansMetrics',
  803. environment: ['prod'],
  804. field: [
  805. 'span.description',
  806. 'span.op',
  807. 'project.id',
  808. 'span.group',
  809. 'sum(span.self_time)',
  810. 'avg(span.self_time)',
  811. 'time_spent_percentage()',
  812. ],
  813. per_page: QUERY_LIMIT_PARAM,
  814. project: ['-42'],
  815. query:
  816. '!span.description:browser-extension://* resource.render_blocking_status:blocking ( span.op:resource.script OR file_extension:css OR file_extension:[woff,woff2,ttf,otf,eot] ) transaction.op:pageload',
  817. sort: '-time_spent_percentage()',
  818. statsPeriod: '7d',
  819. }),
  820. })
  821. );
  822. });
  823. it('Best Page Opportunities widget', async function () {
  824. const data = initializeData();
  825. wrapper = render(
  826. <MEPSettingProvider forceTransactions>
  827. <WrappedComponent
  828. data={data}
  829. defaultChartSetting={PerformanceWidgetSetting.HIGHEST_OPPORTUNITY_PAGES}
  830. />
  831. </MEPSettingProvider>
  832. );
  833. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  834. 'Best Page Opportunities'
  835. );
  836. expect(eventsMock).toHaveBeenCalledTimes(2);
  837. expect(eventsMock).toHaveBeenNthCalledWith(
  838. 1,
  839. expect.anything(),
  840. expect.objectContaining({
  841. query: expect.objectContaining({
  842. dataset: 'metrics',
  843. field: [
  844. 'p75(measurements.lcp)',
  845. 'p75(measurements.fcp)',
  846. 'p75(measurements.cls)',
  847. 'p75(measurements.ttfb)',
  848. 'p75(measurements.fid)',
  849. 'p75(transaction.duration)',
  850. 'count_web_vitals(measurements.lcp, any)',
  851. 'count_web_vitals(measurements.fcp, any)',
  852. 'count_web_vitals(measurements.cls, any)',
  853. 'count_web_vitals(measurements.fid, any)',
  854. 'count_web_vitals(measurements.ttfb, any)',
  855. 'count()',
  856. ],
  857. query: 'transaction.op:pageload',
  858. }),
  859. })
  860. );
  861. expect(eventsMock).toHaveBeenNthCalledWith(
  862. 2,
  863. expect.anything(),
  864. expect.objectContaining({
  865. query: expect.objectContaining({
  866. dataset: 'metrics',
  867. field: [
  868. 'transaction',
  869. 'transaction.op',
  870. 'p75(measurements.lcp)',
  871. 'p75(measurements.fcp)',
  872. 'p75(measurements.cls)',
  873. 'p75(measurements.ttfb)',
  874. 'p75(measurements.fid)',
  875. 'count_web_vitals(measurements.lcp, any)',
  876. 'count_web_vitals(measurements.fcp, any)',
  877. 'count_web_vitals(measurements.cls, any)',
  878. 'count_web_vitals(measurements.ttfb, any)',
  879. 'count_web_vitals(measurements.fid, any)',
  880. 'count()',
  881. ],
  882. per_page: 4,
  883. query: 'transaction.op:pageload',
  884. sort: '-count()',
  885. }),
  886. })
  887. );
  888. });
  889. it('Most regressed trends widget', async function () {
  890. const data = initializeData();
  891. wrapper = render(
  892. <WrappedComponent
  893. data={data}
  894. defaultChartSetting={PerformanceWidgetSetting.MOST_REGRESSED}
  895. />
  896. );
  897. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  898. 'Most Regressed'
  899. );
  900. expect(eventsTrendsStats).toHaveBeenCalledTimes(1);
  901. expect(eventsTrendsStats).toHaveBeenNthCalledWith(
  902. 1,
  903. expect.anything(),
  904. expect.objectContaining({
  905. query: expect.objectContaining({
  906. environment: ['prod'],
  907. field: ['transaction', 'project'],
  908. interval: undefined,
  909. middle: undefined,
  910. per_page: QUERY_LIMIT_PARAM,
  911. project: ['-42'],
  912. query:
  913. 'transaction.op:pageload tpm():>0.01 count_percentage():>0.25 count_percentage():<4 trend_percentage():>0% confidence():>6',
  914. sort: '-trend_percentage()',
  915. statsPeriod: '7d',
  916. trendFunction: 'p50(transaction.duration)',
  917. trendType: 'regression',
  918. }),
  919. })
  920. );
  921. });
  922. it('Most slow frames widget', async function () {
  923. const data = initializeData();
  924. wrapper = render(
  925. <WrappedComponent
  926. data={data}
  927. defaultChartSetting={PerformanceWidgetSetting.MOST_SLOW_FRAMES}
  928. />
  929. );
  930. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  931. 'Most Slow Frames'
  932. );
  933. expect(eventsMock).toHaveBeenCalledTimes(1);
  934. expect(eventsMock).toHaveBeenNthCalledWith(
  935. 1,
  936. expect.anything(),
  937. expect.objectContaining({
  938. query: expect.objectContaining({
  939. cursor: '0:0:1',
  940. environment: ['prod'],
  941. field: ['transaction', 'project.id', 'epm()', 'avg(measurements.frames_slow)'],
  942. noPagination: true,
  943. per_page: QUERY_LIMIT_PARAM,
  944. project: ['-42'],
  945. query: 'transaction.op:pageload epm():>0.01 avg(measurements.frames_slow):>0',
  946. sort: '-avg(measurements.frames_slow)',
  947. statsPeriod: '7d',
  948. }),
  949. })
  950. );
  951. expect(await screen.findByTestId('empty-state')).toBeInTheDocument();
  952. });
  953. it('Most slow frames widget - MEP', async function () {
  954. const data = initializeData(
  955. {},
  956. {
  957. features: ['performance-use-metrics'],
  958. }
  959. );
  960. wrapper = render(
  961. <WrappedComponent
  962. data={data}
  963. defaultChartSetting={PerformanceWidgetSetting.MOST_SLOW_FRAMES}
  964. />
  965. );
  966. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  967. 'Most Slow Frames'
  968. );
  969. expect(eventsMock).toHaveBeenCalledTimes(1);
  970. expect(eventsMock).toHaveBeenNthCalledWith(
  971. 1,
  972. expect.anything(),
  973. expect.objectContaining({
  974. query: expect.objectContaining({
  975. cursor: '0:0:1',
  976. environment: ['prod'],
  977. field: ['transaction', 'project.id', 'epm()', 'avg(measurements.frames_slow)'],
  978. noPagination: true,
  979. per_page: QUERY_LIMIT_PARAM,
  980. project: ['-42'],
  981. query: 'transaction.op:pageload epm():>0.01 avg(measurements.frames_slow):>0',
  982. sort: '-avg(measurements.frames_slow)',
  983. statsPeriod: '7d',
  984. }),
  985. })
  986. );
  987. expect(await screen.findByTestId('empty-state')).toBeInTheDocument();
  988. });
  989. it('Most frozen frames widget', async function () {
  990. const data = initializeData();
  991. wrapper = render(
  992. <WrappedComponent
  993. data={data}
  994. defaultChartSetting={PerformanceWidgetSetting.MOST_FROZEN_FRAMES}
  995. />
  996. );
  997. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  998. 'Most Frozen Frames'
  999. );
  1000. expect(eventsMock).toHaveBeenCalledTimes(1);
  1001. expect(eventsMock).toHaveBeenNthCalledWith(
  1002. 1,
  1003. expect.anything(),
  1004. expect.objectContaining({
  1005. query: expect.objectContaining({
  1006. cursor: '0:0:1',
  1007. environment: ['prod'],
  1008. field: [
  1009. 'transaction',
  1010. 'project.id',
  1011. 'epm()',
  1012. 'avg(measurements.frames_frozen)',
  1013. ],
  1014. noPagination: true,
  1015. per_page: QUERY_LIMIT_PARAM,
  1016. project: ['-42'],
  1017. query: 'transaction.op:pageload epm():>0.01 avg(measurements.frames_frozen):>0',
  1018. sort: '-avg(measurements.frames_frozen)',
  1019. statsPeriod: '7d',
  1020. }),
  1021. })
  1022. );
  1023. expect(await screen.findByTestId('empty-state')).toBeInTheDocument();
  1024. });
  1025. it('Able to change widget type from menu', async function () {
  1026. const data = initializeData();
  1027. const setRowChartSettings = jest.fn(() => {});
  1028. wrapper = render(
  1029. <WrappedComponent
  1030. data={data}
  1031. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  1032. setRowChartSettings={setRowChartSettings}
  1033. />
  1034. );
  1035. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  1036. 'Failure Rate'
  1037. );
  1038. expect(eventStatsMock).toHaveBeenCalledTimes(1);
  1039. expect(setRowChartSettings).toHaveBeenCalledTimes(0);
  1040. await userEvent.click(await screen.findByLabelText('More'));
  1041. await userEvent.click(await screen.findByText('User Misery'));
  1042. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  1043. 'User Misery'
  1044. );
  1045. expect(eventStatsMock).toHaveBeenCalledTimes(2);
  1046. expect(setRowChartSettings).toHaveBeenCalledTimes(1);
  1047. });
  1048. it('Chart settings passed from the row are disabled in the menu', async function () {
  1049. const data = initializeData();
  1050. const setRowChartSettings = jest.fn(() => {});
  1051. wrapper = render(
  1052. <WrappedComponent
  1053. data={data}
  1054. defaultChartSetting={PerformanceWidgetSetting.FAILURE_RATE_AREA}
  1055. setRowChartSettings={setRowChartSettings}
  1056. rowChartSettings={[
  1057. PerformanceWidgetSetting.FAILURE_RATE_AREA,
  1058. PerformanceWidgetSetting.USER_MISERY_AREA,
  1059. ]}
  1060. />
  1061. );
  1062. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  1063. 'Failure Rate'
  1064. );
  1065. // Open context menu
  1066. await userEvent.click(await screen.findByLabelText('More'));
  1067. // Check that the the "User Misery" option is disabled by clicking on it,
  1068. // expecting that the selected option doesn't change
  1069. const userMiseryOption = await screen.findByRole('option', {name: 'User Misery'});
  1070. await userEvent.click(userMiseryOption);
  1071. expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
  1072. 'Failure Rate'
  1073. );
  1074. });
  1075. });