changeExplorer.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. import {Fragment} from 'react';
  2. import moment from 'moment';
  3. import {initializeData} from 'sentry-test/performance/initializePerformanceData';
  4. import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import {SuspectSpans} from 'sentry/utils/performance/suspectSpans/types';
  7. import {EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types';
  8. import {PerformanceChangeExplorer} from 'sentry/views/performance/trends/changeExplorer';
  9. import {
  10. FunctionsField,
  11. FunctionsList,
  12. } from 'sentry/views/performance/trends/changeExplorerUtils/functionsList';
  13. import {
  14. COLUMNS,
  15. MetricsTable,
  16. renderBodyCell,
  17. } from 'sentry/views/performance/trends/changeExplorerUtils/metricsTable';
  18. import {SpansList} from 'sentry/views/performance/trends/changeExplorerUtils/spansList';
  19. import {
  20. NormalizedTrendsTransaction,
  21. TrendChangeType,
  22. TrendFunctionField,
  23. } from 'sentry/views/performance/trends/types';
  24. import {TRENDS_PARAMETERS} from 'sentry/views/performance/trends/utils';
  25. async function waitForMockCall(mock: jest.Mock) {
  26. await waitFor(() => {
  27. expect(mock).toHaveBeenCalled();
  28. });
  29. }
  30. const transaction: NormalizedTrendsTransaction = {
  31. aggregate_range_1: 78.2757131147541,
  32. aggregate_range_2: 110.50465131578949,
  33. breakpoint: 1687262400,
  34. project: 'sentry',
  35. transaction: 'sentry.tasks.store.save_event',
  36. trend_difference: 32.22893820103539,
  37. trend_percentage: 1.411736117354651,
  38. count: 3459,
  39. received_at: moment(1601251200000),
  40. };
  41. const spanResults: SuspectSpans = [
  42. {
  43. op: 'db',
  44. group: '1',
  45. description: 'span1',
  46. frequency: 4,
  47. count: 4,
  48. avgOccurrences: undefined,
  49. sumExclusiveTime: 345345,
  50. p50ExclusiveTime: undefined,
  51. p75ExclusiveTime: 25,
  52. p95ExclusiveTime: undefined,
  53. p99ExclusiveTime: undefined,
  54. examples: [],
  55. },
  56. {
  57. op: 'db',
  58. group: '2',
  59. description: 'span2',
  60. frequency: 4,
  61. count: 4,
  62. avgOccurrences: undefined,
  63. sumExclusiveTime: 345345,
  64. p50ExclusiveTime: undefined,
  65. p75ExclusiveTime: 25,
  66. p95ExclusiveTime: undefined,
  67. p99ExclusiveTime: undefined,
  68. examples: [],
  69. },
  70. {
  71. op: 'db',
  72. group: '3',
  73. description: 'span3',
  74. frequency: 4,
  75. count: 4,
  76. avgOccurrences: undefined,
  77. sumExclusiveTime: 345345,
  78. p50ExclusiveTime: undefined,
  79. p75ExclusiveTime: 25,
  80. p95ExclusiveTime: undefined,
  81. p99ExclusiveTime: undefined,
  82. examples: [],
  83. },
  84. {
  85. op: 'db',
  86. group: '4',
  87. description: 'span4',
  88. frequency: 4,
  89. count: 4,
  90. avgOccurrences: undefined,
  91. sumExclusiveTime: 345345,
  92. p50ExclusiveTime: undefined,
  93. p75ExclusiveTime: 25,
  94. p95ExclusiveTime: undefined,
  95. p99ExclusiveTime: undefined,
  96. examples: [],
  97. },
  98. {
  99. op: 'db',
  100. group: '5',
  101. description: 'span5',
  102. frequency: 4,
  103. count: 4,
  104. avgOccurrences: undefined,
  105. sumExclusiveTime: 345345,
  106. p50ExclusiveTime: undefined,
  107. p75ExclusiveTime: 25,
  108. p95ExclusiveTime: undefined,
  109. p99ExclusiveTime: undefined,
  110. examples: [],
  111. },
  112. {
  113. op: 'db',
  114. group: '6',
  115. description: 'span6',
  116. frequency: 4,
  117. count: 4,
  118. avgOccurrences: undefined,
  119. sumExclusiveTime: 345345,
  120. p50ExclusiveTime: undefined,
  121. p75ExclusiveTime: 25,
  122. p95ExclusiveTime: undefined,
  123. p99ExclusiveTime: undefined,
  124. examples: [],
  125. },
  126. {
  127. op: 'db',
  128. group: '7',
  129. description: 'span7',
  130. frequency: 4,
  131. count: 4,
  132. avgOccurrences: undefined,
  133. sumExclusiveTime: 345345,
  134. p50ExclusiveTime: undefined,
  135. p75ExclusiveTime: 25,
  136. p95ExclusiveTime: undefined,
  137. p99ExclusiveTime: undefined,
  138. examples: [],
  139. },
  140. {
  141. op: 'db',
  142. group: '8',
  143. description: 'span8',
  144. frequency: 4,
  145. count: 4,
  146. avgOccurrences: undefined,
  147. sumExclusiveTime: 345345,
  148. p50ExclusiveTime: undefined,
  149. p75ExclusiveTime: 25,
  150. p95ExclusiveTime: undefined,
  151. p99ExclusiveTime: undefined,
  152. examples: [],
  153. },
  154. {
  155. op: 'db',
  156. group: '9',
  157. description: 'span9',
  158. frequency: 4,
  159. count: 4,
  160. avgOccurrences: undefined,
  161. sumExclusiveTime: 345345,
  162. p50ExclusiveTime: undefined,
  163. p75ExclusiveTime: 25,
  164. p95ExclusiveTime: undefined,
  165. p99ExclusiveTime: undefined,
  166. examples: [],
  167. },
  168. {
  169. op: 'db',
  170. group: '10',
  171. description: 'span10',
  172. frequency: 4,
  173. count: 4,
  174. avgOccurrences: undefined,
  175. sumExclusiveTime: 345345,
  176. p50ExclusiveTime: undefined,
  177. p75ExclusiveTime: 25,
  178. p95ExclusiveTime: undefined,
  179. p99ExclusiveTime: undefined,
  180. examples: [],
  181. },
  182. {
  183. op: 'db',
  184. group: '11',
  185. description: 'span11',
  186. frequency: 4,
  187. count: 4,
  188. avgOccurrences: undefined,
  189. sumExclusiveTime: 345345,
  190. p50ExclusiveTime: undefined,
  191. p75ExclusiveTime: 25,
  192. p95ExclusiveTime: undefined,
  193. p99ExclusiveTime: undefined,
  194. examples: [],
  195. },
  196. ];
  197. const functionResults: EventsResultsDataRow<FunctionsField>[] = [
  198. {
  199. 'count()': 234,
  200. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  201. function: 'f1',
  202. 'p75()': 4239847,
  203. package: 'p1',
  204. 'sum()': 2304823908,
  205. },
  206. {
  207. 'count()': 234,
  208. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  209. function: 'f2',
  210. 'p75()': 4239847,
  211. package: 'p2',
  212. 'sum()': 2304823908,
  213. },
  214. {
  215. 'count()': 234,
  216. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  217. function: 'f3',
  218. 'p75()': 4239847,
  219. package: 'p3',
  220. 'sum()': 2304823908,
  221. },
  222. {
  223. 'count()': 234,
  224. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  225. function: 'f4',
  226. 'p75()': 4239847,
  227. package: 'p4',
  228. 'sum()': 2304823908,
  229. },
  230. {
  231. 'count()': 234,
  232. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  233. function: 'f5',
  234. 'p75()': 4239847,
  235. package: 'p5',
  236. 'sum()': 2304823908,
  237. },
  238. {
  239. 'count()': 234,
  240. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  241. function: 'f5',
  242. 'p75()': 4239847,
  243. package: 'p5',
  244. 'sum()': 2304823908,
  245. },
  246. {
  247. 'count()': 234,
  248. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  249. function: 'f7',
  250. 'p75()': 4239847,
  251. package: 'p7',
  252. 'sum()': 2304823908,
  253. },
  254. {
  255. 'count()': 234,
  256. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  257. function: 'f8',
  258. 'p75()': 4239847,
  259. package: 'p8',
  260. 'sum()': 2304823908,
  261. },
  262. {
  263. 'count()': 234,
  264. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  265. function: 'f9',
  266. 'p75()': 4239847,
  267. package: 'p9',
  268. 'sum()': 2304823908,
  269. },
  270. {
  271. 'count()': 234,
  272. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  273. function: 'f10',
  274. 'p75()': 4239847,
  275. package: 'p10',
  276. 'sum()': 2304823908,
  277. },
  278. {
  279. 'count()': 234,
  280. 'examples()': ['serw8r9s', 'aeo4i2u38'],
  281. function: 'f11',
  282. 'p75()': 4239847,
  283. package: 'p11',
  284. 'sum()': 2304823908,
  285. },
  286. ];
  287. describe('Performance > Trends > Performance Change Explorer', function () {
  288. let eventsMockBefore;
  289. let spansMock;
  290. beforeEach(function () {
  291. eventsMockBefore = MockApiClient.addMockResponse({
  292. url: '/organizations/org-slug/events/',
  293. body: {
  294. data: [
  295. {
  296. 'p95()': 1010.9232499999998,
  297. 'p50()': 47.34580982348902,
  298. 'tps()': 3.7226926286168966,
  299. 'count()': 345,
  300. 'failure_rate()': 0.23498234,
  301. 'examples()': ['dkwj4w8sdjk', 'asdi389a8'],
  302. },
  303. ],
  304. meta: {
  305. fields: {
  306. 'p95()': 'duration',
  307. '950()': 'duration',
  308. 'tps()': 'number',
  309. 'count()': 'number',
  310. 'failure_rate()': 'number',
  311. 'examples()': 'Array',
  312. },
  313. units: {
  314. 'p95()': 'millisecond',
  315. 'p50()': 'millisecond',
  316. 'tps()': null,
  317. 'count()': null,
  318. 'failure_rate()': null,
  319. 'examples()': null,
  320. },
  321. isMetricsData: true,
  322. tips: {},
  323. dataset: 'metrics',
  324. },
  325. },
  326. });
  327. spansMock = MockApiClient.addMockResponse({
  328. url: '/organizations/org-slug/events-spans-performance/',
  329. body: [],
  330. });
  331. });
  332. afterEach(function () {
  333. MockApiClient.clearMockResponses();
  334. act(() => ProjectsStore.reset());
  335. });
  336. it('renders basic UI elements', async function () {
  337. const data = initializeData();
  338. const statsData = {
  339. ['/organizations/:orgId/performance/']: {
  340. data: [],
  341. order: 0,
  342. },
  343. };
  344. render(
  345. <PerformanceChangeExplorer
  346. collapsed={false}
  347. transaction={transaction}
  348. onClose={() => {}}
  349. trendChangeType={TrendChangeType.REGRESSION}
  350. trendFunction={TrendFunctionField.P50}
  351. trendParameter={TRENDS_PARAMETERS[0]}
  352. trendView={data.eventView}
  353. statsData={statsData}
  354. isLoading={false}
  355. organization={data.organization}
  356. projects={data.projects}
  357. location={data.location}
  358. />,
  359. {
  360. context: data.routerContext,
  361. organization: data.organization,
  362. }
  363. );
  364. await waitForMockCall(eventsMockBefore);
  365. await waitForMockCall(spansMock);
  366. await waitFor(() => {
  367. expect(screen.getByTestId('pce-header')).toBeInTheDocument();
  368. expect(screen.getByTestId('pce-graph')).toBeInTheDocument();
  369. expect(screen.getByTestId('grid-editable')).toBeInTheDocument();
  370. expect(screen.getAllByTestId('pce-metrics-chart-row-metric')).toHaveLength(4);
  371. expect(screen.getAllByTestId('pce-metrics-chart-row-before')).toHaveLength(4);
  372. expect(screen.getAllByTestId('pce-metrics-chart-row-after')).toHaveLength(4);
  373. expect(screen.getAllByTestId('pce-metrics-chart-row-change')).toHaveLength(4);
  374. expect(screen.getByTestId('list-item')).toBeInTheDocument();
  375. });
  376. });
  377. it('shows correct change notation for no change', async () => {
  378. const data = initializeData();
  379. render(
  380. <MetricsTable
  381. isLoading={false}
  382. location={data.location}
  383. trendFunction={TrendFunctionField.P50}
  384. transaction={transaction}
  385. trendView={data.eventView}
  386. organization={data.organization}
  387. />
  388. );
  389. await waitForMockCall(eventsMockBefore);
  390. await waitFor(() => {
  391. expect(screen.getAllByText('3.7 ps')).toHaveLength(2);
  392. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  393. });
  394. });
  395. it('shows correct change notation for positive change', async () => {
  396. const data = initializeData();
  397. render(
  398. <MetricsTable
  399. isLoading={false}
  400. location={data.location}
  401. trendFunction={TrendFunctionField.P50}
  402. transaction={transaction}
  403. trendView={data.eventView}
  404. organization={data.organization}
  405. />
  406. );
  407. await waitForMockCall(eventsMockBefore);
  408. await waitFor(() => {
  409. expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
  410. '78.3 ms'
  411. );
  412. expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
  413. '110.5 ms'
  414. );
  415. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
  416. '+41.2%'
  417. );
  418. });
  419. });
  420. it('shows correct change notation for negative change', async () => {
  421. const data = initializeData();
  422. const negativeTransaction = {
  423. ...transaction,
  424. aggregate_range_1: 110.50465131578949,
  425. aggregate_range_2: 78.2757131147541,
  426. trend_percentage: 0.588263882645349,
  427. };
  428. render(
  429. <MetricsTable
  430. isLoading={false}
  431. location={data.location}
  432. trendFunction={TrendFunctionField.P50}
  433. transaction={negativeTransaction}
  434. trendView={data.eventView}
  435. organization={data.organization}
  436. />
  437. );
  438. await waitForMockCall(eventsMockBefore);
  439. await waitFor(() => {
  440. expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
  441. '78.3 ms'
  442. );
  443. expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
  444. '110.5 ms'
  445. );
  446. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
  447. '-41.2%'
  448. );
  449. });
  450. });
  451. it('shows correct change notation for no results', async () => {
  452. const data = initializeData();
  453. const nullEventsMock = MockApiClient.addMockResponse({
  454. url: '/organizations/org-slug/events/',
  455. body: {
  456. data: [
  457. {
  458. 'p95()': 1010.9232499999998,
  459. 'p50()': 47.34580982348902,
  460. 'count()': 345,
  461. },
  462. ],
  463. meta: {
  464. fields: {
  465. 'p95()': 'duration',
  466. '950()': 'duration',
  467. 'count()': 'number',
  468. },
  469. units: {
  470. 'p95()': 'millisecond',
  471. 'p50()': 'millisecond',
  472. 'count()': null,
  473. },
  474. isMetricsData: true,
  475. tips: {},
  476. dataset: 'metrics',
  477. },
  478. },
  479. });
  480. render(
  481. <MetricsTable
  482. isLoading={false}
  483. location={data.location}
  484. trendFunction={TrendFunctionField.P50}
  485. transaction={transaction}
  486. trendView={data.eventView}
  487. organization={data.organization}
  488. />
  489. );
  490. await waitForMockCall(nullEventsMock);
  491. await waitFor(() => {
  492. expect(screen.getAllByTestId('pce-metrics-text-after')[0]).toHaveTextContent('-');
  493. expect(screen.getAllByTestId('pce-metrics-text-before')[0]).toHaveTextContent('-');
  494. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  495. });
  496. });
  497. it('returns correct null formatting for change column', () => {
  498. render(
  499. <Fragment>
  500. {renderBodyCell(COLUMNS.change, {
  501. metric: null,
  502. before: null,
  503. after: null,
  504. change: '0%',
  505. })}
  506. {renderBodyCell(COLUMNS.change, {
  507. metric: null,
  508. before: null,
  509. after: null,
  510. change: '+NaN%',
  511. })}
  512. {renderBodyCell(COLUMNS.change, {
  513. metric: null,
  514. before: null,
  515. after: null,
  516. change: '-',
  517. })}
  518. </Fragment>
  519. );
  520. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  521. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent('-');
  522. expect(screen.getAllByTestId('pce-metrics-text-change')[2]).toHaveTextContent('-');
  523. });
  524. it('returns correct positive formatting for change column', () => {
  525. render(
  526. renderBodyCell(COLUMNS.change, {
  527. metric: null,
  528. before: null,
  529. after: null,
  530. change: '40.3%',
  531. })
  532. );
  533. expect(screen.getByText('+40.3%')).toBeInTheDocument();
  534. });
  535. it('renders spans list with no results', async () => {
  536. const data = initializeData();
  537. const emptyEventsMock = MockApiClient.addMockResponse({
  538. url: '/organizations/org-slug/events/',
  539. body: {},
  540. });
  541. render(
  542. <div>
  543. <SpansList
  544. location={data.location}
  545. organization={data.organization}
  546. trendView={data.eventView}
  547. breakpoint={transaction.breakpoint!}
  548. transaction={transaction}
  549. trendChangeType={TrendChangeType.REGRESSION}
  550. />
  551. <FunctionsList
  552. location={data.location}
  553. organization={data.organization}
  554. trendView={data.eventView}
  555. breakpoint={transaction.breakpoint!}
  556. transaction={transaction}
  557. trendChangeType={TrendChangeType.REGRESSION}
  558. />
  559. </div>
  560. );
  561. await waitForMockCall(spansMock);
  562. await waitForMockCall(emptyEventsMock);
  563. await waitFor(() => {
  564. expect(screen.getAllByTestId('empty-state')).toHaveLength(2);
  565. expect(screen.getByTestId('spans-no-results')).toBeInTheDocument();
  566. expect(screen.getByTestId('functions-no-results')).toBeInTheDocument();
  567. });
  568. });
  569. it('renders spans list with error message', async () => {
  570. MockApiClient.addMockResponse({
  571. url: '/organizations/org-slug/events-spans-performance/',
  572. statusCode: 504,
  573. });
  574. MockApiClient.addMockResponse({
  575. url: '/organizations/org-slug/events/',
  576. statusCode: 504,
  577. });
  578. const data = initializeData();
  579. render(
  580. <div>
  581. <SpansList
  582. location={data.location}
  583. organization={data.organization}
  584. trendView={data.eventView}
  585. breakpoint={transaction.breakpoint!}
  586. transaction={transaction}
  587. trendChangeType={TrendChangeType.REGRESSION}
  588. />
  589. <FunctionsList
  590. location={data.location}
  591. organization={data.organization}
  592. trendView={data.eventView}
  593. breakpoint={transaction.breakpoint!}
  594. transaction={transaction}
  595. trendChangeType={TrendChangeType.REGRESSION}
  596. />
  597. </div>
  598. );
  599. await waitFor(() => {
  600. expect(screen.getByTestId('error-indicator-spans')).toBeInTheDocument();
  601. expect(screen.getByTestId('error-indicator-functions')).toBeInTheDocument();
  602. });
  603. });
  604. it('renders spans list with no changes message', async () => {
  605. MockApiClient.addMockResponse({
  606. url: '/organizations/org-slug/events-spans-performance/',
  607. body: spanResults,
  608. });
  609. MockApiClient.addMockResponse({
  610. url: '/organizations/org-slug/events/',
  611. body: {
  612. data: functionResults,
  613. meta: {},
  614. },
  615. });
  616. const data = initializeData();
  617. render(
  618. <div>
  619. <SpansList
  620. location={data.location}
  621. organization={data.organization}
  622. trendView={data.eventView}
  623. breakpoint={transaction.breakpoint!}
  624. transaction={transaction}
  625. trendChangeType={TrendChangeType.REGRESSION}
  626. />
  627. <FunctionsList
  628. location={data.location}
  629. organization={data.organization}
  630. trendView={data.eventView}
  631. breakpoint={transaction.breakpoint!}
  632. transaction={transaction}
  633. trendChangeType={TrendChangeType.REGRESSION}
  634. />
  635. </div>
  636. );
  637. await waitFor(() => {
  638. expect(screen.getAllByTestId('empty-state')).toHaveLength(2);
  639. expect(screen.getByTestId('spans-no-changes')).toBeInTheDocument();
  640. expect(screen.getByTestId('functions-no-changes')).toBeInTheDocument();
  641. });
  642. });
  643. });