serverSideSampling.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import {
  2. createMemoryHistory,
  3. IndexRoute,
  4. Route,
  5. Router,
  6. RouterContext,
  7. } from 'react-router';
  8. import {
  9. render,
  10. screen,
  11. userEvent,
  12. waitFor,
  13. waitForElementToBeRemoved,
  14. within,
  15. } from 'sentry-test/reactTestingLibrary';
  16. import {ServerSideSamplingStore} from 'sentry/stores/serverSideSamplingStore';
  17. import {Organization, Project} from 'sentry/types';
  18. import {SamplingSdkVersion} from 'sentry/types/sampling';
  19. import {RouteContext} from 'sentry/views/routeContext';
  20. import {SERVER_SIDE_SAMPLING_DOC_LINK} from 'sentry/views/settings/project/server-side-sampling/utils';
  21. import {samplingBreakdownTitle} from './samplingBreakdown.spec';
  22. import {
  23. getMockData,
  24. mockedProjects,
  25. mockedSamplingDistribution,
  26. mockedSamplingSdkVersions,
  27. specificRule,
  28. TestComponent,
  29. uniformRule,
  30. } from './testUtils';
  31. function renderMockRequests({
  32. organizationSlug,
  33. projectSlug,
  34. mockedSdkVersionsResponse = mockedSamplingSdkVersions,
  35. }: {
  36. organizationSlug: Organization['slug'];
  37. projectSlug: Project['slug'];
  38. mockedSdkVersionsResponse?: SamplingSdkVersion[];
  39. }) {
  40. const distribution = MockApiClient.addMockResponse({
  41. url: `/projects/${organizationSlug}/${projectSlug}/dynamic-sampling/distribution/`,
  42. method: 'GET',
  43. body: mockedSamplingDistribution,
  44. });
  45. const sdkVersions = MockApiClient.addMockResponse({
  46. url: `/organizations/${organizationSlug}/dynamic-sampling/sdk-versions/`,
  47. method: 'GET',
  48. body: mockedSdkVersionsResponse,
  49. });
  50. const projects = MockApiClient.addMockResponse({
  51. url: `/organizations/${organizationSlug}/projects/`,
  52. method: 'GET',
  53. body: mockedSamplingDistribution.projectBreakdown!.map(p =>
  54. TestStubs.Project({id: p.projectId, slug: p.project})
  55. ),
  56. });
  57. const statsV2 = MockApiClient.addMockResponse({
  58. url: `/organizations/${organizationSlug}/stats_v2/`,
  59. method: 'GET',
  60. body: TestStubs.Outcomes(),
  61. });
  62. return {distribution, sdkVersions, projects, statsV2};
  63. }
  64. describe('Server-Side Sampling', function () {
  65. beforeEach(() => {
  66. MockApiClient.clearMockResponses();
  67. });
  68. it('renders onboarding promo and open uniform rule modal', async function () {
  69. const {organization, project} = getMockData();
  70. const mockRequests = renderMockRequests({
  71. organizationSlug: organization.slug,
  72. projectSlug: project.slug,
  73. });
  74. const memoryHistory = createMemoryHistory();
  75. memoryHistory.push(
  76. `/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`
  77. );
  78. function Component() {
  79. return <TestComponent organization={organization} project={project} withModal />;
  80. }
  81. const {container} = render(
  82. <Router
  83. history={memoryHistory}
  84. render={props => {
  85. return (
  86. <RouteContext.Provider value={props}>
  87. <RouterContext {...props} />
  88. </RouteContext.Provider>
  89. );
  90. }}
  91. >
  92. <Route
  93. path={`/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`}
  94. >
  95. <IndexRoute component={Component} />
  96. <Route path="rules/:rule/" component={Component} />
  97. </Route>
  98. </Router>
  99. );
  100. expect(screen.getByRole('heading', {name: /Dynamic Sampling/})).toBeInTheDocument();
  101. expect(screen.getByText(/Improve the accuracy of your/)).toBeInTheDocument();
  102. // Assert that project breakdown is there
  103. expect(await screen.findByText(samplingBreakdownTitle)).toBeInTheDocument();
  104. expect(
  105. screen.getByRole('heading', {name: 'Sample for relevancy'})
  106. ).toBeInTheDocument();
  107. expect(
  108. screen.getByText(
  109. 'Create rules to sample transactions under specific conditions, keeping what you need and dropping what you don’t.'
  110. )
  111. ).toBeInTheDocument();
  112. expect(screen.getByRole('button', {name: 'Read Docs'})).toHaveAttribute(
  113. 'href',
  114. SERVER_SIDE_SAMPLING_DOC_LINK
  115. );
  116. // Open Modal
  117. userEvent.click(screen.getByRole('button', {name: 'Start Setup'}));
  118. expect(
  119. await screen.findByRole('heading', {name: 'Set a global sample rate'})
  120. ).toBeInTheDocument();
  121. expect(mockRequests.statsV2).toHaveBeenCalledTimes(2);
  122. expect(mockRequests.distribution).toHaveBeenCalledTimes(1);
  123. expect(mockRequests.sdkVersions).toHaveBeenCalledTimes(1);
  124. // Close Modal
  125. userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
  126. await waitForElementToBeRemoved(() => screen.getByRole('dialog'));
  127. expect(container).toSnapshot();
  128. });
  129. it('renders rules panel', async function () {
  130. const {router, organization, project} = getMockData({
  131. projects: [
  132. TestStubs.Project({
  133. dynamicSampling: {
  134. rules: [{...uniformRule, sampleRate: 1}],
  135. },
  136. }),
  137. ],
  138. });
  139. renderMockRequests({
  140. organizationSlug: organization.slug,
  141. projectSlug: project.slug,
  142. });
  143. const {container} = render(
  144. <TestComponent router={router} organization={organization} project={project} />
  145. );
  146. // Assert that project breakdown is there
  147. expect(await screen.findByText(samplingBreakdownTitle)).toBeInTheDocument();
  148. // Rule Panel Header
  149. expect(screen.getByText('Operator')).toBeInTheDocument();
  150. expect(screen.getByText('Condition')).toBeInTheDocument();
  151. expect(screen.getByText('Rate')).toBeInTheDocument();
  152. expect(screen.getByText('Active')).toBeInTheDocument();
  153. // Rule Panel Content
  154. expect(screen.getAllByTestId('sampling-rule').length).toBe(1);
  155. expect(screen.queryByLabelText('Drag Rule')).not.toBeInTheDocument();
  156. expect(screen.getByTestId('sampling-rule')).toHaveTextContent('If');
  157. expect(screen.getByTestId('sampling-rule')).toHaveTextContent('All');
  158. expect(screen.getByTestId('sampling-rule')).toHaveTextContent('100%');
  159. expect(screen.getByLabelText('Activate Rule')).toBeInTheDocument();
  160. expect(screen.getByLabelText('Actions')).toBeInTheDocument();
  161. // Rule Panel Footer
  162. expect(screen.getByText('Add Rule')).toBeInTheDocument();
  163. expect(screen.getByRole('button', {name: 'Read Docs'})).toHaveAttribute(
  164. 'href',
  165. SERVER_SIDE_SAMPLING_DOC_LINK
  166. );
  167. expect(container).toSnapshot();
  168. });
  169. it('does not let you delete the base rule', async function () {
  170. const {router, organization, project} = getMockData({
  171. projects: [
  172. TestStubs.Project({
  173. dynamicSampling: {
  174. rules: [
  175. {
  176. sampleRate: 0.2,
  177. type: 'trace',
  178. active: false,
  179. condition: {
  180. op: 'and',
  181. inner: [
  182. {
  183. op: 'glob',
  184. name: 'trace.release',
  185. value: ['1.2.3'],
  186. },
  187. ],
  188. },
  189. id: 2,
  190. },
  191. {
  192. sampleRate: 0.2,
  193. type: 'trace',
  194. active: false,
  195. condition: {
  196. op: 'and',
  197. inner: [],
  198. },
  199. id: 1,
  200. },
  201. ],
  202. next_id: 3,
  203. },
  204. }),
  205. ],
  206. });
  207. renderMockRequests({
  208. organizationSlug: organization.slug,
  209. projectSlug: project.slug,
  210. });
  211. render(
  212. <TestComponent router={router} organization={organization} project={project} />
  213. );
  214. // Assert that project breakdown is there (avoids 'act' warnings)
  215. expect(await screen.findByText(samplingBreakdownTitle)).toBeInTheDocument();
  216. userEvent.click(screen.getAllByLabelText('Actions')[0]);
  217. expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toHaveAttribute(
  218. 'aria-disabled',
  219. 'false'
  220. );
  221. userEvent.click(screen.getAllByLabelText('Actions')[0]);
  222. userEvent.click(screen.getAllByLabelText('Actions')[1]);
  223. expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toHaveAttribute(
  224. 'aria-disabled',
  225. 'true'
  226. );
  227. });
  228. it('display "update sdk versions" alert and open "recommended next step" modal', async function () {
  229. const {organization, projects, router} = getMockData({
  230. projects: mockedProjects,
  231. });
  232. const mockRequests = renderMockRequests({
  233. organizationSlug: organization.slug,
  234. projectSlug: projects[2].slug,
  235. });
  236. render(
  237. <TestComponent
  238. organization={organization}
  239. project={projects[2]}
  240. router={router}
  241. withModal
  242. />
  243. );
  244. expect(mockRequests.distribution).toHaveBeenCalled();
  245. await waitFor(() => {
  246. expect(mockRequests.sdkVersions).toHaveBeenCalled();
  247. });
  248. const recommendedSdkUpgradesAlert = await screen.findByTestId(
  249. 'recommended-sdk-upgrades-alert'
  250. );
  251. expect(
  252. within(recommendedSdkUpgradesAlert).getByText(
  253. 'To activate sampling rules, it’s a requirement to update the following project SDK(s):'
  254. )
  255. ).toBeInTheDocument();
  256. expect(
  257. within(recommendedSdkUpgradesAlert).getByRole('link', {
  258. name: mockedProjects[1].slug,
  259. })
  260. ).toHaveAttribute(
  261. 'href',
  262. `/organizations/org-slug/projects/sentry/?project=${mockedProjects[1].id}`
  263. );
  264. // Open Modal
  265. userEvent.click(
  266. within(recommendedSdkUpgradesAlert).getByRole('button', {
  267. name: 'Learn More',
  268. })
  269. );
  270. expect(
  271. await screen.findByRole('heading', {name: 'Important next steps'})
  272. ).toBeInTheDocument();
  273. });
  274. it('open specific conditions modal when adding rule', async function () {
  275. const {project, organization} = getMockData({
  276. projects: [
  277. TestStubs.Project({
  278. dynamicSampling: {
  279. rules: [
  280. {
  281. sampleRate: 1,
  282. type: 'trace',
  283. active: false,
  284. condition: {
  285. op: 'and',
  286. inner: [],
  287. },
  288. id: 1,
  289. },
  290. ],
  291. },
  292. }),
  293. ],
  294. });
  295. const mockRequests = renderMockRequests({
  296. organizationSlug: organization.slug,
  297. projectSlug: project.slug,
  298. });
  299. const memoryHistory = createMemoryHistory();
  300. memoryHistory.push(
  301. `/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`
  302. );
  303. function DynamicSamplingPage() {
  304. return <TestComponent organization={organization} project={project} withModal />;
  305. }
  306. function AlternativePage() {
  307. return <div>alternative page</div>;
  308. }
  309. render(
  310. <Router
  311. history={memoryHistory}
  312. render={props => {
  313. return (
  314. <RouteContext.Provider value={props}>
  315. <RouterContext {...props} />
  316. </RouteContext.Provider>
  317. );
  318. }}
  319. >
  320. <Route
  321. path={`/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`}
  322. >
  323. <IndexRoute component={DynamicSamplingPage} />
  324. <Route path="rules/:rule/" component={DynamicSamplingPage} />
  325. </Route>
  326. <Route path="mock-path" component={AlternativePage} />
  327. </Router>
  328. );
  329. // Store is reset on the first load
  330. expect(ServerSideSamplingStore.getState().projectStats48h.data).toBe(undefined);
  331. expect(ServerSideSamplingStore.getState().projectStats30d.data).toBe(undefined);
  332. expect(ServerSideSamplingStore.getState().distribution.data).toBe(undefined);
  333. expect(ServerSideSamplingStore.getState().sdkVersions.data).toBe(undefined);
  334. // Store is updated with request responses on first load
  335. await waitFor(() => {
  336. expect(ServerSideSamplingStore.getState().sdkVersions.data).not.toBe(undefined);
  337. });
  338. expect(ServerSideSamplingStore.getState().projectStats48h.data).not.toBe(undefined);
  339. expect(ServerSideSamplingStore.getState().projectStats30d.data).not.toBe(undefined);
  340. expect(ServerSideSamplingStore.getState().distribution.data).not.toBe(undefined);
  341. // Open Modal (new route)
  342. userEvent.click(screen.getByLabelText('Add Rule'));
  343. expect(await screen.findByRole('heading', {name: 'Add Rule'})).toBeInTheDocument();
  344. // In a new route, if the store contains the required values, no further requests are sent
  345. expect(mockRequests.statsV2).toHaveBeenCalledTimes(2);
  346. expect(mockRequests.distribution).toHaveBeenCalledTimes(1);
  347. expect(mockRequests.sdkVersions).toHaveBeenCalledTimes(1);
  348. // Leave dynamic sampling's page
  349. memoryHistory.push(`mock-path`);
  350. // When leaving dynamic sampling's page the ServerSideSamplingStore is reset
  351. expect(ServerSideSamplingStore.getState().projectStats48h.data).toBe(undefined);
  352. expect(ServerSideSamplingStore.getState().projectStats30d.data).toBe(undefined);
  353. expect(ServerSideSamplingStore.getState().distribution.data).toBe(undefined);
  354. expect(ServerSideSamplingStore.getState().sdkVersions.data).toBe(undefined);
  355. });
  356. it('does not let user add without permissions', async function () {
  357. const {organization, router, project} = getMockData({
  358. projects: [
  359. TestStubs.Project({
  360. dynamicSampling: {
  361. rules: [uniformRule],
  362. },
  363. }),
  364. ],
  365. access: [],
  366. });
  367. const mockRequests = renderMockRequests({
  368. organizationSlug: organization.slug,
  369. projectSlug: project.slug,
  370. });
  371. render(
  372. <TestComponent organization={organization} project={project} router={router} />
  373. );
  374. expect(screen.getByRole('button', {name: 'Add Rule'})).toBeDisabled();
  375. userEvent.hover(screen.getByText('Add Rule'));
  376. expect(
  377. await screen.findByText("You don't have permission to add a rule")
  378. ).toBeInTheDocument();
  379. expect(mockRequests.distribution).not.toHaveBeenCalled();
  380. expect(mockRequests.sdkVersions).not.toHaveBeenCalled();
  381. });
  382. it('does not let the user activate a rule if sdk updates exists', async function () {
  383. const {organization, router, project} = getMockData({
  384. projects: [
  385. TestStubs.Project({
  386. dynamicSampling: {
  387. rules: [uniformRule],
  388. },
  389. }),
  390. ],
  391. });
  392. renderMockRequests({
  393. organizationSlug: organization.slug,
  394. projectSlug: project.slug,
  395. });
  396. render(
  397. <TestComponent organization={organization} project={project} router={router} />
  398. );
  399. await screen.findByTestId('recommended-sdk-upgrades-alert');
  400. expect(screen.getByRole('checkbox', {name: 'Activate Rule'})).toBeDisabled();
  401. userEvent.hover(screen.getByLabelText('Activate Rule'));
  402. expect(
  403. await screen.findByText(
  404. 'To enable the rule, the recommended sdk version have to be updated'
  405. )
  406. ).toBeInTheDocument();
  407. });
  408. it('does not let the user activate an uniform rule if still processing', async function () {
  409. const {organization, router, project} = getMockData({
  410. projects: [
  411. TestStubs.Project({
  412. dynamicSampling: {
  413. rules: [uniformRule],
  414. },
  415. }),
  416. ],
  417. });
  418. renderMockRequests({
  419. organizationSlug: organization.slug,
  420. projectSlug: project.slug,
  421. mockedSdkVersionsResponse: [],
  422. });
  423. render(
  424. <TestComponent router={router} organization={organization} project={project} />
  425. );
  426. expect(await screen.findByRole('checkbox', {name: 'Activate Rule'})).toBeDisabled();
  427. userEvent.hover(screen.getByLabelText('Activate Rule'));
  428. expect(
  429. await screen.findByText(
  430. 'We are processing sampling information for your project, so you cannot enable the rule yet. Please check again later'
  431. )
  432. ).toBeInTheDocument();
  433. });
  434. it('open uniform rate modal when editing a uniform rule', async function () {
  435. const {organization, project} = getMockData({
  436. projects: [
  437. TestStubs.Project({
  438. dynamicSampling: {
  439. rules: [uniformRule],
  440. },
  441. }),
  442. ],
  443. });
  444. const mockRequests = renderMockRequests({
  445. organizationSlug: organization.slug,
  446. projectSlug: project.slug,
  447. });
  448. const memoryHistory = createMemoryHistory();
  449. memoryHistory.push(
  450. `/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`
  451. );
  452. function Component() {
  453. return <TestComponent organization={organization} project={project} withModal />;
  454. }
  455. render(
  456. <Router
  457. history={memoryHistory}
  458. render={props => {
  459. return (
  460. <RouteContext.Provider value={props}>
  461. <RouterContext {...props} />
  462. </RouteContext.Provider>
  463. );
  464. }}
  465. >
  466. <Route
  467. path={`/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/`}
  468. >
  469. <IndexRoute component={Component} />
  470. <Route path="rules/:rule/" component={Component} />
  471. </Route>
  472. </Router>
  473. );
  474. userEvent.click(await screen.findByLabelText('Actions'));
  475. // Open Modal
  476. userEvent.click(screen.getByLabelText('Edit'));
  477. expect(
  478. await screen.findByRole('heading', {name: 'Set a global sample rate'})
  479. ).toBeInTheDocument();
  480. expect(mockRequests.statsV2).toHaveBeenCalledTimes(2);
  481. expect(mockRequests.distribution).toHaveBeenCalledTimes(1);
  482. expect(mockRequests.sdkVersions).toHaveBeenCalledTimes(1);
  483. });
  484. it('does not let user reorder uniform rule', async function () {
  485. const {organization, router, project} = getMockData({
  486. projects: [
  487. TestStubs.Project({
  488. dynamicSampling: {
  489. rules: [specificRule, uniformRule],
  490. },
  491. }),
  492. ],
  493. });
  494. renderMockRequests({
  495. organizationSlug: organization.slug,
  496. projectSlug: project.slug,
  497. });
  498. render(
  499. <TestComponent
  500. organization={organization}
  501. project={project}
  502. router={router}
  503. withModal
  504. />
  505. );
  506. const samplingUniformRule = screen.getAllByTestId('sampling-rule')[1];
  507. expect(
  508. within(samplingUniformRule).getByRole('button', {name: 'Drag Rule'})
  509. ).toHaveAttribute('aria-disabled', 'true');
  510. userEvent.hover(within(samplingUniformRule).getByLabelText('Drag Rule'));
  511. expect(
  512. await screen.findByText('Uniform rules cannot be reordered')
  513. ).toBeInTheDocument();
  514. });
  515. });