test_project_settings_sampling.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. from datetime import datetime, timedelta
  2. import pytest
  3. import pytz
  4. import requests
  5. from django.conf import settings
  6. from selenium.webdriver.common.action_chains import ActionChains
  7. from selenium.webdriver.common.keys import Keys
  8. from sentry import audit_log
  9. from sentry.api.endpoints.project_details import DynamicSamplingSerializer
  10. from sentry.constants import DataCategory
  11. from sentry.models import AuditLogEntry, ProjectOption
  12. from sentry.testutils import AcceptanceTestCase
  13. from sentry.testutils.silo import region_silo_test
  14. from sentry.testutils.skips import requires_snuba
  15. from sentry.utils import json
  16. from sentry.utils.outcomes import Outcome
  17. FEATURE_NAME = [
  18. "organizations:server-side-sampling",
  19. "organizations:server-side-sampling-ui",
  20. "organizations:dynamic-sampling-basic",
  21. "organizations:dynamic-sampling-advanced",
  22. ]
  23. uniform_rule_with_recommended_sampling_values = {
  24. "id": 1,
  25. "active": False,
  26. "type": "trace",
  27. "condition": {
  28. "op": "and",
  29. "inner": [],
  30. },
  31. "sampleRate": 0.1,
  32. }
  33. uniform_rule_with_custom_sampling_values = {
  34. "id": 1,
  35. "active": False,
  36. "type": "trace",
  37. "condition": {
  38. "op": "and",
  39. "inner": [],
  40. },
  41. "sampleRate": 0.5,
  42. }
  43. specific_rule_with_all_current_trace_conditions = {
  44. "id": 2,
  45. "type": "trace",
  46. "active": False,
  47. "condition": {
  48. "op": "and",
  49. "inner": [
  50. {
  51. "op": "eq",
  52. "name": "trace.environment",
  53. "value": ["prod", "production"],
  54. "options": {"ignoreCase": True},
  55. },
  56. {"op": "glob", "name": "trace.release", "value": ["frontend@22*"]},
  57. ],
  58. },
  59. "sampleRate": 0.3,
  60. }
  61. @pytest.mark.snuba
  62. @region_silo_test
  63. @requires_snuba
  64. class ProjectSettingsSamplingTest(AcceptanceTestCase):
  65. def setUp(self):
  66. super().setUp()
  67. self.now = datetime(2013, 5, 18, 15, 13, 58, 132928, tzinfo=pytz.utc)
  68. self.user = self.create_user("foo@example.com")
  69. self.org = self.create_organization(name="Rowdy Tiger", owner=None)
  70. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  71. self.project = self.create_project(organization=self.org, teams=[self.team], name="Bengal")
  72. self.project.update_option(
  73. "sentry:dynamic_sampling",
  74. {
  75. "next_id": 1,
  76. "rules": [],
  77. },
  78. )
  79. self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
  80. self.login_as(self.user)
  81. self.path = f"/settings/{self.org.slug}/projects/{self.project.slug}/server-side-sampling/"
  82. assert requests.post(settings.SENTRY_SNUBA + "/tests/outcomes/drop").status_code == 200
  83. def store_outcomes(self, outcome, num_times=1):
  84. outcomes = []
  85. for _ in range(num_times):
  86. outcome_copy = outcome.copy()
  87. outcome_copy["timestamp"] = outcome_copy["timestamp"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")
  88. outcomes.append(outcome_copy)
  89. assert (
  90. requests.post(
  91. settings.SENTRY_SNUBA + "/tests/entities/outcomes/insert", data=json.dumps(outcomes)
  92. ).status_code
  93. == 200
  94. )
  95. def wait_until_page_loaded(self):
  96. self.browser.get(self.path)
  97. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  98. def test_add_uniform_rule_with_recommended_sampling_values(self):
  99. self.store_outcomes(
  100. {
  101. "org_id": self.org.id,
  102. "timestamp": self.now - timedelta(hours=1),
  103. "project_id": self.project.id,
  104. "outcome": Outcome.ACCEPTED,
  105. "reason": "none",
  106. "category": DataCategory.TRANSACTION,
  107. "quantity": 1,
  108. }
  109. )
  110. with self.feature(FEATURE_NAME):
  111. self.wait_until_page_loaded()
  112. # Open uniform rate modal
  113. self.browser.click_when_visible('[aria-label="Start Setup"]')
  114. self.browser.wait_until('[id="recommended-client-sampling"]')
  115. # Click on the recommended sampling values option
  116. self.browser.click_when_visible('[id="sampling-recommended"]')
  117. # Click on done button
  118. self.browser.click_when_visible('[aria-label="Done"]')
  119. # Wait the success message to show up
  120. self.browser.wait_until('[data-test-id="toast-success"]')
  121. # Validate the payload
  122. project_option = ProjectOption.objects.get(
  123. key="sentry:dynamic_sampling", project=self.project
  124. )
  125. saved_sampling_setting = project_option.value
  126. serializer = DynamicSamplingSerializer(
  127. data=saved_sampling_setting,
  128. partial=True,
  129. context={"project": self.project, "request": self.make_request(user=self.user)},
  130. )
  131. assert serializer.is_valid()
  132. assert len(serializer.validated_data["rules"]) == 1
  133. assert saved_sampling_setting == serializer.validated_data
  134. assert (
  135. uniform_rule_with_recommended_sampling_values
  136. == serializer.validated_data["rules"][0]
  137. )
  138. def test_add_uniform_rule_with_custom_sampling_values(self):
  139. self.store_outcomes(
  140. {
  141. "org_id": self.org.id,
  142. "timestamp": self.now - timedelta(hours=1),
  143. "project_id": self.project.id,
  144. "outcome": Outcome.ACCEPTED,
  145. "reason": "none",
  146. "category": DataCategory.TRANSACTION,
  147. "quantity": 1,
  148. }
  149. )
  150. with self.feature(FEATURE_NAME):
  151. self.wait_until_page_loaded()
  152. # Open uniform rate modal
  153. self.browser.click_when_visible('[aria-label="Start Setup"]')
  154. self.browser.wait_until('[id="recommended-client-sampling"]')
  155. # Enter a custom value for client side sampling
  156. self.browser.element('[id="recommended-client-sampling"]').clear()
  157. self.browser.element('[id="recommended-client-sampling"]').send_keys(80, Keys.ENTER)
  158. # Enter a custom value for server side sampling
  159. self.browser.element('[id="recommended-server-sampling"]').clear()
  160. self.browser.element('[id="recommended-server-sampling"]').send_keys(50, Keys.ENTER)
  161. # Click on next button
  162. self.browser.click_when_visible('[aria-label="Next"]')
  163. # Click on done button
  164. self.browser.click_when_visible('[aria-label="Done"]')
  165. # Wait the success message to show up
  166. self.browser.wait_until('[data-test-id="toast-success"]')
  167. # Validate the payload
  168. project_option = ProjectOption.objects.get(
  169. key="sentry:dynamic_sampling", project=self.project
  170. )
  171. saved_sampling_setting = project_option.value
  172. serializer = DynamicSamplingSerializer(
  173. data=saved_sampling_setting,
  174. partial=True,
  175. context={"project": self.project, "request": self.make_request(user=self.user)},
  176. )
  177. assert serializer.is_valid()
  178. assert len(serializer.validated_data["rules"]) == 1
  179. assert saved_sampling_setting == serializer.validated_data
  180. assert uniform_rule_with_custom_sampling_values == serializer.validated_data["rules"][0]
  181. # Validate the audit log
  182. audit_entry = AuditLogEntry.objects.get(
  183. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_ADD")
  184. )
  185. audit_log_event = audit_log.get(audit_entry.event)
  186. assert audit_log_event.render(audit_entry) == "added server-side sampling rule"
  187. # Make sure that the early return logic worked, as only the above audit log was triggered
  188. with pytest.raises(AuditLogEntry.DoesNotExist):
  189. AuditLogEntry.objects.get(
  190. organization=self.org,
  191. target_object=self.project.id,
  192. event=audit_log.get_event_id("PROJECT_EDIT"),
  193. )
  194. def test_remove_specific_rule(self):
  195. with self.feature(FEATURE_NAME):
  196. self.project.update_option(
  197. "sentry:dynamic_sampling",
  198. {
  199. "next_id": 3,
  200. "rules": [
  201. {
  202. **specific_rule_with_all_current_trace_conditions,
  203. "id": 1,
  204. "condition": {
  205. "op": "and",
  206. "inner": [
  207. {
  208. "op": "glob",
  209. "name": "trace.release",
  210. "value": ["[13].[19]"],
  211. }
  212. ],
  213. },
  214. },
  215. {
  216. **uniform_rule_with_recommended_sampling_values,
  217. "id": 2,
  218. },
  219. ],
  220. },
  221. )
  222. self.wait_until_page_loaded()
  223. action = ActionChains(self.browser.driver)
  224. # Click on action button
  225. action_buttons = self.browser.elements('[aria-label="Actions"]')
  226. action.click(action_buttons[0])
  227. action.perform()
  228. # Click on delete button
  229. delete_buttons = self.browser.elements('[data-test-id="delete"]')
  230. action.click(delete_buttons[0])
  231. action.perform()
  232. # Click on confirm button
  233. action.click(self.browser.element('[aria-label="Confirm"]'))
  234. action.perform()
  235. # Wait the success message to show up
  236. self.browser.wait_until('[data-test-id="toast-success"]')
  237. # Validate the audit log
  238. audit_entry = AuditLogEntry.objects.get(
  239. organization=self.org,
  240. event=audit_log.get_event_id("SAMPLING_RULE_REMOVE"),
  241. target_object=self.project.id,
  242. )
  243. audit_log_event = audit_log.get(audit_entry.event)
  244. assert audit_log_event.render(audit_entry) == "deleted server-side sampling rule"
  245. # Make sure that the early return logic worked, as only the above audit log was triggered
  246. with pytest.raises(AuditLogEntry.DoesNotExist):
  247. AuditLogEntry.objects.get(
  248. organization=self.org,
  249. target_object=self.project.id,
  250. event=audit_log.get_event_id("PROJECT_EDIT"),
  251. )
  252. def test_activate_uniform_rule(self):
  253. with self.feature(FEATURE_NAME):
  254. self.project.update_option(
  255. "sentry:dynamic_sampling",
  256. {
  257. "next_id": 2,
  258. "rules": [uniform_rule_with_recommended_sampling_values],
  259. },
  260. )
  261. self.wait_until_page_loaded()
  262. # Click on activate rule button
  263. self.browser.element('[aria-label="Activate Rule"]').click()
  264. # Wait the success message to show up
  265. self.browser.wait_until('[data-test-id="toast-success"]')
  266. # Validate the payload
  267. project_option = ProjectOption.objects.get(
  268. key="sentry:dynamic_sampling", project=self.project
  269. )
  270. saved_sampling_setting = project_option.value
  271. serializer = DynamicSamplingSerializer(
  272. data=saved_sampling_setting,
  273. partial=True,
  274. context={"project": self.project, "request": self.make_request(user=self.user)},
  275. )
  276. assert serializer.is_valid()
  277. assert len(serializer.validated_data["rules"]) == 1
  278. assert saved_sampling_setting == serializer.validated_data
  279. assert {
  280. **uniform_rule_with_recommended_sampling_values,
  281. "active": True,
  282. "id": 2,
  283. } == serializer.validated_data["rules"][0]
  284. # Validate the audit log
  285. audit_entry = AuditLogEntry.objects.get(
  286. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_ACTIVATE")
  287. )
  288. audit_log_event = audit_log.get(audit_entry.event)
  289. assert audit_log_event.render(audit_entry) == "activated server-side sampling rule"
  290. # Make sure that the early return logic worked, as only the above audit log was triggered
  291. with pytest.raises(AuditLogEntry.DoesNotExist):
  292. AuditLogEntry.objects.get(
  293. organization=self.org,
  294. target_object=self.project.id,
  295. event=audit_log.get_event_id("PROJECT_EDIT"),
  296. )
  297. def test_deactivate_uniform_rule(self):
  298. with self.feature(FEATURE_NAME):
  299. self.project.update_option(
  300. "sentry:dynamic_sampling",
  301. {
  302. "next_id": 2,
  303. "rules": [{**uniform_rule_with_recommended_sampling_values, "active": True}],
  304. },
  305. )
  306. self.wait_until_page_loaded()
  307. # Click on deactivate rule button
  308. self.browser.element('[aria-label="Deactivate Rule"]').click()
  309. # Wait the success message to show up
  310. self.browser.wait_until('[data-test-id="toast-success"]')
  311. # Validate the payload
  312. project_option = ProjectOption.objects.get(
  313. key="sentry:dynamic_sampling", project=self.project
  314. )
  315. saved_sampling_setting = project_option.value
  316. serializer = DynamicSamplingSerializer(
  317. data=saved_sampling_setting,
  318. partial=True,
  319. context={"project": self.project, "request": self.make_request(user=self.user)},
  320. )
  321. assert serializer.is_valid()
  322. assert len(serializer.validated_data["rules"]) == 1
  323. assert saved_sampling_setting == serializer.validated_data
  324. assert {
  325. **uniform_rule_with_recommended_sampling_values,
  326. "active": False,
  327. "id": 2,
  328. } == serializer.validated_data["rules"][0]
  329. # Validate the audit log
  330. audit_entry = AuditLogEntry.objects.get(
  331. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_DEACTIVATE")
  332. )
  333. audit_log_event = audit_log.get(audit_entry.event)
  334. assert audit_log_event.render(audit_entry) == "deactivated server-side sampling rule"
  335. # Make sure that the early return logic worked, as only the above audit log was triggered
  336. with pytest.raises(AuditLogEntry.DoesNotExist):
  337. AuditLogEntry.objects.get(
  338. organization=self.org,
  339. target_object=self.project.id,
  340. event=audit_log.get_event_id("PROJECT_EDIT"),
  341. )
  342. def test_add_specific_rule(self):
  343. with self.feature(FEATURE_NAME):
  344. self.project.update_option(
  345. "sentry:dynamic_sampling",
  346. {
  347. "next_id": 2,
  348. "rules": [uniform_rule_with_recommended_sampling_values],
  349. },
  350. )
  351. self.wait_until_page_loaded()
  352. # Open specific rule modal
  353. self.browser.element('[aria-label="Add Rule"]').click()
  354. # Open conditions dropdown
  355. self.browser.element('[aria-label="Add Condition"]').click()
  356. # Add Environment
  357. self.browser.element('[data-test-id="trace.environment"]').click()
  358. # Add Release
  359. self.browser.element('[data-test-id="trace.release"]').click()
  360. # Fill in Environment
  361. self.browser.element('[aria-label="Search or add an environment"]').send_keys("prod")
  362. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  363. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  364. Keys.ENTER
  365. )
  366. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  367. "production"
  368. )
  369. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  370. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  371. Keys.ENTER
  372. )
  373. # Fill in Release
  374. self.browser.element('[aria-label="Search or add a release"]').send_keys("frontend@22*")
  375. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  376. self.browser.element('[aria-label="Search or add a release"]').send_keys(Keys.ENTER)
  377. # Fill in sample rate
  378. self.browser.element('[placeholder="%"]').send_keys("30")
  379. # Save rule
  380. self.browser.element('[aria-label="Save Rule"]').click()
  381. # Wait the success message to show up
  382. self.browser.wait_until('[data-test-id="toast-success"]')
  383. # Take a screenshot
  384. self.browser.snapshot("sampling settings rule with current trace conditions")
  385. # Validate the payload
  386. project_option = ProjectOption.objects.get(
  387. key="sentry:dynamic_sampling", project=self.project
  388. )
  389. saved_sampling_setting = project_option.value
  390. serializer = DynamicSamplingSerializer(
  391. data=saved_sampling_setting,
  392. partial=True,
  393. context={"project": self.project, "request": self.make_request(user=self.user)},
  394. )
  395. assert serializer.is_valid()
  396. assert len(serializer.validated_data["rules"]) == 2
  397. assert saved_sampling_setting == serializer.validated_data
  398. assert (
  399. specific_rule_with_all_current_trace_conditions
  400. == serializer.validated_data["rules"][0]
  401. )
  402. def test_drag_and_drop_rule_error(self):
  403. with self.feature(FEATURE_NAME):
  404. self.project.update_option(
  405. "sentry:dynamic_sampling",
  406. {
  407. "next_id": 3,
  408. "rules": [
  409. {**specific_rule_with_all_current_trace_conditions, "id": 1},
  410. {**uniform_rule_with_recommended_sampling_values, "id": 2},
  411. ],
  412. },
  413. )
  414. self.wait_until_page_loaded()
  415. # Tries to drag specific rules below an uniform rule
  416. dragHandleSource = self.browser.elements(
  417. '[data-test-id="sampling-rule"] [aria-roledescription="sortable"]'
  418. )[0]
  419. dragHandleTarget = self.browser.elements(
  420. '[data-test-id="sampling-rule"] [aria-roledescription="sortable"]'
  421. )[1]
  422. action = ActionChains(self.browser.driver)
  423. action.drag_and_drop(dragHandleSource, dragHandleTarget)
  424. action.perform()
  425. self.browser.wait_until_test_id("toast-error")
  426. def test_drag_and_drop_rule_success(self):
  427. with self.feature(FEATURE_NAME):
  428. self.project.update_option(
  429. "sentry:dynamic_sampling",
  430. {
  431. "next_id": 4,
  432. "rules": [
  433. {
  434. **specific_rule_with_all_current_trace_conditions,
  435. "id": 1,
  436. "condition": {
  437. "op": "and",
  438. "inner": [
  439. {
  440. "op": "glob",
  441. "name": "trace.release",
  442. "value": ["[13].[19]"],
  443. }
  444. ],
  445. },
  446. },
  447. {
  448. **specific_rule_with_all_current_trace_conditions,
  449. "sampleRate": 0.8,
  450. "id": 2,
  451. "type": "trace",
  452. "condition": {
  453. "op": "and",
  454. "inner": [
  455. {
  456. "op": "eq",
  457. "name": "trace.environment",
  458. "value": ["production"],
  459. "options": {"ignoreCase": True},
  460. }
  461. ],
  462. },
  463. },
  464. {
  465. **uniform_rule_with_recommended_sampling_values,
  466. "id": 3,
  467. },
  468. ],
  469. },
  470. )
  471. self.wait_until_page_loaded()
  472. # Before
  473. rules_before = self.browser.elements('[data-test-id="sampling-rule"]')
  474. assert "Release" in rules_before[0].text
  475. assert "Environment" in rules_before[1].text
  476. drag_handle_source = self.browser.elements('[aria-roledescription="sortable"]')[1]
  477. dragHandleTarget = self.browser.elements('[aria-roledescription="sortable"]')[0]
  478. action = ActionChains(self.browser.driver)
  479. action.drag_and_drop(drag_handle_source, dragHandleTarget)
  480. action.perform()
  481. # Wait the success message to show up
  482. self.browser.wait_until('[data-test-id="toast-success"]')
  483. # After
  484. rulesAfter = self.browser.elements('[data-test-id="sampling-rule"]')
  485. assert "Environment" in rulesAfter[0].text
  486. assert "Release" in rulesAfter[1].text
  487. # Validate the audit log
  488. audit_entry = AuditLogEntry.objects.get(
  489. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_EDIT")
  490. )
  491. audit_log_event = audit_log.get(audit_entry.event)
  492. assert audit_log_event.render(audit_entry) == "edited server-side sampling rule"
  493. # Make sure that the early return logic worked, as only the above audit log was triggered
  494. with pytest.raises(AuditLogEntry.DoesNotExist):
  495. AuditLogEntry.objects.get(
  496. organization=self.org,
  497. target_object=self.project.id,
  498. event=audit_log.get_event_id("PROJECT_EDIT"),
  499. )