spanDetails.spec.tsx 21 KB

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