content.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  5. import * as pageFilters from 'sentry/actionCreators/pageFilters';
  6. import OrganizationStore from 'sentry/stores/organizationStore';
  7. import ProjectsStore from 'sentry/stores/projectsStore';
  8. import TeamStore from 'sentry/stores/teamStore';
  9. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  10. import type {Project} from 'sentry/types/project';
  11. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  12. import PerformanceContent from 'sentry/views/performance/content';
  13. import {DEFAULT_MAX_DURATION} from 'sentry/views/performance/trends/utils';
  14. const FEATURES = ['performance-view'];
  15. function WrappedComponent({router}: {router: InjectedRouter}) {
  16. return (
  17. <MEPSettingProvider>
  18. <PerformanceContent router={router} location={router.location} />
  19. </MEPSettingProvider>
  20. );
  21. }
  22. function initializeData(
  23. projects: Project[],
  24. query: Record<string, string | string[] | undefined>,
  25. features = FEATURES
  26. ) {
  27. const organization = OrganizationFixture({
  28. features,
  29. });
  30. const initialData = initializeOrg({
  31. projects,
  32. organization,
  33. router: {
  34. location: {
  35. pathname: '/test',
  36. query: query || {},
  37. },
  38. },
  39. });
  40. act(() => void OrganizationStore.onUpdate(initialData.organization, {replace: true}));
  41. act(() => ProjectsStore.loadInitialData(initialData.projects));
  42. return initialData;
  43. }
  44. function initializeTrendsData(
  45. query: Record<string, string | string[] | undefined>,
  46. addDefaultQuery = true
  47. ) {
  48. const projects = [
  49. ProjectFixture({id: '1', firstTransactionEvent: false}),
  50. ProjectFixture({id: '2', firstTransactionEvent: true}),
  51. ];
  52. const organization = OrganizationFixture({features: FEATURES});
  53. const otherTrendsQuery = addDefaultQuery
  54. ? {
  55. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  56. }
  57. : {};
  58. const initialData = initializeOrg({
  59. organization,
  60. projects,
  61. router: {
  62. location: {
  63. pathname: '/test',
  64. query: {
  65. ...otherTrendsQuery,
  66. ...query,
  67. },
  68. },
  69. },
  70. });
  71. act(() => ProjectsStore.loadInitialData(initialData.projects));
  72. return initialData;
  73. }
  74. describe('Performance > Content', function () {
  75. beforeEach(function () {
  76. act(() => void TeamStore.loadInitialData([], false, null));
  77. jest.spyOn(pageFilters, 'updateDateTime');
  78. MockApiClient.addMockResponse({
  79. url: '/organizations/org-slug/projects/',
  80. body: [],
  81. });
  82. MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/tags/',
  84. body: [],
  85. });
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/events-stats/',
  88. body: {data: [[123, []]]},
  89. });
  90. MockApiClient.addMockResponse({
  91. url: '/organizations/org-slug/events-histogram/',
  92. body: {'transaction.duration': [{bin: 0, count: 1000}]},
  93. });
  94. MockApiClient.addMockResponse({
  95. url: '/organizations/org-slug/users/',
  96. body: [],
  97. });
  98. MockApiClient.addMockResponse({
  99. url: '/organizations/org-slug/recent-searches/',
  100. body: [],
  101. });
  102. MockApiClient.addMockResponse({
  103. url: '/organizations/org-slug/recent-searches/',
  104. method: 'POST',
  105. body: [],
  106. });
  107. MockApiClient.addMockResponse({
  108. url: '/organizations/org-slug/sdk-updates/',
  109. body: [],
  110. });
  111. MockApiClient.addMockResponse({
  112. url: '/organizations/org-slug/prompts-activity/',
  113. body: {},
  114. });
  115. MockApiClient.addMockResponse({
  116. url: '/organizations/org-slug/events/',
  117. body: {
  118. meta: {
  119. fields: {
  120. user: 'string',
  121. transaction: 'string',
  122. 'project.id': 'integer',
  123. 'tpm()': 'number',
  124. 'p50()': 'number',
  125. 'p95()': 'number',
  126. 'failure_rate()': 'number',
  127. 'apdex(300)': 'number',
  128. 'count_unique(user)': 'number',
  129. 'count_miserable(user,300)': 'number',
  130. 'user_misery(300)': 'number',
  131. },
  132. },
  133. data: [
  134. {
  135. transaction: '/apple/cart',
  136. 'project.id': 1,
  137. user: 'uhoh@example.com',
  138. 'tpm()': 30,
  139. 'p50()': 100,
  140. 'p95()': 500,
  141. 'failure_rate()': 0.1,
  142. 'apdex(300)': 0.6,
  143. 'count_unique(user)': 1000,
  144. 'count_miserable(user,300)': 122,
  145. 'user_misery(300)': 0.114,
  146. },
  147. ],
  148. },
  149. match: [
  150. (_, options) => {
  151. if (!options.hasOwnProperty('query')) {
  152. return false;
  153. }
  154. if (!options.query?.hasOwnProperty('field')) {
  155. return false;
  156. }
  157. return !options.query?.field.includes('team_key_transaction');
  158. },
  159. ],
  160. });
  161. MockApiClient.addMockResponse({
  162. url: '/organizations/org-slug/events/',
  163. body: {
  164. meta: {
  165. fields: {
  166. user: 'string',
  167. transaction: 'string',
  168. 'project.id': 'integer',
  169. 'tpm()': 'number',
  170. 'p50()': 'number',
  171. 'p95()': 'number',
  172. 'failure_rate()': 'number',
  173. 'apdex(300)': 'number',
  174. 'count_unique(user)': 'number',
  175. 'count_miserable(user,300)': 'number',
  176. 'user_misery(300)': 'number',
  177. },
  178. },
  179. data: [
  180. {
  181. team_key_transaction: 1,
  182. transaction: '/apple/cart',
  183. 'project.id': 1,
  184. user: 'uhoh@example.com',
  185. 'tpm()': 30,
  186. 'p50()': 100,
  187. 'p95()': 500,
  188. 'failure_rate()': 0.1,
  189. 'apdex(300)': 0.6,
  190. 'count_unique(user)': 1000,
  191. 'count_miserable(user,300)': 122,
  192. 'user_misery(300)': 0.114,
  193. },
  194. {
  195. team_key_transaction: 0,
  196. transaction: '/apple/checkout',
  197. 'project.id': 1,
  198. user: 'uhoh@example.com',
  199. 'tpm()': 30,
  200. 'p50()': 100,
  201. 'p95()': 500,
  202. 'failure_rate()': 0.1,
  203. 'apdex(300)': 0.6,
  204. 'count_unique(user)': 1000,
  205. 'count_miserable(user,300)': 122,
  206. 'user_misery(300)': 0.114,
  207. },
  208. ],
  209. },
  210. match: [
  211. (_, options) => {
  212. if (!options.hasOwnProperty('query')) {
  213. return false;
  214. }
  215. if (!options.query?.hasOwnProperty('field')) {
  216. return false;
  217. }
  218. return options.query?.field.includes('team_key_transaction');
  219. },
  220. ],
  221. });
  222. MockApiClient.addMockResponse({
  223. url: '/organizations/org-slug/events-meta/',
  224. body: {
  225. count: 2,
  226. },
  227. });
  228. MockApiClient.addMockResponse({
  229. url: '/organizations/org-slug/events-trends/',
  230. body: {
  231. stats: {},
  232. events: {meta: {}, data: []},
  233. },
  234. });
  235. MockApiClient.addMockResponse({
  236. url: '/organizations/org-slug/events-trends-stats/',
  237. body: {
  238. stats: {},
  239. events: {meta: {}, data: []},
  240. },
  241. });
  242. MockApiClient.addMockResponse({
  243. url: '/organizations/org-slug/events-vitals/',
  244. body: {
  245. 'measurements.lcp': {
  246. poor: 1,
  247. meh: 2,
  248. good: 3,
  249. total: 6,
  250. p75: 4500,
  251. },
  252. },
  253. });
  254. MockApiClient.addMockResponse({
  255. method: 'GET',
  256. url: `/organizations/org-slug/key-transactions-list/`,
  257. body: [],
  258. });
  259. });
  260. afterEach(function () {
  261. MockApiClient.clearMockResponses();
  262. act(() => ProjectsStore.reset());
  263. // @ts-expect-error // TODO: This was likely a defensive check added due to a previous isolation issue, it can possibly be removed.
  264. pageFilters.updateDateTime.mockRestore();
  265. });
  266. it('renders basic UI elements', async function () {
  267. const projects = [ProjectFixture({firstTransactionEvent: true})];
  268. const {router} = initializeData(projects, {});
  269. render(<WrappedComponent router={router} />, {
  270. router,
  271. });
  272. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  273. expect(screen.getByTestId('performance-table')).toBeInTheDocument();
  274. expect(screen.queryByText('Pinpoint problems')).not.toBeInTheDocument();
  275. });
  276. it('renders onboarding state when the selected project has no events', async function () {
  277. const projects = [
  278. ProjectFixture({id: '1', firstTransactionEvent: false}),
  279. ProjectFixture({id: '2', firstTransactionEvent: true}),
  280. ];
  281. const {router} = initializeData(projects, {project: ['1']});
  282. render(<WrappedComponent router={router} />, {
  283. router,
  284. });
  285. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  286. expect(screen.getByText('Pinpoint problems')).toBeInTheDocument();
  287. expect(screen.queryByTestId('performance-table')).not.toBeInTheDocument();
  288. });
  289. it('does not render onboarding for "my projects"', async function () {
  290. const projects = [
  291. ProjectFixture({id: '1', firstTransactionEvent: false}),
  292. ProjectFixture({id: '2', firstTransactionEvent: true}),
  293. ];
  294. const {router} = initializeData(projects, {project: ['-1']});
  295. render(<WrappedComponent router={router} />, {
  296. router,
  297. });
  298. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  299. expect(screen.queryByText('Pinpoint problems')).not.toBeInTheDocument();
  300. });
  301. it('forwards conditions to transaction summary', async function () {
  302. const projects = [ProjectFixture({id: '1', firstTransactionEvent: true})];
  303. const {router} = initializeData(projects, {project: ['1'], query: 'sentry:yes'});
  304. render(<WrappedComponent router={router} />, {
  305. router,
  306. });
  307. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  308. const link = screen.getByRole('link', {name: '/apple/cart'});
  309. await userEvent.click(link);
  310. expect(router.push).toHaveBeenCalledWith(
  311. expect.objectContaining({
  312. query: expect.objectContaining({
  313. transaction: '/apple/cart',
  314. query: 'sentry:yes',
  315. }),
  316. })
  317. );
  318. });
  319. it('Default period for trends does not call updateDateTime', async function () {
  320. const {router} = initializeTrendsData({query: 'tag:value'}, false);
  321. render(<WrappedComponent router={router} />, {
  322. router,
  323. });
  324. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  325. expect(pageFilters.updateDateTime).toHaveBeenCalledTimes(0);
  326. });
  327. it('Navigating to trends does not modify statsPeriod when already set', async function () {
  328. const {router} = initializeTrendsData({
  329. query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION} api`,
  330. statsPeriod: '24h',
  331. });
  332. render(<WrappedComponent router={router} />, {
  333. router,
  334. });
  335. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  336. const link = screen.getByRole('button', {name: 'View Trends'});
  337. await userEvent.click(link);
  338. expect(pageFilters.updateDateTime).toHaveBeenCalledTimes(0);
  339. expect(router.push).toHaveBeenCalledWith(
  340. expect.objectContaining({
  341. pathname: '/organizations/org-slug/performance/trends/',
  342. query: {
  343. query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  344. statsPeriod: '24h',
  345. },
  346. })
  347. );
  348. });
  349. it('Default page (transactions) without trends feature will not update filters if none are set', async function () {
  350. const projects = [
  351. ProjectFixture({id: '1', firstTransactionEvent: false}),
  352. ProjectFixture({id: '2', firstTransactionEvent: true}),
  353. ];
  354. const {router} = initializeData(projects, {view: undefined});
  355. render(<WrappedComponent router={router} />, {
  356. router,
  357. });
  358. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  359. expect(router.push).toHaveBeenCalledTimes(0);
  360. });
  361. it('Default page (transactions) with trends feature will not update filters if none are set', async function () {
  362. const {router} = initializeTrendsData({view: undefined}, false);
  363. render(<WrappedComponent router={router} />, {
  364. router,
  365. });
  366. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  367. expect(router.push).toHaveBeenCalledTimes(0);
  368. });
  369. it('Tags are replaced with trends default query if navigating to trends', async function () {
  370. const {router} = initializeTrendsData({query: 'device.family:Mac'}, false);
  371. render(<WrappedComponent router={router} />, {
  372. router,
  373. });
  374. const trendsLinks = await screen.findAllByTestId('landing-header-trends');
  375. await userEvent.click(trendsLinks[0]!);
  376. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  377. expect(router.push).toHaveBeenCalledWith(
  378. expect.objectContaining({
  379. pathname: '/organizations/org-slug/performance/trends/',
  380. query: {
  381. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  382. },
  383. })
  384. );
  385. });
  386. it('Display Create Sample Transaction Button', async function () {
  387. const projects = [
  388. ProjectFixture({id: '1', firstTransactionEvent: false}),
  389. ProjectFixture({id: '2', firstTransactionEvent: false}),
  390. ];
  391. const {router} = initializeData(projects, {view: undefined});
  392. render(<WrappedComponent router={router} />, {
  393. router,
  394. });
  395. expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument();
  396. expect(screen.getByTestId('create-sample-transaction-btn')).toBeInTheDocument();
  397. });
  398. });