index.spec.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. import {browserHistory} from 'react-router';
  2. import {
  3. generateSuspectSpansResponse,
  4. initializeData as _initializeData,
  5. } from 'sentry-test/performance/initializePerformanceData';
  6. import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
  7. import ProjectsStore from 'sentry/stores/projectsStore';
  8. import SpanDetails from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails';
  9. import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
  10. function initializeData(settings) {
  11. const data = _initializeData(settings);
  12. act(() => void ProjectsStore.loadInitialData(data.organization.projects));
  13. return data;
  14. }
  15. describe('Performance > Transaction Spans > Span Summary', function () {
  16. beforeEach(function () {
  17. MockApiClient.addMockResponse({
  18. url: '/organizations/org-slug/projects/',
  19. body: [],
  20. });
  21. MockApiClient.addMockResponse({
  22. url: '/organizations/org-slug/events/',
  23. body: {data: [{'count()': 1}]},
  24. });
  25. });
  26. afterEach(function () {
  27. MockApiClient.clearMockResponses();
  28. ProjectsStore.reset();
  29. // need to typecast to any to be able to call mockReset
  30. (browserHistory.push as any).mockReset();
  31. });
  32. describe('Without Span Data', function () {
  33. beforeEach(function () {
  34. MockApiClient.addMockResponse({
  35. url: '/organizations/org-slug/events-spans-performance/',
  36. body: [],
  37. });
  38. MockApiClient.addMockResponse({
  39. url: '/organizations/org-slug/events-spans/',
  40. body: [],
  41. });
  42. MockApiClient.addMockResponse({
  43. url: '/organizations/org-slug/events-spans-stats/',
  44. body: {
  45. 'percentileArray(spans_exclusive_time, 0.50)': {
  46. data: [
  47. [0, [{count: 0}]],
  48. [10, [{count: 0}]],
  49. ],
  50. order: 2,
  51. start: 0,
  52. end: 10,
  53. },
  54. 'percentileArray(spans_exclusive_time, 0.75)': {
  55. data: [
  56. [0, [{count: 0}]],
  57. [10, [{count: 0}]],
  58. ],
  59. order: 2,
  60. start: 0,
  61. end: 10,
  62. },
  63. 'percentileArray(spans_exclusive_time, 0.95)': {
  64. data: [
  65. [0, [{count: 0}]],
  66. [10, [{count: 0}]],
  67. ],
  68. order: 2,
  69. start: 0,
  70. end: 10,
  71. },
  72. 'percentileArray(spans_exclusive_time, 0.99)': {
  73. data: [
  74. [0, [{count: 0}]],
  75. [10, [{count: 0}]],
  76. ],
  77. order: 2,
  78. start: 0,
  79. end: 10,
  80. },
  81. },
  82. });
  83. });
  84. it('renders empty when missing project param', function () {
  85. const data = initializeData({query: {transaction: 'transaction'}});
  86. const {container} = render(
  87. <SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />,
  88. {organization: data.organization}
  89. );
  90. expect(container).toBeEmptyDOMElement();
  91. });
  92. it('renders empty when missing transaction param', function () {
  93. const data = initializeData({query: {project: '1'}});
  94. const {container} = render(
  95. <SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />,
  96. {organization: data.organization}
  97. );
  98. expect(container).toBeEmptyDOMElement();
  99. });
  100. it('renders no data when empty response', async function () {
  101. const data = initializeData({
  102. features: ['performance-view'],
  103. query: {project: '1', transaction: 'transaction'},
  104. });
  105. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  106. context: data.routerContext,
  107. organization: data.organization,
  108. });
  109. expect(
  110. await screen.findByText('No results found for your query')
  111. ).toBeInTheDocument();
  112. expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument();
  113. });
  114. });
  115. describe('With Bad Span Data', function () {
  116. it('filters examples missing spans', async function () {
  117. MockApiClient.addMockResponse({
  118. url: '/organizations/org-slug/events-spans-performance/',
  119. body: generateSuspectSpansResponse(),
  120. });
  121. // just want to get one span in the response
  122. const badExamples = [generateSuspectSpansResponse({examplesOnly: true})[0]];
  123. for (const example of badExamples[0].examples) {
  124. // make sure that the spans array is empty
  125. example.spans = [];
  126. }
  127. MockApiClient.addMockResponse({
  128. url: '/organizations/org-slug/events-spans/',
  129. body: badExamples,
  130. });
  131. MockApiClient.addMockResponse({
  132. url: '/organizations/org-slug/events-spans-stats/',
  133. body: {
  134. 'percentileArray(spans_exclusive_time, 0.50)': {
  135. data: [
  136. [0, [{count: 0}]],
  137. [10, [{count: 0}]],
  138. ],
  139. order: 2,
  140. start: 0,
  141. end: 10,
  142. },
  143. 'percentileArray(spans_exclusive_time, 0.75)': {
  144. data: [
  145. [0, [{count: 0}]],
  146. [10, [{count: 0}]],
  147. ],
  148. order: 2,
  149. start: 0,
  150. end: 10,
  151. },
  152. 'percentileArray(spans_exclusive_time, 0.95)': {
  153. data: [
  154. [0, [{count: 0}]],
  155. [10, [{count: 0}]],
  156. ],
  157. order: 2,
  158. start: 0,
  159. end: 10,
  160. },
  161. 'percentileArray(spans_exclusive_time, 0.99)': {
  162. data: [
  163. [0, [{count: 0}]],
  164. [10, [{count: 0}]],
  165. ],
  166. order: 2,
  167. start: 0,
  168. end: 10,
  169. },
  170. },
  171. });
  172. const data = initializeData({
  173. features: ['performance-view'],
  174. query: {project: '1', transaction: 'transaction'},
  175. });
  176. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  177. context: data.routerContext,
  178. organization: data.organization,
  179. });
  180. expect(await screen.findByText('Event ID')).toBeInTheDocument();
  181. expect(await screen.findByText('Timestamp')).toBeInTheDocument();
  182. expect(await screen.findByText('Span Duration')).toBeInTheDocument();
  183. expect(await screen.findByText('Count')).toBeInTheDocument();
  184. expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument();
  185. });
  186. });
  187. describe('With Span Data', function () {
  188. beforeEach(function () {
  189. MockApiClient.addMockResponse({
  190. url: '/organizations/org-slug/events-spans-performance/',
  191. body: generateSuspectSpansResponse(),
  192. });
  193. MockApiClient.addMockResponse({
  194. url: '/organizations/org-slug/events-spans/',
  195. body: generateSuspectSpansResponse({examplesOnly: true}),
  196. });
  197. MockApiClient.addMockResponse({
  198. url: '/organizations/org-slug/events-spans-stats/',
  199. body: {
  200. 'percentileArray(spans_exclusive_time, 0.50)': {
  201. data: [
  202. [0, [{count: 0}]],
  203. [10, [{count: 0}]],
  204. ],
  205. order: 2,
  206. start: 0,
  207. end: 10,
  208. },
  209. 'percentileArray(spans_exclusive_time, 0.75)': {
  210. data: [
  211. [0, [{count: 0}]],
  212. [10, [{count: 0}]],
  213. ],
  214. order: 2,
  215. start: 0,
  216. end: 10,
  217. },
  218. 'percentileArray(spans_exclusive_time, 0.95)': {
  219. data: [
  220. [0, [{count: 0}]],
  221. [10, [{count: 0}]],
  222. ],
  223. order: 2,
  224. start: 0,
  225. end: 10,
  226. },
  227. 'percentileArray(spans_exclusive_time, 0.99)': {
  228. data: [
  229. [0, [{count: 0}]],
  230. [10, [{count: 0}]],
  231. ],
  232. order: 2,
  233. start: 0,
  234. end: 10,
  235. },
  236. },
  237. });
  238. });
  239. it('renders header elements', async function () {
  240. const data = initializeData({
  241. features: ['performance-view'],
  242. query: {project: '1', transaction: 'transaction'},
  243. });
  244. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  245. context: data.routerContext,
  246. organization: data.organization,
  247. });
  248. expect(await screen.findByText('Span Summary')).toBeInTheDocument();
  249. const operationNameHeader = await screen.findByTestId('header-operation-name');
  250. expect(
  251. await within(operationNameHeader).findByText('Span Operation')
  252. ).toBeInTheDocument();
  253. // TODO: add an expect for the span description here
  254. expect(
  255. await within(operationNameHeader).findByTestId('operation-name')
  256. ).toHaveTextContent('op');
  257. const percentilesHeader = await screen.findByTestId('header-percentiles');
  258. expect(
  259. await within(percentilesHeader).findByText('Self Time Percentiles')
  260. ).toBeInTheDocument();
  261. const p50Section = await within(percentilesHeader).findByTestId('section-p50');
  262. expect(await within(p50Section).findByText('1.00ms')).toBeInTheDocument();
  263. expect(await within(p50Section).findByText('p50')).toBeInTheDocument();
  264. const p75Section = await within(percentilesHeader).findByTestId('section-p75');
  265. expect(await within(p75Section).findByText('2.00ms')).toBeInTheDocument();
  266. expect(await within(p75Section).findByText('p75')).toBeInTheDocument();
  267. const p95Section = await within(percentilesHeader).findByTestId('section-p95');
  268. expect(await within(p95Section).findByText('3.00ms')).toBeInTheDocument();
  269. expect(await within(p95Section).findByText('p95')).toBeInTheDocument();
  270. const p99Section = await within(percentilesHeader).findByTestId('section-p99');
  271. expect(await within(p99Section).findByText('4.00ms')).toBeInTheDocument();
  272. expect(await within(p99Section).findByText('p99')).toBeInTheDocument();
  273. const frequencyHeader = await screen.findByTestId('header-frequency');
  274. expect(await within(frequencyHeader).findByText('100%')).toBeInTheDocument();
  275. expect(
  276. await within(frequencyHeader).findByText((_content, element) =>
  277. Boolean(
  278. element &&
  279. element.tagName === 'DIV' &&
  280. element.textContent === '1.00 times per event'
  281. )
  282. )
  283. ).toBeInTheDocument();
  284. const totalExclusiveTimeHeader = await screen.findByTestId(
  285. 'header-total-exclusive-time'
  286. );
  287. expect(
  288. await within(totalExclusiveTimeHeader).findByText('5.00ms')
  289. ).toBeInTheDocument();
  290. expect(
  291. await within(totalExclusiveTimeHeader).findByText((_content, element) =>
  292. Boolean(
  293. element && element.tagName === 'DIV' && element.textContent === '1 events'
  294. )
  295. )
  296. ).toBeInTheDocument();
  297. });
  298. it('renders timeseries chart', async function () {
  299. const data = initializeData({
  300. features: ['performance-view'],
  301. query: {project: '1', transaction: 'transaction'},
  302. });
  303. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  304. context: data.routerContext,
  305. organization: data.organization,
  306. });
  307. expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument();
  308. });
  309. it('renders table headers', async function () {
  310. const data = initializeData({
  311. features: ['performance-view'],
  312. query: {project: '1', transaction: 'transaction'},
  313. });
  314. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  315. context: data.routerContext,
  316. organization: data.organization,
  317. });
  318. expect(await screen.findByText('Event ID')).toBeInTheDocument();
  319. expect(await screen.findByText('Timestamp')).toBeInTheDocument();
  320. expect(await screen.findByText('Span Duration')).toBeInTheDocument();
  321. expect(await screen.findByText('Count')).toBeInTheDocument();
  322. expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument();
  323. });
  324. describe('With histogram view feature flag enabled', function () {
  325. const FEATURES = ['performance-view', 'performance-span-histogram-view'];
  326. beforeEach(function () {
  327. MockApiClient.addMockResponse({
  328. url: '/organizations/org-slug/recent-searches/',
  329. method: 'GET',
  330. body: [],
  331. });
  332. });
  333. it('renders a search bar', function () {
  334. const data = initializeData({
  335. features: FEATURES,
  336. query: {project: '1', transaction: 'transaction'},
  337. });
  338. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  339. context: data.routerContext,
  340. organization: data.organization,
  341. });
  342. const searchBarNode = screen.getByPlaceholderText('Filter Transactions');
  343. expect(searchBarNode).toBeInTheDocument();
  344. });
  345. it('disables reset button when no min or max query parameters were set', function () {
  346. const data = initializeData({
  347. features: FEATURES,
  348. query: {project: '1', transaction: 'transaction'},
  349. });
  350. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  351. context: data.routerContext,
  352. organization: data.organization,
  353. });
  354. const resetButton = screen.getByRole('button', {
  355. name: /reset view/i,
  356. });
  357. expect(resetButton).toBeInTheDocument();
  358. expect(resetButton).toBeDisabled();
  359. });
  360. it('enables reset button when min and max are set', function () {
  361. const data = initializeData({
  362. features: FEATURES,
  363. query: {project: '1', transaction: 'transaction', min: '10', max: '100'},
  364. });
  365. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  366. context: data.routerContext,
  367. organization: data.organization,
  368. });
  369. const resetButton = screen.getByRole('button', {
  370. name: /reset view/i,
  371. });
  372. expect(resetButton).toBeEnabled();
  373. });
  374. it('clears min and max query parameters when reset button is clicked', function () {
  375. const data = initializeData({
  376. features: FEATURES,
  377. query: {project: '1', transaction: 'transaction', min: '10', max: '100'},
  378. });
  379. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  380. context: data.routerContext,
  381. organization: data.organization,
  382. });
  383. const resetButton = screen.getByRole('button', {
  384. name: /reset view/i,
  385. });
  386. resetButton.click();
  387. expect(browserHistory.push).toHaveBeenCalledWith(
  388. expect.not.objectContaining({min: expect.any(String), max: expect.any(String)})
  389. );
  390. });
  391. it('does not add aggregate filters to the query', async function () {
  392. const data = initializeData({
  393. features: FEATURES,
  394. query: {project: '1', transaction: 'transaction'},
  395. });
  396. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  397. context: data.routerContext,
  398. organization: data.organization,
  399. });
  400. const searchBarNode = await screen.findByPlaceholderText('Filter Transactions');
  401. userEvent.type(searchBarNode, 'count():>3');
  402. expect(searchBarNode).toHaveTextContent('count():>3');
  403. expect(browserHistory.push).not.toHaveBeenCalled();
  404. });
  405. it('renders a display toggle that changes a chart view between timeseries and histogram by pushing it to the browser history', async function () {
  406. MockApiClient.addMockResponse({
  407. url: '/organizations/org-slug/events-spans-histogram/',
  408. body: [
  409. {bin: 0, count: 0},
  410. {bin: 10, count: 2},
  411. {bin: 20, count: 4},
  412. ],
  413. });
  414. const data = initializeData({
  415. features: FEATURES,
  416. query: {project: '1', transaction: 'transaction'},
  417. });
  418. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  419. context: data.routerContext,
  420. organization: data.organization,
  421. });
  422. expect(await screen.findByTestId('total-value')).toBeInTheDocument();
  423. const chartTitleNodes = await screen.findAllByText('Self Time Breakdown');
  424. expect(chartTitleNodes[0]).toBeInTheDocument();
  425. const displayToggle = await screen.findByTestId('display-toggle');
  426. expect(displayToggle).toBeInTheDocument();
  427. expect(await within(displayToggle).findByRole('button')).toHaveTextContent(
  428. 'Self Time Breakdown'
  429. );
  430. (await within(displayToggle).findByRole('button')).click();
  431. (
  432. await within(displayToggle).findByRole('option', {
  433. name: 'Self Time Distribution',
  434. })
  435. ).click();
  436. expect(browserHistory.push).toHaveBeenCalledWith(
  437. expect.objectContaining({
  438. query: {
  439. display: 'histogram',
  440. project: '1',
  441. transaction: 'transaction',
  442. },
  443. })
  444. );
  445. });
  446. it('renders a histogram when display is set to histogram', async function () {
  447. MockApiClient.addMockResponse({
  448. url: '/organizations/org-slug/events-spans-histogram/',
  449. body: [
  450. {bin: 0, count: 0},
  451. {bin: 10, count: 2},
  452. {bin: 20, count: 4},
  453. ],
  454. });
  455. const data = initializeData({
  456. features: FEATURES,
  457. query: {project: '1', transaction: 'transaction', display: 'histogram'},
  458. });
  459. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  460. context: data.routerContext,
  461. organization: data.organization,
  462. });
  463. const displayToggle = await screen.findByTestId('display-toggle');
  464. expect(await within(displayToggle).findByRole('button')).toHaveTextContent(
  465. 'Self Time Distribution'
  466. );
  467. const nodes = await screen.findAllByText('Self Time Distribution');
  468. expect(nodes[0]).toBeInTheDocument();
  469. });
  470. it('gracefully handles error response', async function () {
  471. MockApiClient.addMockResponse({
  472. url: '/organizations/org-slug/events-spans-histogram/',
  473. statusCode: 400,
  474. });
  475. const data = initializeData({
  476. features: FEATURES,
  477. query: {project: '1', transaction: 'transaction', display: 'histogram'},
  478. });
  479. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  480. context: data.routerContext,
  481. organization: data.organization,
  482. });
  483. expect(await screen.findByTestId('histogram-error-panel')).toBeInTheDocument();
  484. });
  485. it('gracefully renders empty histogram when empty buckets are received', async function () {
  486. MockApiClient.addMockResponse({
  487. url: '/organizations/org-slug/events-spans-histogram/',
  488. body: [
  489. {bin: 0, count: 0},
  490. {bin: 10, count: 0},
  491. {bin: 20, count: 0},
  492. ],
  493. });
  494. const data = initializeData({
  495. features: FEATURES,
  496. query: {project: '1', transaction: 'transaction', display: 'histogram'},
  497. });
  498. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  499. context: data.routerContext,
  500. organization: data.organization,
  501. });
  502. const nodes = await screen.findAllByText('Self Time Distribution');
  503. expect(nodes[0]).toBeInTheDocument();
  504. });
  505. it('sends min and max to span example query', function () {
  506. const mock = MockApiClient.addMockResponse({
  507. url: '/organizations/org-slug/events-spans/',
  508. body: {},
  509. });
  510. const data = initializeData({
  511. features: FEATURES,
  512. query: {project: '1', transaction: 'transaction', min: '10', max: '120'},
  513. });
  514. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  515. context: data.routerContext,
  516. organization: data.organization,
  517. });
  518. expect(mock).toHaveBeenLastCalledWith(
  519. '/organizations/org-slug/events-spans/',
  520. expect.objectContaining({
  521. query: expect.objectContaining({
  522. min_exclusive_time: '10',
  523. max_exclusive_time: '120',
  524. }),
  525. })
  526. );
  527. });
  528. it('sends min and max to suspect spans query', function () {
  529. const mock = MockApiClient.addMockResponse({
  530. url: '/organizations/org-slug/events-spans-performance/',
  531. body: {},
  532. });
  533. const data = initializeData({
  534. features: FEATURES,
  535. query: {project: '1', transaction: 'transaction', min: '10', max: '120'},
  536. });
  537. render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
  538. context: data.routerContext,
  539. organization: data.organization,
  540. });
  541. expect(mock).toHaveBeenLastCalledWith(
  542. '/organizations/org-slug/events-spans-performance/',
  543. expect.objectContaining({
  544. query: expect.objectContaining({
  545. min_exclusive_time: '10',
  546. max_exclusive_time: '120',
  547. }),
  548. })
  549. );
  550. });
  551. });
  552. });
  553. });
  554. describe('spanDetailsRouteWithQuery', function () {
  555. it('should encode slashes in span op', function () {
  556. const target = spanDetailsRouteWithQuery({
  557. orgSlug: 'org-slug',
  558. transaction: 'transaction',
  559. query: {},
  560. spanSlug: {op: 'o/p', group: 'aaaaaaaaaaaaaaaa'},
  561. projectID: '1',
  562. });
  563. expect(target).toEqual(
  564. expect.objectContaining({
  565. pathname:
  566. '/organizations/org-slug/performance/summary/spans/o%2Fp:aaaaaaaaaaaaaaaa/',
  567. })
  568. );
  569. });
  570. });