trends.spec.jsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import React from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {mountWithTheme} from 'sentry-test/enzyme';
  5. import PerformanceLanding from 'app/views/performance/landing';
  6. import ProjectsStore from 'app/stores/projectsStore';
  7. import {
  8. TRENDS_FUNCTIONS,
  9. getTrendAliasedFieldPercentage,
  10. getTrendAliasedQueryPercentage,
  11. getTrendAliasedMinus,
  12. } from 'app/views/performance/trends/utils';
  13. import {TrendFunctionField} from 'app/views/performance/trends/types';
  14. import {getUtcDateString} from 'app/utils/dates';
  15. const trendsViewQuery = {
  16. view: 'TRENDS',
  17. query: 'count():>1000 transaction.duration:>0',
  18. };
  19. jest.mock('moment', () => {
  20. const moment = jest.requireActual('moment');
  21. moment.now = jest.fn().mockReturnValue(1601251200000);
  22. return moment;
  23. });
  24. function selectTrendFunction(wrapper, field) {
  25. const menu = wrapper.find('TrendsDropdown DropdownMenu');
  26. expect(menu).toHaveLength(1);
  27. menu.find('DropdownButton').simulate('click');
  28. const option = menu.find(`DropdownItem[data-test-id="${field}"] span`);
  29. expect(option).toHaveLength(1);
  30. option.simulate('click');
  31. wrapper.update();
  32. }
  33. function initializeData(projects, query) {
  34. const features = ['transaction-event', 'performance-view', 'trends'];
  35. const organization = TestStubs.Organization({
  36. features,
  37. projects,
  38. });
  39. const initialData = initializeOrg({
  40. organization,
  41. router: {
  42. location: {
  43. query: {...trendsViewQuery, ...query},
  44. },
  45. },
  46. });
  47. ProjectsStore.loadInitialData(initialData.organization.projects);
  48. return initialData;
  49. }
  50. describe('Performance > Trends', function() {
  51. let trendsMock;
  52. let trendsStatsMock;
  53. let baselineMock;
  54. beforeEach(function() {
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/projects/',
  57. body: [],
  58. });
  59. MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/tags/',
  61. body: [],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: '/organizations/org-slug/users/',
  65. body: [],
  66. });
  67. MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/recent-searches/',
  69. body: [],
  70. });
  71. MockApiClient.addMockResponse({
  72. url: '/organizations/org-slug/recent-searches/',
  73. method: 'POST',
  74. body: [],
  75. });
  76. MockApiClient.addMockResponse({
  77. url: '/organizations/org-slug/releases/',
  78. body: [],
  79. });
  80. trendsStatsMock = MockApiClient.addMockResponse({
  81. url: '/organizations/org-slug/events-trends-stats/',
  82. body: {
  83. stats: {
  84. 'internal,/organizations/:orgId/performance/': {
  85. data: [[123, []]],
  86. },
  87. order: 0,
  88. },
  89. events: {
  90. meta: {
  91. count_range_1: 'integer',
  92. count_range_2: 'integer',
  93. percentage_count_range_2_count_range_1: 'percentage',
  94. percentage_percentile_range_2_percentile_range_1: 'percentage',
  95. minus_percentile_range_2_percentile_range_1: 'number',
  96. percentile_range_1: 'duration',
  97. percentile_range_2: 'duration',
  98. transaction: 'string',
  99. },
  100. data: [
  101. {
  102. count: 8,
  103. project: 'internal',
  104. count_range_1: 2,
  105. count_range_2: 6,
  106. percentage_count_range_2_count_range_1: 3,
  107. percentage_percentile_range_2_percentile_range_1: 1.9235225955967554,
  108. minus_percentile_range_2_percentile_range_1: 797,
  109. percentile_range_1: 863,
  110. percentile_range_2: 1660,
  111. transaction: '/organizations/:orgId/performance/',
  112. },
  113. {
  114. count: 60,
  115. project: 'internal',
  116. count_range_1: 20,
  117. count_range_2: 40,
  118. percentage_count_range_2_count_range_1: 2,
  119. percentage_percentile_range_2_percentile_range_1: 1.204968944099379,
  120. minus_percentile_range_2_percentile_range_1: 66,
  121. percentile_range_1: 322,
  122. percentile_range_2: 388,
  123. transaction: '/api/0/internal/health/',
  124. },
  125. ],
  126. },
  127. },
  128. });
  129. trendsMock = MockApiClient.addMockResponse({
  130. url: '/organizations/org-slug/events-trends/',
  131. body: {
  132. meta: {
  133. count_range_1: 'integer',
  134. count_range_2: 'integer',
  135. percentage_count_range_2_count_range_1: 'percentage',
  136. percentage_percentile_range_2_percentile_range_1: 'percentage',
  137. minus_percentile_range_2_percentile_range_1: 'number',
  138. percentile_range_1: 'duration',
  139. percentile_range_2: 'duration',
  140. transaction: 'string',
  141. },
  142. data: [
  143. {
  144. count: 8,
  145. project: 'internal',
  146. count_range_1: 2,
  147. count_range_2: 6,
  148. percentage_count_range_2_count_range_1: 3,
  149. percentage_percentile_range_2_percentile_range_1: 1.9235225955967554,
  150. minus_percentile_range_2_percentile_range_1: 797,
  151. percentile_range_1: 863,
  152. percentile_range_2: 1660,
  153. transaction: '/organizations/:orgId/performance/',
  154. },
  155. {
  156. count: 60,
  157. project: 'internal',
  158. count_range_1: 20,
  159. count_range_2: 40,
  160. percentage_count_range_2_count_range_1: 2,
  161. percentage_percentile_range_2_percentile_range_1: 1.204968944099379,
  162. minus_percentile_range_2_percentile_range_1: 66,
  163. percentile_range_1: 322,
  164. percentile_range_2: 388,
  165. transaction: '/api/0/internal/health/',
  166. },
  167. ],
  168. },
  169. });
  170. baselineMock = MockApiClient.addMockResponse({
  171. url: '/organizations/org-slug/event-baseline/',
  172. body: {
  173. project: 'sentry',
  174. id: '66877921c6ff440b8b891d3734f074e7',
  175. },
  176. });
  177. });
  178. afterEach(function() {
  179. MockApiClient.clearMockResponses();
  180. ProjectsStore.reset();
  181. });
  182. it('renders basic UI elements', async function() {
  183. const projects = [TestStubs.Project()];
  184. const data = initializeData(projects, {});
  185. const wrapper = mountWithTheme(
  186. <PerformanceLanding
  187. organization={data.organization}
  188. location={data.router.location}
  189. />,
  190. data.routerContext
  191. );
  192. await tick();
  193. wrapper.update();
  194. // Trends dropdown and transaction widgets should render.
  195. expect(wrapper.find('TrendsDropdown')).toHaveLength(1);
  196. expect(wrapper.find('ChangedTransactions')).toHaveLength(2);
  197. });
  198. it('transaction list items are rendered', async function() {
  199. const projects = [TestStubs.Project()];
  200. const data = initializeData(projects, {project: ['-1']});
  201. const wrapper = mountWithTheme(
  202. <PerformanceLanding
  203. organization={data.organization}
  204. location={data.router.location}
  205. />,
  206. data.routerContext
  207. );
  208. await tick();
  209. wrapper.update();
  210. expect(wrapper.find('TrendsListItem')).toHaveLength(4);
  211. });
  212. it('view summary menu action links to the correct view', async function() {
  213. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  214. const data = initializeData(projects, {project: ['1']});
  215. const wrapper = mountWithTheme(
  216. <PerformanceLanding
  217. organization={data.organization}
  218. location={data.router.location}
  219. />,
  220. data.routerContext
  221. );
  222. await tick();
  223. wrapper.update();
  224. wrapper
  225. .find('DropdownLink')
  226. .first()
  227. .simulate('click');
  228. const firstTransaction = wrapper.find('TrendsListItem').first();
  229. const summaryLink = firstTransaction.find('StyledSummaryLink');
  230. expect(summaryLink).toHaveLength(1);
  231. expect(summaryLink.text()).toEqual('View Summary');
  232. expect(summaryLink.props().to.pathname).toEqual(
  233. '/organizations/org-slug/performance/summary/'
  234. );
  235. expect(summaryLink.props().to.query.project).toEqual(1);
  236. });
  237. it('transaction link with stats period calls comparison view', async function() {
  238. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  239. const data = initializeData(projects, {project: ['1'], statsPeriod: '30d'});
  240. const wrapper = mountWithTheme(
  241. <PerformanceLanding
  242. organization={data.organization}
  243. location={data.router.location}
  244. />,
  245. data.routerContext
  246. );
  247. await tick();
  248. wrapper.update();
  249. const firstTransaction = wrapper.find('TrendsListItem').first();
  250. const transactionLink = firstTransaction.find('CompareLink').first();
  251. transactionLink.simulate('click');
  252. await tick();
  253. wrapper.update();
  254. expect(baselineMock).toHaveBeenNthCalledWith(
  255. 1,
  256. '/organizations/org-slug/event-baseline/',
  257. expect.objectContaining({
  258. query: expect.objectContaining({
  259. baselineValue: 863,
  260. start: '2020-08-29T00:00:00',
  261. end: '2020-09-13T00:00:00',
  262. }),
  263. })
  264. );
  265. expect(baselineMock).toHaveBeenNthCalledWith(
  266. 2,
  267. '/organizations/org-slug/event-baseline/',
  268. expect.objectContaining({
  269. query: expect.objectContaining({
  270. baselineValue: 1660,
  271. start: '2020-09-13T00:00:00',
  272. end: '2020-09-28T00:00:00',
  273. }),
  274. })
  275. );
  276. expect(baselineMock).toHaveBeenCalledTimes(2);
  277. expect(browserHistory.push).toHaveBeenCalledWith({
  278. pathname:
  279. '/organizations/org-slug/performance/compare/sentry:66877921c6ff440b8b891d3734f074e7/sentry:66877921c6ff440b8b891d3734f074e7/',
  280. query: expect.anything(),
  281. });
  282. });
  283. it('transaction link with start and end calls comparison view', async function() {
  284. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  285. const data = initializeData(projects, {
  286. project: ['1'],
  287. start: getUtcDateString(1601164800000),
  288. end: getUtcDateString(1601251200000),
  289. });
  290. const wrapper = mountWithTheme(
  291. <PerformanceLanding
  292. organization={data.organization}
  293. location={data.router.location}
  294. />,
  295. data.routerContext
  296. );
  297. await tick();
  298. wrapper.update();
  299. const firstTransaction = wrapper.find('TrendsListItem').first();
  300. const transactionLink = firstTransaction.find('CompareLink').first();
  301. transactionLink.simulate('click');
  302. await tick();
  303. wrapper.update();
  304. expect(baselineMock).toHaveBeenNthCalledWith(
  305. 1,
  306. '/organizations/org-slug/event-baseline/',
  307. expect.objectContaining({
  308. query: expect.objectContaining({
  309. baselineValue: 863,
  310. start: '2020-09-27T00:00:00',
  311. end: '2020-09-27T12:00:00',
  312. }),
  313. })
  314. );
  315. expect(baselineMock).toHaveBeenNthCalledWith(
  316. 2,
  317. '/organizations/org-slug/event-baseline/',
  318. expect.objectContaining({
  319. query: expect.objectContaining({
  320. baselineValue: 1660,
  321. start: '2020-09-27T12:00:00',
  322. end: '2020-09-28T00:00:00',
  323. }),
  324. })
  325. );
  326. expect(baselineMock).toHaveBeenCalledTimes(2);
  327. });
  328. it('choosing a trend function changes location', async function() {
  329. const projects = [TestStubs.Project()];
  330. const data = initializeData(projects, {project: ['-1']});
  331. const wrapper = mountWithTheme(
  332. <PerformanceLanding
  333. organization={data.organization}
  334. location={data.router.location}
  335. />,
  336. data.routerContext
  337. );
  338. for (const trendFunction of TRENDS_FUNCTIONS) {
  339. selectTrendFunction(wrapper, trendFunction.field);
  340. await tick();
  341. expect(browserHistory.push).toHaveBeenCalledWith({
  342. query: expect.objectContaining({
  343. trendFunction: trendFunction.field,
  344. }),
  345. });
  346. }
  347. });
  348. it('clicking project trend view transactions changes location', async function() {
  349. const projectId = 42;
  350. const projects = [TestStubs.Project({id: projectId, slug: 'internal'})];
  351. const data = initializeData(projects, {project: ['-1']});
  352. const wrapper = mountWithTheme(
  353. <PerformanceLanding
  354. organization={data.organization}
  355. location={data.router.location}
  356. />,
  357. data.routerContext
  358. );
  359. await tick();
  360. wrapper.update();
  361. const mostImprovedProject = wrapper.find('TrendsProjectPanel').first();
  362. const viewTransactions = mostImprovedProject.find('StyledProjectButton').first();
  363. viewTransactions.simulate('click');
  364. expect(browserHistory.push).toHaveBeenCalledWith({
  365. query: expect.objectContaining({
  366. project: [projectId],
  367. }),
  368. });
  369. });
  370. it('viewing a single project will hide the changed project widgets', async function() {
  371. const projectId = 42;
  372. const projects = [TestStubs.Project({id: projectId, slug: 'internal'})];
  373. const data = initializeData(projects, {project: ['42']});
  374. const wrapper = mountWithTheme(
  375. <PerformanceLanding
  376. organization={data.organization}
  377. location={data.router.location}
  378. />,
  379. data.routerContext
  380. );
  381. await tick();
  382. wrapper.update();
  383. const changedProjects = wrapper.find('ChangedProjects');
  384. const changedTransactions = wrapper.find('ChangedTransactions');
  385. expect(changedProjects).toHaveLength(0);
  386. expect(changedTransactions).toHaveLength(2);
  387. });
  388. it('trend functions in location make api calls', async function() {
  389. const projects = [TestStubs.Project(), TestStubs.Project()];
  390. const data = initializeData(projects, {project: ['-1']});
  391. const wrapper = mountWithTheme(
  392. <PerformanceLanding
  393. organization={data.organization}
  394. location={data.router.location}
  395. />,
  396. data.routerContext
  397. );
  398. await tick();
  399. wrapper.update();
  400. for (const trendFunction of TRENDS_FUNCTIONS) {
  401. trendsMock.mockReset();
  402. trendsStatsMock.mockReset();
  403. wrapper.setProps({
  404. location: {query: {...trendsViewQuery, trendFunction: trendFunction.field}},
  405. });
  406. wrapper.update();
  407. await tick();
  408. expect(trendsMock).toHaveBeenCalledTimes(2);
  409. expect(trendsStatsMock).toHaveBeenCalledTimes(2);
  410. const aliasedFieldDivide = getTrendAliasedFieldPercentage(trendFunction.alias);
  411. const aliasedQueryDivide = getTrendAliasedQueryPercentage(trendFunction.alias);
  412. const sort =
  413. trendFunction.field === TrendFunctionField.USER_MISERY
  414. ? getTrendAliasedMinus(trendFunction.alias)
  415. : aliasedFieldDivide;
  416. const defaultTrendsFields = ['project', 'count()'];
  417. const trendFunctionFields = TRENDS_FUNCTIONS.map(({field}) => field);
  418. const transactionFields = [
  419. ...trendFunctionFields,
  420. 'transaction',
  421. ...defaultTrendsFields,
  422. ];
  423. const projectFields = [...trendFunctionFields, ...defaultTrendsFields];
  424. expect(transactionFields).toHaveLength(8);
  425. expect(projectFields).toHaveLength(transactionFields.length - 1);
  426. // Improved projects call
  427. expect(trendsMock).toHaveBeenNthCalledWith(
  428. 1,
  429. expect.anything(),
  430. expect.objectContaining({
  431. query: expect.objectContaining({
  432. trendFunction: trendFunction.field,
  433. sort,
  434. query: expect.stringContaining(aliasedQueryDivide + ':<1'),
  435. interval: '12h',
  436. field: projectFields,
  437. statsPeriod: '14d',
  438. }),
  439. })
  440. );
  441. // Improved transactions call
  442. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  443. 1,
  444. expect.anything(),
  445. expect.objectContaining({
  446. query: expect.objectContaining({
  447. trendFunction: trendFunction.field,
  448. sort,
  449. query: expect.stringContaining(aliasedQueryDivide + ':<1'),
  450. interval: '12h',
  451. field: transactionFields,
  452. statsPeriod: '14d',
  453. }),
  454. })
  455. );
  456. // Regression projects call
  457. expect(trendsMock).toHaveBeenNthCalledWith(
  458. 2,
  459. expect.anything(),
  460. expect.objectContaining({
  461. query: expect.objectContaining({
  462. trendFunction: trendFunction.field,
  463. sort: '-' + sort,
  464. query: expect.stringContaining(aliasedQueryDivide + ':>1'),
  465. interval: '12h',
  466. field: projectFields,
  467. statsPeriod: '14d',
  468. }),
  469. })
  470. );
  471. // Regression transactions call
  472. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  473. 2,
  474. expect.anything(),
  475. expect.objectContaining({
  476. query: expect.objectContaining({
  477. trendFunction: trendFunction.field,
  478. sort: '-' + sort,
  479. query: expect.stringContaining(aliasedQueryDivide + ':>1'),
  480. interval: '12h',
  481. field: transactionFields,
  482. statsPeriod: '14d',
  483. }),
  484. })
  485. );
  486. }
  487. });
  488. });