widgetContainer.spec.tsx 37 KB

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