test_project_settings_sampling_deprecated.py 27 KB

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