test_project_settings_sampling.py 27 KB

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