cacheLandingPage.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary';
  4. import type {Organization} from 'sentry/types/organization';
  5. import {useLocation} from 'sentry/utils/useLocation';
  6. import usePageFilters from 'sentry/utils/usePageFilters';
  7. import useProjects from 'sentry/utils/useProjects';
  8. import {CacheLandingPage} from 'sentry/views/performance/cache/cacheLandingPage';
  9. jest.mock('sentry/utils/useLocation');
  10. jest.mock('sentry/utils/usePageFilters');
  11. jest.mock('sentry/utils/useProjects');
  12. const requestMocks = {
  13. onboardingDataCheck: jest.fn(),
  14. missRateChart: jest.fn(),
  15. cacheSamplesMissRateChart: jest.fn(),
  16. throughputChart: jest.fn(),
  17. spanTransactionList: jest.fn(),
  18. transactionDurations: jest.fn(),
  19. spanFields: jest.fn(),
  20. };
  21. describe('CacheLandingPage', function () {
  22. const organization = OrganizationFixture();
  23. jest.mocked(usePageFilters).mockReturnValue({
  24. isReady: true,
  25. desyncedFilters: new Set(),
  26. pinnedFilters: new Set(),
  27. shouldPersist: true,
  28. selection: {
  29. datetime: {
  30. period: '10d',
  31. start: null,
  32. end: null,
  33. utc: false,
  34. },
  35. environments: [],
  36. projects: [],
  37. },
  38. });
  39. jest.mocked(useLocation).mockReturnValue({
  40. pathname: '',
  41. search: '',
  42. query: {statsPeriod: '10d', project: '1'},
  43. hash: '',
  44. state: undefined,
  45. action: 'PUSH',
  46. key: '',
  47. });
  48. jest.mocked(useProjects).mockReturnValue({
  49. projects: [
  50. ProjectFixture({
  51. id: '1',
  52. name: 'Backend',
  53. slug: 'backend',
  54. firstTransactionEvent: true,
  55. platform: 'javascript',
  56. }),
  57. ],
  58. onSearch: jest.fn(),
  59. placeholders: [],
  60. fetching: false,
  61. hasMore: null,
  62. fetchError: null,
  63. initiallyLoaded: false,
  64. });
  65. beforeEach(function () {
  66. jest.clearAllMocks();
  67. setRequestMocks(organization);
  68. });
  69. afterAll(function () {
  70. jest.resetAllMocks();
  71. });
  72. it('fetches module data', async function () {
  73. render(<CacheLandingPage />, {organization});
  74. await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator'));
  75. expect(requestMocks.missRateChart).toHaveBeenCalledWith(
  76. `/organizations/${organization.slug}/events-stats/`,
  77. expect.objectContaining({
  78. method: 'GET',
  79. query: {
  80. cursor: undefined,
  81. dataset: 'spansMetrics',
  82. environment: [],
  83. excludeOther: 0,
  84. field: [],
  85. interval: '30m',
  86. orderby: undefined,
  87. partial: 1,
  88. per_page: 50,
  89. project: [],
  90. query: 'span.op:[cache.get_item,cache.get] project.id:1',
  91. referrer: 'api.performance.cache.samples-cache-hit-miss-chart',
  92. statsPeriod: '10d',
  93. topEvents: undefined,
  94. yAxis: 'cache_miss_rate()',
  95. },
  96. })
  97. );
  98. expect(requestMocks.throughputChart).toHaveBeenCalledWith(
  99. `/organizations/${organization.slug}/events-stats/`,
  100. expect.objectContaining({
  101. method: 'GET',
  102. query: {
  103. cursor: undefined,
  104. dataset: 'spansMetrics',
  105. environment: [],
  106. excludeOther: 0,
  107. field: [],
  108. interval: '30m',
  109. orderby: undefined,
  110. partial: 1,
  111. per_page: 50,
  112. project: [],
  113. query: 'span.op:[cache.get_item,cache.get]',
  114. referrer: 'api.performance.cache.landing-cache-throughput-chart',
  115. statsPeriod: '10d',
  116. topEvents: undefined,
  117. yAxis: 'spm()',
  118. },
  119. })
  120. );
  121. expect(requestMocks.spanTransactionList).toHaveBeenCalledWith(
  122. `/organizations/${organization.slug}/events/`,
  123. expect.objectContaining({
  124. method: 'GET',
  125. query: {
  126. dataset: 'spansMetrics',
  127. environment: [],
  128. field: [
  129. 'project',
  130. 'project.id',
  131. 'transaction',
  132. 'spm()',
  133. 'cache_miss_rate()',
  134. 'sum(span.self_time)',
  135. 'time_spent_percentage()',
  136. 'avg(cache.item_size)',
  137. ],
  138. per_page: 20,
  139. project: [],
  140. query: 'span.op:[cache.get_item,cache.get]',
  141. referrer: 'api.performance.cache.landing-cache-transaction-list',
  142. sort: '-time_spent_percentage()',
  143. statsPeriod: '10d',
  144. },
  145. })
  146. );
  147. expect(requestMocks.transactionDurations).toHaveBeenCalledWith(
  148. `/organizations/${organization.slug}/events/`,
  149. expect.objectContaining({
  150. method: 'GET',
  151. query: {
  152. dataset: 'metrics',
  153. environment: [],
  154. field: ['avg(transaction.duration)', 'transaction'],
  155. per_page: 50,
  156. project: [],
  157. query: 'transaction:["my-transaction"]',
  158. referrer: 'api.performance.cache.landing-cache-transaction-duration',
  159. statsPeriod: '10d',
  160. },
  161. })
  162. );
  163. });
  164. it('renders a list of transactions', async function () {
  165. render(<CacheLandingPage />, {organization});
  166. await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator'));
  167. expect(screen.getByRole('columnheader', {name: 'Transaction'})).toBeInTheDocument();
  168. expect(screen.getByRole('cell', {name: 'my-transaction'})).toBeInTheDocument();
  169. expect(screen.getByRole('link', {name: 'my-transaction'})).toHaveAttribute(
  170. 'href',
  171. '/organizations/org-slug/insights/caches/?project=123&statsPeriod=10d&transaction=my-transaction'
  172. );
  173. expect(screen.getByRole('columnheader', {name: 'Project'})).toBeInTheDocument();
  174. expect(screen.getByRole('cell', {name: 'backend'})).toBeInTheDocument();
  175. expect(screen.getByRole('link', {name: 'backend'})).toHaveAttribute(
  176. 'href',
  177. '/organizations/org-slug/projects/backend/?project=1'
  178. );
  179. expect(
  180. screen.getByRole('columnheader', {name: 'Avg Value Size'})
  181. ).toBeInTheDocument();
  182. expect(screen.getByRole('cell', {name: '123.0 B'})).toBeInTheDocument();
  183. expect(
  184. screen.getByRole('columnheader', {name: 'Requests Per Minute'})
  185. ).toBeInTheDocument();
  186. expect(screen.getByRole('cell', {name: '123/s'})).toBeInTheDocument();
  187. expect(
  188. screen.getByRole('columnheader', {name: 'Avg Transaction Duration'})
  189. ).toBeInTheDocument();
  190. const avgTxnCell = screen
  191. .getAllByRole('cell')
  192. .find(cell => cell?.textContent?.includes('456.00ms'));
  193. expect(avgTxnCell).toBeInTheDocument();
  194. expect(screen.getByRole('columnheader', {name: 'Miss Rate'})).toBeInTheDocument();
  195. expect(screen.getByRole('cell', {name: '12.3%'})).toBeInTheDocument();
  196. expect(screen.getByRole('columnheader', {name: 'Time Spent'})).toBeInTheDocument();
  197. const timeSpentCell = screen
  198. .getAllByRole('cell')
  199. .find(cell => cell?.textContent?.includes('123.00ms'));
  200. expect(timeSpentCell).toBeInTheDocument();
  201. });
  202. it('shows module onboarding', async function () {
  203. requestMocks.onboardingDataCheck = MockApiClient.addMockResponse({
  204. url: `/organizations/${organization.slug}/events/`,
  205. method: 'GET',
  206. match: [
  207. MockApiClient.matchQuery({
  208. referrer: 'api.performance.cache.landing-cache-onboarding',
  209. }),
  210. ],
  211. body: {
  212. data: [{'count()': 0}],
  213. meta: {fields: {'count()': 'integer'}},
  214. },
  215. });
  216. render(<CacheLandingPage />, {organization});
  217. await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator'));
  218. expect(
  219. screen.getByText('Start collecting Insights about your Caches!')
  220. ).toBeInTheDocument();
  221. });
  222. });
  223. const setRequestMocks = (organization: Organization) => {
  224. MockApiClient.addMockResponse({
  225. url: `/organizations/${organization.slug}/projects/`,
  226. body: [ProjectFixture({name: 'backend'})],
  227. });
  228. requestMocks.onboardingDataCheck = MockApiClient.addMockResponse({
  229. url: `/organizations/${organization.slug}/events/`,
  230. method: 'GET',
  231. match: [
  232. MockApiClient.matchQuery({
  233. referrer: 'api.performance.cache.landing-cache-onboarding',
  234. }),
  235. ],
  236. body: {
  237. data: [{'count()': 43374}],
  238. meta: {
  239. fields: {'count()': 'integer'},
  240. },
  241. },
  242. });
  243. requestMocks.missRateChart = MockApiClient.addMockResponse({
  244. url: `/organizations/${organization.slug}/events-stats/`,
  245. method: 'GET',
  246. match: [
  247. MockApiClient.matchQuery({
  248. referrer: 'api.performance.cache.landing-cache-hit-miss-chart',
  249. }),
  250. ],
  251. body: {
  252. data: [
  253. [1716379200, [{count: 0.5}]],
  254. [1716393600, [{count: 0.75}]],
  255. ],
  256. meta: {
  257. fields: {
  258. time: 'date',
  259. cache_miss_rate: 'percentage',
  260. },
  261. },
  262. },
  263. });
  264. requestMocks.missRateChart = MockApiClient.addMockResponse({
  265. url: `/organizations/${organization.slug}/events-stats/`,
  266. method: 'GET',
  267. match: [
  268. MockApiClient.matchQuery({
  269. referrer: 'api.performance.cache.samples-cache-hit-miss-chart',
  270. }),
  271. ],
  272. body: {
  273. data: [
  274. [1716379200, [{count: 0.5}]],
  275. [1716393600, [{count: 0.75}]],
  276. ],
  277. meta: {
  278. fields: {
  279. time: 'date',
  280. cache_miss_rate: 'percentage',
  281. },
  282. },
  283. },
  284. });
  285. requestMocks.throughputChart = MockApiClient.addMockResponse({
  286. url: `/organizations/${organization.slug}/events-stats/`,
  287. method: 'GET',
  288. match: [
  289. MockApiClient.matchQuery({
  290. referrer: 'api.performance.cache.landing-cache-throughput-chart',
  291. }),
  292. ],
  293. body: {
  294. data: [
  295. [1716379200, [{count: 100}]],
  296. [1716393600, [{count: 200}]],
  297. ],
  298. meta: {
  299. fields: {
  300. time: 'date',
  301. spm_14400: 'rate',
  302. },
  303. },
  304. },
  305. });
  306. requestMocks.spanTransactionList = MockApiClient.addMockResponse({
  307. url: `/organizations/${organization.slug}/events/`,
  308. method: 'GET',
  309. match: [
  310. MockApiClient.matchQuery({
  311. referrer: 'api.performance.cache.landing-cache-transaction-list',
  312. }),
  313. ],
  314. body: {
  315. data: [
  316. {
  317. transaction: 'my-transaction',
  318. project: 'backend',
  319. 'project.id': 123,
  320. 'avg(cache.item_size)': 123,
  321. 'spm()': 123,
  322. 'sum(span.self_time)': 123,
  323. 'cache_miss_rate()': 0.123,
  324. 'time_spent_percentage()': 0.123,
  325. },
  326. ],
  327. meta: {
  328. fields: {
  329. transaction: 'string',
  330. project: 'string',
  331. 'project.id': 'integer',
  332. 'avg(cache.item_size)': 'number',
  333. 'spm()': 'rate',
  334. 'sum(span.self_time)': 'duration',
  335. 'cache_miss_rate()': 'percentage',
  336. 'time_spent_percentage()': 'percentage',
  337. },
  338. units: {},
  339. },
  340. },
  341. });
  342. requestMocks.transactionDurations = MockApiClient.addMockResponse({
  343. url: `/organizations/${organization.slug}/events/`,
  344. method: 'GET',
  345. match: [
  346. MockApiClient.matchQuery({
  347. referrer: 'api.performance.cache.landing-cache-transaction-duration',
  348. }),
  349. ],
  350. body: {
  351. data: [
  352. {
  353. transaction: 'my-transaction',
  354. 'avg(transaction.duration)': 456,
  355. },
  356. ],
  357. meta: {
  358. fields: {
  359. transaction: 'string',
  360. 'avg(transaction.duration)': 'duration',
  361. },
  362. units: {},
  363. },
  364. },
  365. });
  366. requestMocks.spanFields = MockApiClient.addMockResponse({
  367. url: `/organizations/${organization.slug}/spans/fields/`,
  368. method: 'GET',
  369. body: [],
  370. });
  371. };