index.spec.tsx 22 KB

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