changeExplorer.spec.tsx 18 KB

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