changeExplorer.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. import React 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 {PerformanceChangeExplorer} from 'sentry/views/performance/trends/changeExplorer';
  8. import {
  9. COLUMNS,
  10. MetricsTable,
  11. renderBodyCell,
  12. } from 'sentry/views/performance/trends/changeExplorerUtils/metricsTable';
  13. import {SpansList} from 'sentry/views/performance/trends/changeExplorerUtils/spansList';
  14. import {
  15. NormalizedTrendsTransaction,
  16. TrendChangeType,
  17. TrendFunctionField,
  18. } 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. describe('Performance > Trends > Performance Change Explorer', function () {
  193. let eventsMockBefore;
  194. let spansMock;
  195. beforeEach(function () {
  196. eventsMockBefore = MockApiClient.addMockResponse({
  197. url: '/organizations/org-slug/events/',
  198. body: {
  199. data: [
  200. {
  201. 'p95()': 1010.9232499999998,
  202. 'p50()': 47.34580982348902,
  203. 'tps()': 3.7226926286168966,
  204. 'count()': 345,
  205. },
  206. ],
  207. meta: {
  208. fields: {
  209. 'p95()': 'duration',
  210. '950()': 'duration',
  211. 'tps()': 'number',
  212. 'count()': 'number',
  213. },
  214. units: {
  215. 'p95()': 'millisecond',
  216. 'p50()': 'millisecond',
  217. 'tps()': null,
  218. 'count()': null,
  219. },
  220. isMetricsData: true,
  221. tips: {},
  222. dataset: 'metrics',
  223. },
  224. },
  225. });
  226. spansMock = MockApiClient.addMockResponse({
  227. url: '/organizations/org-slug/events-spans-performance/',
  228. body: [],
  229. });
  230. });
  231. afterEach(function () {
  232. MockApiClient.clearMockResponses();
  233. act(() => ProjectsStore.reset());
  234. });
  235. it('renders basic UI elements', async function () {
  236. const data = initializeData();
  237. const statsData = {
  238. ['/organizations/:orgId/performance/']: {
  239. data: [],
  240. order: 0,
  241. },
  242. };
  243. render(
  244. <PerformanceChangeExplorer
  245. collapsed={false}
  246. transaction={transaction}
  247. onClose={() => {}}
  248. trendChangeType={TrendChangeType.REGRESSION}
  249. trendFunction={TrendFunctionField.P50}
  250. trendParameter={TRENDS_PARAMETERS[0]}
  251. trendView={data.eventView}
  252. statsData={statsData}
  253. isLoading={false}
  254. organization={data.organization}
  255. projects={data.projects}
  256. location={data.location}
  257. />,
  258. {
  259. context: data.routerContext,
  260. organization: data.organization,
  261. }
  262. );
  263. await waitForMockCall(eventsMockBefore);
  264. await waitForMockCall(spansMock);
  265. await waitFor(() => {
  266. expect(screen.getByTestId('pce-header')).toBeInTheDocument();
  267. expect(screen.getByTestId('pce-graph')).toBeInTheDocument();
  268. expect(screen.getByTestId('grid-editable')).toBeInTheDocument();
  269. expect(screen.getAllByTestId('pce-metrics-chart-row-metric')).toHaveLength(4);
  270. expect(screen.getAllByTestId('pce-metrics-chart-row-before')).toHaveLength(4);
  271. expect(screen.getAllByTestId('pce-metrics-chart-row-after')).toHaveLength(4);
  272. expect(screen.getAllByTestId('pce-metrics-chart-row-change')).toHaveLength(4);
  273. expect(screen.getByTestId('spans-no-results')).toBeInTheDocument();
  274. });
  275. });
  276. it('shows correct change notation for no change', async () => {
  277. const data = initializeData();
  278. render(
  279. <MetricsTable
  280. isLoading={false}
  281. location={data.location}
  282. trendFunction={TrendFunctionField.P50}
  283. transaction={transaction}
  284. trendView={data.eventView}
  285. organization={data.organization}
  286. />
  287. );
  288. await waitForMockCall(eventsMockBefore);
  289. await waitFor(() => {
  290. expect(screen.getAllByText('3.7 ps')).toHaveLength(2);
  291. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  292. });
  293. });
  294. it('shows correct change notation for positive change', async () => {
  295. const data = initializeData();
  296. render(
  297. <MetricsTable
  298. isLoading={false}
  299. location={data.location}
  300. trendFunction={TrendFunctionField.P50}
  301. transaction={transaction}
  302. trendView={data.eventView}
  303. organization={data.organization}
  304. />
  305. );
  306. await waitForMockCall(eventsMockBefore);
  307. await waitFor(() => {
  308. expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
  309. '78.3 ms'
  310. );
  311. expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
  312. '110.5 ms'
  313. );
  314. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
  315. '+41.2%'
  316. );
  317. });
  318. });
  319. it('shows correct change notation for negative change', async () => {
  320. const data = initializeData();
  321. const negativeTransaction = {
  322. ...transaction,
  323. aggregate_range_1: 110.50465131578949,
  324. aggregate_range_2: 78.2757131147541,
  325. trend_percentage: 0.588263882645349,
  326. };
  327. render(
  328. <MetricsTable
  329. isLoading={false}
  330. location={data.location}
  331. trendFunction={TrendFunctionField.P50}
  332. transaction={negativeTransaction}
  333. trendView={data.eventView}
  334. organization={data.organization}
  335. />
  336. );
  337. await waitForMockCall(eventsMockBefore);
  338. await waitFor(() => {
  339. expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
  340. '78.3 ms'
  341. );
  342. expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
  343. '110.5 ms'
  344. );
  345. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
  346. '-41.2%'
  347. );
  348. });
  349. });
  350. it('shows correct change notation for no results', async () => {
  351. const data = initializeData();
  352. const nullEventsMock = MockApiClient.addMockResponse({
  353. url: '/organizations/org-slug/events/',
  354. body: {
  355. data: [
  356. {
  357. 'p95()': 1010.9232499999998,
  358. 'p50()': 47.34580982348902,
  359. 'count()': 345,
  360. },
  361. ],
  362. meta: {
  363. fields: {
  364. 'p95()': 'duration',
  365. '950()': 'duration',
  366. 'count()': 'number',
  367. },
  368. units: {
  369. 'p95()': 'millisecond',
  370. 'p50()': 'millisecond',
  371. 'count()': null,
  372. },
  373. isMetricsData: true,
  374. tips: {},
  375. dataset: 'metrics',
  376. },
  377. },
  378. });
  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(nullEventsMock);
  390. await waitFor(() => {
  391. expect(screen.getAllByTestId('pce-metrics-text-after')[0]).toHaveTextContent('-');
  392. expect(screen.getAllByTestId('pce-metrics-text-before')[0]).toHaveTextContent('-');
  393. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  394. });
  395. });
  396. it('returns correct null formatting for change column', () => {
  397. render(
  398. <React.Fragment>
  399. {renderBodyCell(COLUMNS.change, {
  400. metric: null,
  401. before: null,
  402. after: null,
  403. change: '0%',
  404. })}
  405. {renderBodyCell(COLUMNS.change, {
  406. metric: null,
  407. before: null,
  408. after: null,
  409. change: '+NaN%',
  410. })}
  411. {renderBodyCell(COLUMNS.change, {
  412. metric: null,
  413. before: null,
  414. after: null,
  415. change: '-',
  416. })}
  417. </React.Fragment>
  418. );
  419. expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
  420. expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent('-');
  421. expect(screen.getAllByTestId('pce-metrics-text-change')[2]).toHaveTextContent('-');
  422. });
  423. it('returns correct positive formatting for change column', () => {
  424. render(
  425. renderBodyCell(COLUMNS.change, {
  426. metric: null,
  427. before: null,
  428. after: null,
  429. change: '40.3%',
  430. })
  431. );
  432. expect(screen.getByText('+40.3%')).toBeInTheDocument();
  433. });
  434. it('renders spans list with no results', async () => {
  435. const data = initializeData();
  436. render(
  437. <SpansList
  438. location={data.location}
  439. organization={data.organization}
  440. trendView={data.eventView}
  441. breakpoint={transaction.breakpoint!}
  442. transaction={transaction}
  443. trendChangeType={TrendChangeType.REGRESSION}
  444. />
  445. );
  446. await waitForMockCall(spansMock);
  447. await waitFor(() => {
  448. expect(screen.getByTestId('empty-state')).toBeInTheDocument();
  449. expect(screen.getByTestId('spans-no-results')).toBeInTheDocument();
  450. });
  451. });
  452. it('renders spans list with error message', async () => {
  453. MockApiClient.addMockResponse({
  454. url: '/organizations/org-slug/events-spans-performance/',
  455. statusCode: 504,
  456. });
  457. const data = initializeData();
  458. render(
  459. <SpansList
  460. location={data.location}
  461. organization={data.organization}
  462. trendView={data.eventView}
  463. breakpoint={transaction.breakpoint!}
  464. transaction={transaction}
  465. trendChangeType={TrendChangeType.REGRESSION}
  466. />
  467. );
  468. await waitFor(() => {
  469. expect(screen.getByTestId('error-indicator')).toBeInTheDocument();
  470. });
  471. });
  472. it('renders spans list with no changes message', async () => {
  473. MockApiClient.addMockResponse({
  474. url: '/organizations/org-slug/events-spans-performance/',
  475. body: spanResults,
  476. });
  477. const data = initializeData();
  478. render(
  479. <SpansList
  480. location={data.location}
  481. organization={data.organization}
  482. trendView={data.eventView}
  483. breakpoint={transaction.breakpoint!}
  484. transaction={transaction}
  485. trendChangeType={TrendChangeType.REGRESSION}
  486. />
  487. );
  488. await waitFor(() => {
  489. expect(screen.getByTestId('empty-state')).toBeInTheDocument();
  490. expect(screen.getByTestId('spans-no-changes')).toBeInTheDocument();
  491. });
  492. });
  493. });