test_project_settings_sampling.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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(data=saved_sampling_setting)
  127. assert serializer.is_valid()
  128. assert len(serializer.validated_data["rules"]) == 1
  129. assert saved_sampling_setting == serializer.validated_data
  130. assert (
  131. uniform_rule_with_recommended_sampling_values
  132. == serializer.validated_data["rules"][0]
  133. )
  134. def test_add_uniform_rule_with_custom_sampling_values(self):
  135. self.store_outcomes(
  136. {
  137. "org_id": self.org.id,
  138. "timestamp": self.now - timedelta(hours=1),
  139. "project_id": self.project.id,
  140. "outcome": Outcome.ACCEPTED,
  141. "reason": "none",
  142. "category": DataCategory.TRANSACTION,
  143. "quantity": 1,
  144. }
  145. )
  146. with self.feature(FEATURE_NAME):
  147. self.wait_until_page_loaded()
  148. # Open uniform rate modal
  149. self.browser.click_when_visible('[aria-label="Start Setup"]')
  150. self.browser.wait_until('[id="recommended-client-sampling"]')
  151. # Enter a custom value for client side sampling
  152. self.browser.element('[id="recommended-client-sampling"]').clear()
  153. self.browser.element('[id="recommended-client-sampling"]').send_keys(80, Keys.ENTER)
  154. # Enter a custom value for server side sampling
  155. self.browser.element('[id="recommended-server-sampling"]').clear()
  156. self.browser.element('[id="recommended-server-sampling"]').send_keys(50, Keys.ENTER)
  157. # Click on next button
  158. self.browser.click_when_visible('[aria-label="Next"]')
  159. # Click on done button
  160. self.browser.click_when_visible('[aria-label="Done"]')
  161. # Wait the success message to show up
  162. self.browser.wait_until('[data-test-id="toast-success"]')
  163. # Validate the payload
  164. project_option = ProjectOption.objects.get(
  165. key="sentry:dynamic_sampling", project=self.project
  166. )
  167. saved_sampling_setting = project_option.value
  168. serializer = DynamicSamplingSerializer(data=saved_sampling_setting)
  169. assert serializer.is_valid()
  170. assert len(serializer.validated_data["rules"]) == 1
  171. assert saved_sampling_setting == serializer.validated_data
  172. assert uniform_rule_with_custom_sampling_values == serializer.validated_data["rules"][0]
  173. # Validate the audit log
  174. audit_entry = AuditLogEntry.objects.get(
  175. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_ADD")
  176. )
  177. audit_log_event = audit_log.get(audit_entry.event)
  178. assert audit_log_event.render(audit_entry) == "added server-side sampling rule"
  179. # Make sure that the early return logic worked, as only the above audit log was triggered
  180. with pytest.raises(AuditLogEntry.DoesNotExist):
  181. AuditLogEntry.objects.get(
  182. organization=self.org,
  183. target_object=self.project.id,
  184. event=audit_log.get_event_id("PROJECT_EDIT"),
  185. )
  186. def test_remove_specific_rule(self):
  187. with self.feature(FEATURE_NAME):
  188. self.project.update_option(
  189. "sentry:dynamic_sampling",
  190. {
  191. "next_id": 3,
  192. "rules": [
  193. {
  194. **specific_rule_with_all_current_trace_conditions,
  195. "id": 1,
  196. "condition": {
  197. "op": "and",
  198. "inner": [
  199. {
  200. "op": "glob",
  201. "name": "trace.release",
  202. "value": ["[13].[19]"],
  203. }
  204. ],
  205. },
  206. },
  207. {
  208. **uniform_rule_with_recommended_sampling_values,
  209. "id": 2,
  210. },
  211. ],
  212. },
  213. )
  214. self.wait_until_page_loaded()
  215. action = ActionChains(self.browser.driver)
  216. # Click on action button
  217. action_buttons = self.browser.elements('[aria-label="Actions"]')
  218. action.click(action_buttons[0])
  219. action.perform()
  220. # Click on delete button
  221. delete_buttons = self.browser.elements('[data-test-id="delete"]')
  222. action.click(delete_buttons[0])
  223. action.perform()
  224. # Click on confirm button
  225. action.click(self.browser.element('[aria-label="Confirm"]'))
  226. action.perform()
  227. # Wait the success message to show up
  228. self.browser.wait_until('[data-test-id="toast-success"]')
  229. # Validate the audit log
  230. audit_entry = AuditLogEntry.objects.get(
  231. organization=self.org,
  232. event=audit_log.get_event_id("SAMPLING_RULE_REMOVE"),
  233. target_object=self.project.id,
  234. )
  235. audit_log_event = audit_log.get(audit_entry.event)
  236. assert audit_log_event.render(audit_entry) == "deleted server-side sampling rule"
  237. # Make sure that the early return logic worked, as only the above audit log was triggered
  238. with pytest.raises(AuditLogEntry.DoesNotExist):
  239. AuditLogEntry.objects.get(
  240. organization=self.org,
  241. target_object=self.project.id,
  242. event=audit_log.get_event_id("PROJECT_EDIT"),
  243. )
  244. def test_activate_uniform_rule(self):
  245. with self.feature(FEATURE_NAME):
  246. self.project.update_option(
  247. "sentry:dynamic_sampling",
  248. {
  249. "next_id": 2,
  250. "rules": [uniform_rule_with_recommended_sampling_values],
  251. },
  252. )
  253. self.wait_until_page_loaded()
  254. # Click on activate rule button
  255. self.browser.element('[aria-label="Activate Rule"]').click()
  256. # Wait the success message to show up
  257. self.browser.wait_until('[data-test-id="toast-success"]')
  258. # Validate the payload
  259. project_option = ProjectOption.objects.get(
  260. key="sentry:dynamic_sampling", project=self.project
  261. )
  262. saved_sampling_setting = project_option.value
  263. serializer = DynamicSamplingSerializer(data=saved_sampling_setting)
  264. assert serializer.is_valid()
  265. assert len(serializer.validated_data["rules"]) == 1
  266. assert saved_sampling_setting == serializer.validated_data
  267. assert {
  268. **uniform_rule_with_recommended_sampling_values,
  269. "active": True,
  270. "id": 2,
  271. } == serializer.validated_data["rules"][0]
  272. # Validate the audit log
  273. audit_entry = AuditLogEntry.objects.get(
  274. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_ACTIVATE")
  275. )
  276. audit_log_event = audit_log.get(audit_entry.event)
  277. assert audit_log_event.render(audit_entry) == "activated server-side sampling rule"
  278. # Make sure that the early return logic worked, as only the above audit log was triggered
  279. with pytest.raises(AuditLogEntry.DoesNotExist):
  280. AuditLogEntry.objects.get(
  281. organization=self.org,
  282. target_object=self.project.id,
  283. event=audit_log.get_event_id("PROJECT_EDIT"),
  284. )
  285. def test_deactivate_uniform_rule(self):
  286. with self.feature(FEATURE_NAME):
  287. self.project.update_option(
  288. "sentry:dynamic_sampling",
  289. {
  290. "next_id": 2,
  291. "rules": [{**uniform_rule_with_recommended_sampling_values, "active": True}],
  292. },
  293. )
  294. self.wait_until_page_loaded()
  295. # Click on deactivate rule button
  296. self.browser.element('[aria-label="Deactivate Rule"]').click()
  297. # Wait the success message to show up
  298. self.browser.wait_until('[data-test-id="toast-success"]')
  299. # Validate the payload
  300. project_option = ProjectOption.objects.get(
  301. key="sentry:dynamic_sampling", project=self.project
  302. )
  303. saved_sampling_setting = project_option.value
  304. serializer = DynamicSamplingSerializer(data=saved_sampling_setting)
  305. assert serializer.is_valid()
  306. assert len(serializer.validated_data["rules"]) == 1
  307. assert saved_sampling_setting == serializer.validated_data
  308. assert {
  309. **uniform_rule_with_recommended_sampling_values,
  310. "active": False,
  311. "id": 2,
  312. } == serializer.validated_data["rules"][0]
  313. # Validate the audit log
  314. audit_entry = AuditLogEntry.objects.get(
  315. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_DEACTIVATE")
  316. )
  317. audit_log_event = audit_log.get(audit_entry.event)
  318. assert audit_log_event.render(audit_entry) == "deactivated server-side sampling rule"
  319. # Make sure that the early return logic worked, as only the above audit log was triggered
  320. with pytest.raises(AuditLogEntry.DoesNotExist):
  321. AuditLogEntry.objects.get(
  322. organization=self.org,
  323. target_object=self.project.id,
  324. event=audit_log.get_event_id("PROJECT_EDIT"),
  325. )
  326. def test_add_specific_rule(self):
  327. with self.feature(FEATURE_NAME):
  328. self.project.update_option(
  329. "sentry:dynamic_sampling",
  330. {
  331. "next_id": 2,
  332. "rules": [uniform_rule_with_recommended_sampling_values],
  333. },
  334. )
  335. self.wait_until_page_loaded()
  336. # Open specific rule modal
  337. self.browser.element('[aria-label="Add Rule"]').click()
  338. # Open conditions dropdown
  339. self.browser.element('[aria-label="Add Condition"]').click()
  340. # Add Environment
  341. self.browser.element('[data-test-id="trace.environment"]').click()
  342. # Add Release
  343. self.browser.element('[data-test-id="trace.release"]').click()
  344. # Fill in Environment
  345. self.browser.element('[aria-label="Search or add an environment"]').send_keys("prod")
  346. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  347. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  348. Keys.ENTER
  349. )
  350. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  351. "production"
  352. )
  353. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  354. self.browser.element('[aria-label="Search or add an environment"]').send_keys(
  355. Keys.ENTER
  356. )
  357. # Fill in Release
  358. self.browser.element('[aria-label="Search or add a release"]').send_keys("frontend@22*")
  359. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  360. self.browser.element('[aria-label="Search or add a release"]').send_keys(Keys.ENTER)
  361. # Fill in sample rate
  362. self.browser.element('[placeholder="%"]').send_keys("30")
  363. # Save rule
  364. self.browser.element('[aria-label="Save Rule"]').click()
  365. # Wait the success message to show up
  366. self.browser.wait_until('[data-test-id="toast-success"]')
  367. # Take a screenshot
  368. self.browser.snapshot("sampling settings rule with current trace conditions")
  369. # Validate the payload
  370. project_option = ProjectOption.objects.get(
  371. key="sentry:dynamic_sampling", project=self.project
  372. )
  373. saved_sampling_setting = project_option.value
  374. serializer = DynamicSamplingSerializer(data=saved_sampling_setting)
  375. assert serializer.is_valid()
  376. assert len(serializer.validated_data["rules"]) == 2
  377. assert saved_sampling_setting == serializer.validated_data
  378. assert (
  379. specific_rule_with_all_current_trace_conditions
  380. == serializer.validated_data["rules"][0]
  381. )
  382. def test_drag_and_drop_rule_error(self):
  383. with self.feature(FEATURE_NAME):
  384. self.project.update_option(
  385. "sentry:dynamic_sampling",
  386. {
  387. "next_id": 3,
  388. "rules": [
  389. {**specific_rule_with_all_current_trace_conditions, "id": 1},
  390. {**uniform_rule_with_recommended_sampling_values, "id": 2},
  391. ],
  392. },
  393. )
  394. self.wait_until_page_loaded()
  395. # Tries to drag specific rules below an uniform rule
  396. dragHandleSource = self.browser.elements(
  397. '[data-test-id="sampling-rule"] [aria-roledescription="sortable"]'
  398. )[0]
  399. dragHandleTarget = self.browser.elements(
  400. '[data-test-id="sampling-rule"] [aria-roledescription="sortable"]'
  401. )[1]
  402. action = ActionChains(self.browser.driver)
  403. action.drag_and_drop(dragHandleSource, dragHandleTarget)
  404. action.perform()
  405. self.browser.wait_until_test_id("toast-error")
  406. def test_drag_and_drop_rule_success(self):
  407. with self.feature(FEATURE_NAME):
  408. self.project.update_option(
  409. "sentry:dynamic_sampling",
  410. {
  411. "next_id": 4,
  412. "rules": [
  413. {
  414. **specific_rule_with_all_current_trace_conditions,
  415. "id": 1,
  416. "condition": {
  417. "op": "and",
  418. "inner": [
  419. {
  420. "op": "glob",
  421. "name": "trace.release",
  422. "value": ["[13].[19]"],
  423. }
  424. ],
  425. },
  426. },
  427. {
  428. **specific_rule_with_all_current_trace_conditions,
  429. "sampleRate": 0.8,
  430. "id": 2,
  431. "type": "trace",
  432. "condition": {
  433. "op": "and",
  434. "inner": [
  435. {
  436. "op": "eq",
  437. "name": "trace.environment",
  438. "value": ["production"],
  439. "options": {"ignoreCase": True},
  440. }
  441. ],
  442. },
  443. },
  444. {
  445. **uniform_rule_with_recommended_sampling_values,
  446. "id": 3,
  447. },
  448. ],
  449. },
  450. )
  451. self.wait_until_page_loaded()
  452. # Before
  453. rules_before = self.browser.elements('[data-test-id="sampling-rule"]')
  454. assert "Release" in rules_before[0].text
  455. assert "Environment" in rules_before[1].text
  456. drag_handle_source = self.browser.elements('[aria-roledescription="sortable"]')[1]
  457. dragHandleTarget = self.browser.elements('[aria-roledescription="sortable"]')[0]
  458. action = ActionChains(self.browser.driver)
  459. action.drag_and_drop(drag_handle_source, dragHandleTarget)
  460. action.perform()
  461. # Wait the success message to show up
  462. self.browser.wait_until('[data-test-id="toast-success"]')
  463. # After
  464. rulesAfter = self.browser.elements('[data-test-id="sampling-rule"]')
  465. assert "Environment" in rulesAfter[0].text
  466. assert "Release" in rulesAfter[1].text
  467. # Validate the audit log
  468. audit_entry = AuditLogEntry.objects.get(
  469. organization=self.org, event=audit_log.get_event_id("SAMPLING_RULE_EDIT")
  470. )
  471. audit_log_event = audit_log.get(audit_entry.event)
  472. assert audit_log_event.render(audit_entry) == "edited server-side sampling rule"
  473. # Make sure that the early return logic worked, as only the above audit log was triggered
  474. with pytest.raises(AuditLogEntry.DoesNotExist):
  475. AuditLogEntry.objects.get(
  476. organization=self.org,
  477. target_object=self.project.id,
  478. event=audit_log.get_event_id("PROJECT_EDIT"),
  479. )