test_project_settings_sampling.py 22 KB

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