content.spec.jsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. import {browserHistory} from 'react-router';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act} from 'sentry-test/reactTestingLibrary';
  5. import * as globalSelection from 'app/actionCreators/globalSelection';
  6. import ProjectsStore from 'app/stores/projectsStore';
  7. import TeamStore from 'app/stores/teamStore';
  8. import {OrganizationContext} from 'app/views/organizationContext';
  9. import PerformanceContent from 'app/views/performance/content';
  10. import {DEFAULT_MAX_DURATION} from 'app/views/performance/trends/utils';
  11. import {vitalAbbreviations} from 'app/views/performance/vitalDetail/utils';
  12. const FEATURES = ['transaction-event', 'performance-view'];
  13. function initializeData(projects, query, features = FEATURES) {
  14. const organization = TestStubs.Organization({
  15. features,
  16. projects,
  17. });
  18. const initialData = initializeOrg({
  19. organization,
  20. router: {
  21. location: {
  22. query: query || {},
  23. },
  24. },
  25. });
  26. act(() => ProjectsStore.loadInitialData(initialData.organization.projects));
  27. return initialData;
  28. }
  29. function initializeTrendsData(query, addDefaultQuery = true) {
  30. const projects = [
  31. TestStubs.Project({id: '1', firstTransactionEvent: false}),
  32. TestStubs.Project({id: '2', firstTransactionEvent: true}),
  33. ];
  34. const organization = TestStubs.Organization({
  35. FEATURES,
  36. projects,
  37. });
  38. const otherTrendsQuery = addDefaultQuery
  39. ? {
  40. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  41. }
  42. : {};
  43. const initialData = initializeOrg({
  44. organization,
  45. router: {
  46. location: {
  47. query: {
  48. ...otherTrendsQuery,
  49. ...query,
  50. },
  51. },
  52. },
  53. });
  54. act(() => ProjectsStore.loadInitialData(initialData.organization.projects));
  55. return initialData;
  56. }
  57. describe('Performance > Content', function () {
  58. beforeEach(function () {
  59. act(() => void TeamStore.loadInitialData([]));
  60. browserHistory.push = jest.fn();
  61. jest.spyOn(globalSelection, 'updateDateTime');
  62. MockApiClient.addMockResponse({
  63. url: '/organizations/org-slug/projects/',
  64. body: [],
  65. });
  66. MockApiClient.addMockResponse({
  67. url: '/organizations/org-slug/tags/',
  68. body: [],
  69. });
  70. MockApiClient.addMockResponse({
  71. url: '/organizations/org-slug/events-stats/',
  72. body: {data: [[123, []]]},
  73. });
  74. MockApiClient.addMockResponse({
  75. url: '/organizations/org-slug/events-histogram/',
  76. body: {'transaction.duration': [{bin: 0, count: 1000}]},
  77. });
  78. MockApiClient.addMockResponse({
  79. url: '/organizations/org-slug/users/',
  80. body: [],
  81. });
  82. MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/recent-searches/',
  84. body: [],
  85. });
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/recent-searches/',
  88. method: 'POST',
  89. body: [],
  90. });
  91. MockApiClient.addMockResponse({
  92. url: '/organizations/org-slug/sdk-updates/',
  93. body: [],
  94. });
  95. MockApiClient.addMockResponse({
  96. url: '/prompts-activity/',
  97. body: {},
  98. });
  99. MockApiClient.addMockResponse(
  100. {
  101. url: '/organizations/org-slug/eventsv2/',
  102. body: {
  103. meta: {
  104. user: 'string',
  105. transaction: 'string',
  106. 'project.id': 'integer',
  107. tpm: 'number',
  108. p50: 'number',
  109. p95: 'number',
  110. failure_rate: 'number',
  111. apdex_300: 'number',
  112. count_unique_user: 'number',
  113. count_miserable_user_300: 'number',
  114. user_misery_300: 'number',
  115. },
  116. data: [
  117. {
  118. transaction: '/apple/cart',
  119. 'project.id': 1,
  120. user: 'uhoh@example.com',
  121. tpm: 30,
  122. p50: 100,
  123. p95: 500,
  124. failure_rate: 0.1,
  125. apdex_300: 0.6,
  126. count_unique_user: 1000,
  127. count_miserable_user_300: 122,
  128. user_misery_300: 0.114,
  129. },
  130. ],
  131. },
  132. },
  133. {
  134. predicate: (_, options) => {
  135. if (!options.hasOwnProperty('query')) {
  136. return false;
  137. } else if (!options.query.hasOwnProperty('field')) {
  138. return false;
  139. }
  140. return !options.query.field.includes('team_key_transaction');
  141. },
  142. }
  143. );
  144. MockApiClient.addMockResponse(
  145. {
  146. url: '/organizations/org-slug/eventsv2/',
  147. body: {
  148. meta: {
  149. user: 'string',
  150. transaction: 'string',
  151. 'project.id': 'integer',
  152. tpm: 'number',
  153. p50: 'number',
  154. p95: 'number',
  155. failure_rate: 'number',
  156. apdex_300: 'number',
  157. count_unique_user: 'number',
  158. count_miserable_user_300: 'number',
  159. user_misery_300: 'number',
  160. },
  161. data: [
  162. {
  163. team_key_transaction: 1,
  164. transaction: '/apple/cart',
  165. 'project.id': 1,
  166. user: 'uhoh@example.com',
  167. tpm: 30,
  168. p50: 100,
  169. p95: 500,
  170. failure_rate: 0.1,
  171. apdex_300: 0.6,
  172. count_unique_user: 1000,
  173. count_miserable_user_300: 122,
  174. user_misery_300: 0.114,
  175. },
  176. {
  177. team_key_transaction: 0,
  178. transaction: '/apple/checkout',
  179. 'project.id': 1,
  180. user: 'uhoh@example.com',
  181. tpm: 30,
  182. p50: 100,
  183. p95: 500,
  184. failure_rate: 0.1,
  185. apdex_300: 0.6,
  186. count_unique_user: 1000,
  187. count_miserable_user_300: 122,
  188. user_misery_300: 0.114,
  189. },
  190. ],
  191. },
  192. },
  193. {
  194. predicate: (_, options) => {
  195. if (!options.hasOwnProperty('query')) {
  196. return false;
  197. } else if (!options.query.hasOwnProperty('field')) {
  198. return false;
  199. }
  200. return options.query.field.includes('team_key_transaction');
  201. },
  202. }
  203. );
  204. MockApiClient.addMockResponse({
  205. url: '/organizations/org-slug/events-meta/',
  206. body: {
  207. count: 2,
  208. },
  209. });
  210. MockApiClient.addMockResponse({
  211. url: '/organizations/org-slug/events-trends/',
  212. body: {
  213. stats: {},
  214. events: {meta: {}, data: []},
  215. },
  216. });
  217. MockApiClient.addMockResponse({
  218. url: '/organizations/org-slug/events-trends-stats/',
  219. body: {
  220. stats: {},
  221. events: {meta: {}, data: []},
  222. },
  223. });
  224. MockApiClient.addMockResponse({
  225. url: '/organizations/org-slug/events-vitals/',
  226. body: {
  227. 'measurements.lcp': {
  228. poor: 1,
  229. meh: 2,
  230. good: 3,
  231. total: 6,
  232. p75: 4500,
  233. },
  234. },
  235. });
  236. MockApiClient.addMockResponse({
  237. method: 'GET',
  238. url: `/organizations/org-slug/key-transactions-list/`,
  239. body: [],
  240. });
  241. });
  242. afterEach(function () {
  243. MockApiClient.clearMockResponses();
  244. act(() => ProjectsStore.reset());
  245. globalSelection.updateDateTime.mockRestore();
  246. });
  247. it('renders basic UI elements', async function () {
  248. const projects = [TestStubs.Project({firstTransactionEvent: true})];
  249. const data = initializeData(projects, {});
  250. const wrapper = mountWithTheme(
  251. <PerformanceContent
  252. organization={data.organization}
  253. location={data.router.location}
  254. />,
  255. data.routerContext
  256. );
  257. await tick();
  258. wrapper.update();
  259. // Check number of rendered tab buttons
  260. expect(wrapper.find('PageHeader Button')).toHaveLength(1);
  261. // No onboarding should show.
  262. expect(wrapper.find('Onboarding')).toHaveLength(0);
  263. // Chart and Table should render.
  264. expect(wrapper.find('ChartFooter')).toHaveLength(1);
  265. expect(wrapper.find('Table')).toHaveLength(1);
  266. });
  267. it('renders onboarding state when the selected project has no events', async function () {
  268. const projects = [
  269. TestStubs.Project({id: 1, firstTransactionEvent: false}),
  270. TestStubs.Project({id: 2, firstTransactionEvent: true}),
  271. ];
  272. const data = initializeData(projects, {project: [1]});
  273. const wrapper = mountWithTheme(
  274. <PerformanceContent
  275. organization={data.organization}
  276. location={data.router.location}
  277. />,
  278. data.routerContext
  279. );
  280. await tick();
  281. wrapper.update();
  282. // onboarding should show.
  283. expect(wrapper.find('Onboarding')).toHaveLength(1);
  284. // Chart and table should not show.
  285. expect(wrapper.find('ChartFooter')).toHaveLength(0);
  286. expect(wrapper.find('Table')).toHaveLength(0);
  287. });
  288. it('does not render onboarding for "my projects"', async function () {
  289. const projects = [
  290. TestStubs.Project({id: '1', firstTransactionEvent: false}),
  291. TestStubs.Project({id: '2', firstTransactionEvent: true}),
  292. ];
  293. const data = initializeData(projects, {project: ['-1']});
  294. const wrapper = mountWithTheme(
  295. <PerformanceContent
  296. organization={data.organization}
  297. location={data.router.location}
  298. />,
  299. data.routerContext
  300. );
  301. await tick();
  302. wrapper.update();
  303. expect(wrapper.find('Onboarding')).toHaveLength(0);
  304. });
  305. it('forwards conditions to transaction summary', async function () {
  306. const projects = [TestStubs.Project({id: '1', firstTransactionEvent: true})];
  307. const data = initializeData(projects, {project: ['1'], query: 'sentry:yes'});
  308. const wrapper = mountWithTheme(
  309. <PerformanceContent
  310. organization={data.organization}
  311. location={data.router.location}
  312. />,
  313. data.routerContext
  314. );
  315. await tick();
  316. wrapper.update();
  317. const link = wrapper.find('[data-test-id="grid-editable"] GridBody Link').at(0);
  318. link.simulate('click', {button: 0});
  319. expect(data.router.push).toHaveBeenCalledWith(
  320. expect.objectContaining({
  321. query: expect.objectContaining({
  322. transaction: '/apple/cart',
  323. query: 'sentry:yes transaction.duration:<15m event.type:transaction',
  324. }),
  325. })
  326. );
  327. });
  328. it('Default period for trends does not call updateDateTime', async function () {
  329. const data = initializeTrendsData({query: 'tag:value'}, false);
  330. const wrapper = mountWithTheme(
  331. <PerformanceContent
  332. organization={data.organization}
  333. location={data.router.location}
  334. />,
  335. data.routerContext
  336. );
  337. await tick();
  338. wrapper.update();
  339. expect(globalSelection.updateDateTime).toHaveBeenCalledTimes(0);
  340. });
  341. it('Navigating to trends does not modify statsPeriod when already set', async function () {
  342. const data = initializeTrendsData({
  343. query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION} api`,
  344. statsPeriod: '24h',
  345. });
  346. const wrapper = mountWithTheme(
  347. <PerformanceContent
  348. organization={data.organization}
  349. location={data.router.location}
  350. />,
  351. data.routerContext
  352. );
  353. await tick();
  354. wrapper.update();
  355. const trendsLink = wrapper.find('[data-test-id="landing-header-trends"]').at(0);
  356. trendsLink.simulate('click');
  357. expect(globalSelection.updateDateTime).toHaveBeenCalledTimes(0);
  358. expect(browserHistory.push).toHaveBeenCalledWith(
  359. expect.objectContaining({
  360. pathname: '/organizations/org-slug/performance/trends/',
  361. query: {
  362. query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  363. statsPeriod: '24h',
  364. },
  365. })
  366. );
  367. });
  368. it('Default page (transactions) without trends feature will not update filters if none are set', async function () {
  369. const projects = [
  370. TestStubs.Project({id: 1, firstTransactionEvent: false}),
  371. TestStubs.Project({id: 2, firstTransactionEvent: true}),
  372. ];
  373. const data = initializeData(projects, {view: undefined});
  374. const wrapper = mountWithTheme(
  375. <PerformanceContent
  376. organization={data.organization}
  377. location={data.router.location}
  378. />,
  379. data.routerContext
  380. );
  381. await tick();
  382. wrapper.update();
  383. expect(browserHistory.push).toHaveBeenCalledTimes(0);
  384. });
  385. it('Default page (transactions) with trends feature will not update filters if none are set', async function () {
  386. const data = initializeTrendsData({view: undefined}, false);
  387. const wrapper = mountWithTheme(
  388. <PerformanceContent
  389. organization={data.organization}
  390. location={data.router.location}
  391. />,
  392. data.routerContext
  393. );
  394. await tick();
  395. wrapper.update();
  396. expect(browserHistory.push).toHaveBeenCalledTimes(0);
  397. });
  398. it('Tags are replaced with trends default query if navigating to trends', async function () {
  399. const data = initializeTrendsData({query: 'device.family:Mac'}, false);
  400. const wrapper = mountWithTheme(
  401. <PerformanceContent
  402. organization={data.organization}
  403. location={data.router.location}
  404. />,
  405. data.routerContext
  406. );
  407. await tick();
  408. wrapper.update();
  409. const trendsLink = wrapper.find('[data-test-id="landing-header-trends"]').at(0);
  410. trendsLink.simulate('click');
  411. expect(browserHistory.push).toHaveBeenCalledWith(
  412. expect.objectContaining({
  413. pathname: '/organizations/org-slug/performance/trends/',
  414. query: {
  415. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  416. },
  417. })
  418. );
  419. });
  420. it('Vitals cards are not shown with overview feature without frontend platform', async function () {
  421. const projects = [TestStubs.Project({id: '1', firstTransactionEvent: true})];
  422. const data = initializeData(projects, {project: ['1'], query: 'sentry:yes'}, [
  423. ...FEATURES,
  424. ]);
  425. const wrapper = mountWithTheme(
  426. <PerformanceContent
  427. organization={data.organization}
  428. location={data.router.location}
  429. />,
  430. data.routerContext
  431. );
  432. await tick();
  433. wrapper.update();
  434. const vitalsContainer = wrapper.find('VitalsContainer');
  435. expect(vitalsContainer).toHaveLength(0);
  436. });
  437. it('Vitals cards are shown with overview feature with frontend platform project', async function () {
  438. const projects = [
  439. TestStubs.Project({
  440. id: '1',
  441. firstTransactionEvent: true,
  442. platform: 'javascript-react',
  443. }),
  444. ];
  445. const data = initializeData(projects, {project: ['1'], query: 'sentry:yes'}, [
  446. ...FEATURES,
  447. ]);
  448. const wrapper = mountWithTheme(
  449. <PerformanceContent
  450. organization={data.organization}
  451. location={data.router.location}
  452. />,
  453. data.routerContext
  454. );
  455. await tick();
  456. wrapper.update();
  457. const vitalsContainer = wrapper.find('VitalsContainer');
  458. expect(vitalsContainer).toHaveLength(1);
  459. const vitalTestIds = Object.values(vitalAbbreviations).map(
  460. abbr => `vitals-linked-card-${abbr}`
  461. );
  462. for (const testId of vitalTestIds) {
  463. const selector = `a[data-test-id="${testId}"]`;
  464. const link = wrapper.find(selector);
  465. expect(link).toHaveLength(1);
  466. }
  467. });
  468. it('Display Create Sample Transaction Button with feature flag on', async function () {
  469. const projects = [
  470. TestStubs.Project({id: 1, firstTransactionEvent: false}),
  471. TestStubs.Project({id: 2, firstTransactionEvent: false}),
  472. ];
  473. const data = initializeData(projects, {view: undefined});
  474. const wrapper = mountWithTheme(
  475. <PerformanceContent
  476. organization={data.organization}
  477. location={data.router.location}
  478. />,
  479. data.routerContext
  480. );
  481. expect(
  482. wrapper.find('Button[data-test-id="create-sample-transaction-btn"]').exists()
  483. ).toBe(true);
  484. });
  485. it('Displays new landing component with feature flag on', async function () {
  486. const projects = [TestStubs.Project({id: 1, firstTransactionEvent: false})];
  487. const data = initializeData(projects, {view: undefined}, [
  488. 'performance-landing-widgets',
  489. ]);
  490. const wrapper = mountWithTheme(
  491. <OrganizationContext.Provider value={data.organization}>
  492. <PerformanceContent
  493. organization={data.organization}
  494. location={data.router.location}
  495. />
  496. </OrganizationContext.Provider>,
  497. data.routerContext
  498. );
  499. expect(wrapper.find('div[data-test-id="performance-landing-v3"]').exists()).toBe(
  500. true
  501. );
  502. });
  503. });