test_generate_rules.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. import time
  2. from unittest.mock import MagicMock, patch
  3. import pytest
  4. from freezegun import freeze_time
  5. from sentry_relay.processing import validate_project_config
  6. from sentry.discover.models import TeamKeyTransaction
  7. from sentry.dynamic_sampling import (
  8. BOOSTED_KEY_TRANSACTION_LIMIT,
  9. ENVIRONMENT_GLOBS,
  10. HEALTH_CHECK_GLOBS,
  11. generate_rules,
  12. get_redis_client_for_ds,
  13. )
  14. from sentry.dynamic_sampling.rules.utils import (
  15. KEY_TRANSACTIONS_BOOST_FACTOR,
  16. LATEST_RELEASES_BOOST_DECAYED_FACTOR,
  17. LATEST_RELEASES_BOOST_FACTOR,
  18. RESERVED_IDS,
  19. RuleType,
  20. )
  21. from sentry.models import ProjectTeam
  22. from sentry.testutils.factories import Factories
  23. from sentry.utils import json
  24. DEFAULT_FACTOR_RULE = lambda factor: {
  25. "condition": {"inner": [], "op": "and"},
  26. "id": 1004,
  27. "samplingValue": {"type": "factor", "value": factor},
  28. "type": "trace",
  29. }
  30. @pytest.fixture
  31. def latest_release_only(default_project):
  32. """
  33. This fixture is a hacky way of automatically changing the default project options to use only the latest release
  34. bias.
  35. """
  36. default_project.update_option(
  37. "sentry:dynamic_sampling_biases",
  38. [
  39. {"id": "boostEnvironments", "active": False},
  40. {"id": "ignoreHealthChecks", "active": False},
  41. ],
  42. )
  43. def _validate_rules(project):
  44. rules = generate_rules(project)
  45. # Generate boilerplate around minimal project config:
  46. project_config = {
  47. "allowedDomains": ["*"],
  48. "piiConfig": None,
  49. "trustedRelays": [],
  50. "dynamicSampling": {
  51. "rules": [],
  52. "rulesV2": rules,
  53. "mode": "total",
  54. },
  55. }
  56. validate_project_config(json.dumps(project_config), strict=True)
  57. @patch("sentry.dynamic_sampling.rules.base.sentry_sdk")
  58. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  59. def test_generate_rules_capture_exception(get_blended_sample_rate, sentry_sdk):
  60. get_blended_sample_rate.return_value = None
  61. # since we mock get_blended_sample_rate function
  62. # no need to create real project in DB
  63. fake_project = MagicMock()
  64. # if blended rate is None that means no dynamic sampling behavior should happen.
  65. # Therefore no rules should be set.
  66. assert generate_rules(fake_project) == []
  67. get_blended_sample_rate.assert_called_with(fake_project)
  68. assert sentry_sdk.capture_exception.call_count == 1
  69. _validate_rules(fake_project)
  70. @pytest.mark.django_db
  71. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  72. def test_generate_rules_return_only_uniform_if_sample_rate_is_100_and_other_rules_are_enabled(
  73. get_blended_sample_rate, default_project
  74. ):
  75. get_blended_sample_rate.return_value = 1.0
  76. default_project.update_option(
  77. "sentry:dynamic_sampling_biases",
  78. [
  79. {"id": "boostEnvironments", "active": True},
  80. {"id": "ignoreHealthChecks", "active": True},
  81. {"id": "boostLatestRelease", "active": True},
  82. {"id": "boostKeyTransactions", "active": True},
  83. ],
  84. )
  85. assert generate_rules(default_project) == [
  86. {
  87. "condition": {"inner": [], "op": "and"},
  88. "id": 1000,
  89. "samplingValue": {"type": "sampleRate", "value": 1.0},
  90. "type": "trace",
  91. },
  92. ]
  93. get_blended_sample_rate.assert_called_with(default_project)
  94. _validate_rules(default_project)
  95. @pytest.mark.django_db
  96. @patch("sentry.dynamic_sampling.rules.base.get_enabled_user_biases")
  97. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  98. def test_generate_rules_return_uniform_rules_with_rate(
  99. get_blended_sample_rate, get_enabled_user_biases, default_project
  100. ):
  101. # it means no enabled user biases
  102. get_enabled_user_biases.return_value = {}
  103. get_blended_sample_rate.return_value = 0.1
  104. assert generate_rules(default_project) == [
  105. {
  106. "condition": {"inner": [], "op": "and"},
  107. "id": 1000,
  108. "samplingValue": {"type": "sampleRate", "value": 0.1},
  109. "type": "trace",
  110. },
  111. ]
  112. get_enabled_user_biases.assert_called_with(
  113. default_project.get_option("sentry:dynamic_sampling_biases", None)
  114. )
  115. _validate_rules(default_project)
  116. @pytest.mark.django_db
  117. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  118. def test_generate_rules_return_uniform_rules_and_env_rule(get_blended_sample_rate, default_project):
  119. get_blended_sample_rate.return_value = 0.1
  120. # since we mock get_blended_sample_rate function
  121. # no need to create real project in DB
  122. assert generate_rules(default_project) == [
  123. {
  124. "samplingValue": {"type": "sampleRate", "value": 0.02},
  125. "type": "transaction",
  126. "condition": {
  127. "op": "or",
  128. "inner": [
  129. {
  130. "op": "glob",
  131. "name": "event.transaction",
  132. "value": HEALTH_CHECK_GLOBS,
  133. }
  134. ],
  135. },
  136. "id": 1002,
  137. },
  138. {
  139. "samplingValue": {"type": "sampleRate", "value": 1.0},
  140. "type": "trace",
  141. "condition": {
  142. "op": "or",
  143. "inner": [
  144. {
  145. "op": "glob",
  146. "name": "trace.environment",
  147. "value": ENVIRONMENT_GLOBS,
  148. }
  149. ],
  150. },
  151. "id": 1001,
  152. },
  153. {
  154. "condition": {"inner": [], "op": "and"},
  155. "id": 1000,
  156. "samplingValue": {"type": "sampleRate", "value": 0.1},
  157. "type": "trace",
  158. },
  159. ]
  160. get_blended_sample_rate.assert_called_with(default_project)
  161. _validate_rules(default_project)
  162. @pytest.mark.django_db
  163. @patch("sentry.dynamic_sampling.rules.biases.boost_key_transactions_bias.apply_dynamic_factor")
  164. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  165. def test_generate_rules_return_uniform_rules_and_key_transaction_rule(
  166. get_blended_sample_rate, apply_dynamic_factor, default_project, default_team
  167. ):
  168. get_blended_sample_rate.return_value = 0.1
  169. apply_dynamic_factor.return_value = KEY_TRANSACTIONS_BOOST_FACTOR
  170. default_project.update_option(
  171. "sentry:dynamic_sampling_biases",
  172. [
  173. {"id": "boostEnvironments", "active": False},
  174. {"id": "ignoreHealthChecks", "active": False},
  175. {"id": "boostLatestRelease", "active": False},
  176. {"id": "boostKeyTransactions", "active": True},
  177. ],
  178. )
  179. default_project.add_team(default_team)
  180. TeamKeyTransaction.objects.create(
  181. organization=default_project.organization,
  182. transaction="/foo",
  183. project_team=ProjectTeam.objects.get(project=default_project, team=default_team),
  184. )
  185. assert generate_rules(default_project) == [
  186. {
  187. "condition": {
  188. "inner": [
  189. {
  190. "name": "event.transaction",
  191. "op": "eq",
  192. "options": {"ignoreCase": True},
  193. "value": ["/foo"],
  194. }
  195. ],
  196. "op": "or",
  197. },
  198. "id": 1003,
  199. "samplingValue": {"type": "factor", "value": KEY_TRANSACTIONS_BOOST_FACTOR},
  200. "type": "transaction",
  201. },
  202. {
  203. "condition": {"inner": [], "op": "and"},
  204. "id": 1000,
  205. "samplingValue": {"type": "sampleRate", "value": 0.1},
  206. "type": "trace",
  207. },
  208. ]
  209. get_blended_sample_rate.assert_called_with(default_project)
  210. _validate_rules(default_project)
  211. @pytest.mark.django_db
  212. @patch("sentry.dynamic_sampling.rules.biases.boost_key_transactions_bias.apply_dynamic_factor")
  213. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  214. def test_generate_rules_return_uniform_rules_and_key_transaction_rule_with_dups(
  215. get_blended_sample_rate, apply_dynamic_factor, default_project, default_team
  216. ):
  217. get_blended_sample_rate.return_value = 0.1
  218. apply_dynamic_factor.return_value = KEY_TRANSACTIONS_BOOST_FACTOR
  219. default_project.update_option(
  220. "sentry:dynamic_sampling_biases",
  221. [
  222. {"id": "boostEnvironments", "active": False},
  223. {"id": "ignoreHealthChecks", "active": False},
  224. {"id": "boostLatestRelease", "active": False},
  225. {"id": "boostKeyTransactions", "active": True},
  226. ],
  227. )
  228. team_a = Factories.create_team(organization=default_project.organization, name="Team A")
  229. default_project.add_team(default_team)
  230. default_project.add_team(team_a)
  231. TeamKeyTransaction.objects.create(
  232. organization=default_project.organization,
  233. transaction="/foo",
  234. project_team=ProjectTeam.objects.get(project=default_project, team=default_team),
  235. )
  236. # Let's assume another team for this project selects same transaction
  237. # so we will have dups
  238. TeamKeyTransaction.objects.create(
  239. organization=default_project.organization,
  240. transaction="/foo",
  241. project_team=ProjectTeam.objects.get(project=default_project, team=team_a),
  242. )
  243. assert generate_rules(default_project) == [
  244. {
  245. "condition": {
  246. "inner": [
  247. {
  248. "name": "event.transaction",
  249. "op": "eq",
  250. "options": {"ignoreCase": True},
  251. "value": ["/foo"],
  252. }
  253. ],
  254. "op": "or",
  255. },
  256. "id": 1003,
  257. "samplingValue": {"type": "factor", "value": KEY_TRANSACTIONS_BOOST_FACTOR},
  258. "type": "transaction",
  259. },
  260. {
  261. "condition": {"inner": [], "op": "and"},
  262. "id": 1000,
  263. "samplingValue": {"type": "sampleRate", "value": 0.1},
  264. "type": "trace",
  265. },
  266. ]
  267. get_blended_sample_rate.assert_called_with(default_project)
  268. _validate_rules(default_project)
  269. @pytest.mark.django_db
  270. @patch("sentry.dynamic_sampling.rules.biases.boost_key_transactions_bias.apply_dynamic_factor")
  271. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  272. def test_generate_rules_return_uniform_rules_and_key_transaction_rule_with_many_records(
  273. get_blended_sample_rate, apply_dynamic_factor, default_project, default_team
  274. ):
  275. get_blended_sample_rate.return_value = 0.1
  276. apply_dynamic_factor.return_value = KEY_TRANSACTIONS_BOOST_FACTOR
  277. default_project.update_option(
  278. "sentry:dynamic_sampling_biases",
  279. [
  280. {"id": "boostEnvironments", "active": False},
  281. {"id": "ignoreHealthChecks", "active": False},
  282. {"id": "boostLatestRelease", "active": False},
  283. {"id": "boostKeyTransactions", "active": True},
  284. ],
  285. )
  286. default_project.add_team(default_team)
  287. # Let's create more then transaction limit
  288. for tx_suffix in range(BOOSTED_KEY_TRANSACTION_LIMIT + 1):
  289. TeamKeyTransaction.objects.create(
  290. organization=default_project.organization,
  291. transaction=f"/foo_{tx_suffix:02d}",
  292. project_team=ProjectTeam.objects.get(project=default_project, team=default_team),
  293. )
  294. assert generate_rules(default_project) == [
  295. {
  296. "condition": {
  297. "inner": [
  298. {
  299. "name": "event.transaction",
  300. "op": "eq",
  301. "options": {"ignoreCase": True},
  302. "value": [f"/foo_{i:02d}" for i in range(BOOSTED_KEY_TRANSACTION_LIMIT)],
  303. }
  304. ],
  305. "op": "or",
  306. },
  307. "id": 1003,
  308. "samplingValue": {"type": "factor", "value": KEY_TRANSACTIONS_BOOST_FACTOR},
  309. "type": "transaction",
  310. },
  311. {
  312. "condition": {"inner": [], "op": "and"},
  313. "id": 1000,
  314. "samplingValue": {"type": "sampleRate", "value": 0.1},
  315. "type": "trace",
  316. },
  317. ]
  318. get_blended_sample_rate.assert_called_with(default_project)
  319. _validate_rules(default_project)
  320. @pytest.mark.django_db
  321. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  322. def test_generate_rules_return_uniform_rule_with_100_rate_and_without_env_rule(
  323. get_blended_sample_rate, default_project
  324. ):
  325. get_blended_sample_rate.return_value = 1.0
  326. default_project.update_option(
  327. "sentry:dynamic_sampling_biases",
  328. [
  329. {"id": "boostEnvironments", "active": False},
  330. {"id": "ignoreHealthChecks", "active": False},
  331. {"id": "boostLatestRelease", "active": False},
  332. {"id": "boostKeyTransactions", "active": False},
  333. ],
  334. )
  335. assert generate_rules(default_project) == [
  336. {
  337. "condition": {"inner": [], "op": "and"},
  338. "id": 1000,
  339. "samplingValue": {"type": "sampleRate", "value": 1.0},
  340. "type": "trace",
  341. },
  342. ]
  343. _validate_rules(default_project)
  344. @freeze_time("2022-10-21T18:50:25Z")
  345. @patch("sentry.dynamic_sampling.rules.biases.boost_latest_releases_bias.apply_dynamic_factor")
  346. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  347. @pytest.mark.django_db
  348. @pytest.mark.parametrize(
  349. ["version", "platform", "end"],
  350. [
  351. (version, platform, end)
  352. for version, platform, end in [
  353. ("1.0", "python", "2022-10-21T20:03:03Z"),
  354. ("2.0", None, "2022-10-21T19:50:25Z"),
  355. ]
  356. ],
  357. )
  358. def test_generate_rules_with_different_project_platforms(
  359. get_blended_sample_rate,
  360. apply_dynamic_factor,
  361. version,
  362. platform,
  363. end,
  364. default_project,
  365. latest_release_only,
  366. ):
  367. get_blended_sample_rate.return_value = 0.1
  368. apply_dynamic_factor.return_value = LATEST_RELEASES_BOOST_FACTOR
  369. redis_client = get_redis_client_for_ds()
  370. default_project.update(platform=platform)
  371. release = Factories.create_release(project=default_project, version=version)
  372. environment = "prod"
  373. redis_client.hset(
  374. f"ds::p:{default_project.id}:boosted_releases",
  375. f"ds::r:{release.id}:e:{environment}",
  376. time.time(),
  377. )
  378. assert generate_rules(default_project) == [
  379. {
  380. "samplingValue": {"type": "factor", "value": LATEST_RELEASES_BOOST_FACTOR},
  381. "type": "trace",
  382. "condition": {
  383. "op": "and",
  384. "inner": [
  385. {"op": "eq", "name": "trace.release", "value": [release.version]},
  386. {
  387. "op": "eq",
  388. "name": "trace.environment",
  389. "value": environment,
  390. },
  391. ],
  392. },
  393. "id": 1500,
  394. "timeRange": {
  395. "start": "2022-10-21T18:50:25Z",
  396. "end": end,
  397. },
  398. "decayingFn": {"type": "linear", "decayedValue": LATEST_RELEASES_BOOST_DECAYED_FACTOR},
  399. },
  400. {
  401. "condition": {"inner": [], "op": "and"},
  402. "id": 1000,
  403. "samplingValue": {"type": "sampleRate", "value": 0.1},
  404. "type": "trace",
  405. },
  406. ]
  407. _validate_rules(default_project)
  408. @pytest.mark.django_db
  409. @freeze_time("2022-10-21T18:50:25Z")
  410. @patch("sentry.dynamic_sampling.rules.biases.boost_latest_releases_bias.apply_dynamic_factor")
  411. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  412. def test_generate_rules_return_uniform_rules_and_latest_release_rule(
  413. get_blended_sample_rate, apply_dynamic_factor, default_project, latest_release_only
  414. ):
  415. get_blended_sample_rate.return_value = 0.1
  416. apply_dynamic_factor.return_value = LATEST_RELEASES_BOOST_FACTOR
  417. redis_client = get_redis_client_for_ds()
  418. default_project.update(platform="python")
  419. first_release = Factories.create_release(project=default_project, version="1.0")
  420. for release, environment in (
  421. (first_release, "prod"),
  422. (first_release, "dev"),
  423. (first_release, None),
  424. ):
  425. env_postfix = f":e:{environment}" if environment is not None else ""
  426. redis_client.hset(
  427. f"ds::p:{default_project.id}:boosted_releases",
  428. f"ds::r:{release.id}{env_postfix}",
  429. time.time(),
  430. )
  431. assert generate_rules(default_project) == [
  432. {
  433. "samplingValue": {"type": "factor", "value": LATEST_RELEASES_BOOST_FACTOR},
  434. "type": "trace",
  435. "condition": {
  436. "op": "and",
  437. "inner": [
  438. {"op": "eq", "name": "trace.release", "value": ["1.0"]},
  439. {"op": "eq", "name": "trace.environment", "value": "prod"},
  440. ],
  441. },
  442. "id": 1500,
  443. "timeRange": {"start": "2022-10-21T18:50:25Z", "end": "2022-10-21T20:03:03Z"},
  444. "decayingFn": {"type": "linear", "decayedValue": LATEST_RELEASES_BOOST_DECAYED_FACTOR},
  445. },
  446. {
  447. "samplingValue": {"type": "factor", "value": LATEST_RELEASES_BOOST_FACTOR},
  448. "type": "trace",
  449. "condition": {
  450. "op": "and",
  451. "inner": [
  452. {"op": "eq", "name": "trace.release", "value": ["1.0"]},
  453. {"op": "eq", "name": "trace.environment", "value": "dev"},
  454. ],
  455. },
  456. "id": 1501,
  457. "timeRange": {"start": "2022-10-21T18:50:25Z", "end": "2022-10-21T20:03:03Z"},
  458. "decayingFn": {"type": "linear", "decayedValue": LATEST_RELEASES_BOOST_DECAYED_FACTOR},
  459. },
  460. {
  461. "samplingValue": {"type": "factor", "value": LATEST_RELEASES_BOOST_FACTOR},
  462. "type": "trace",
  463. "condition": {
  464. "op": "and",
  465. "inner": [
  466. {"op": "eq", "name": "trace.release", "value": ["1.0"]},
  467. {"op": "eq", "name": "trace.environment", "value": None},
  468. ],
  469. },
  470. "id": 1502,
  471. "timeRange": {"start": "2022-10-21T18:50:25Z", "end": "2022-10-21T20:03:03Z"},
  472. "decayingFn": {"type": "linear", "decayedValue": LATEST_RELEASES_BOOST_DECAYED_FACTOR},
  473. },
  474. {
  475. "condition": {"inner": [], "op": "and"},
  476. "id": 1000,
  477. "samplingValue": {"type": "sampleRate", "value": 0.1},
  478. "type": "trace",
  479. },
  480. ]
  481. _validate_rules(default_project)
  482. @pytest.mark.django_db
  483. @freeze_time("2022-10-21T18:50:25Z")
  484. @patch("sentry.dynamic_sampling.rules.biases.boost_latest_releases_bias.apply_dynamic_factor")
  485. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  486. def test_generate_rules_does_not_return_rule_with_deleted_release(
  487. get_blended_sample_rate, apply_dynamic_factor, default_project, latest_release_only
  488. ):
  489. get_blended_sample_rate.return_value = 0.1
  490. apply_dynamic_factor.return_value = LATEST_RELEASES_BOOST_FACTOR
  491. redis_client = get_redis_client_for_ds()
  492. default_project.update(platform="python")
  493. first_release = Factories.create_release(project=default_project, version="1.0")
  494. second_release = Factories.create_release(project=default_project, version="2.0")
  495. redis_client.hset(
  496. f"ds::p:{default_project.id}:boosted_releases",
  497. f"ds::r:{first_release.id}",
  498. time.time(),
  499. )
  500. redis_client.hset(
  501. f"ds::p:{default_project.id}:boosted_releases",
  502. f"ds::r:{second_release.id}",
  503. time.time(),
  504. )
  505. second_release.delete()
  506. assert generate_rules(default_project) == [
  507. {
  508. "samplingValue": {"type": "factor", "value": LATEST_RELEASES_BOOST_FACTOR},
  509. "type": "trace",
  510. "condition": {
  511. "op": "and",
  512. "inner": [
  513. {"op": "eq", "name": "trace.release", "value": ["1.0"]},
  514. {"op": "eq", "name": "trace.environment", "value": None},
  515. ],
  516. },
  517. "id": 1500,
  518. "timeRange": {"start": "2022-10-21T18:50:25Z", "end": "2022-10-21T20:03:03Z"},
  519. "decayingFn": {"type": "linear", "decayedValue": LATEST_RELEASES_BOOST_DECAYED_FACTOR},
  520. },
  521. {
  522. "condition": {"inner": [], "op": "and"},
  523. "id": 1000,
  524. "samplingValue": {"type": "sampleRate", "value": 0.1},
  525. "type": "trace",
  526. },
  527. ]
  528. _validate_rules(default_project)
  529. @pytest.mark.django_db
  530. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  531. def test_generate_rules_return_uniform_rule_with_100_rate_and_without_latest_release_rule(
  532. get_blended_sample_rate, default_project, latest_release_only
  533. ):
  534. get_blended_sample_rate.return_value = 1.0
  535. default_project.update(platform="python")
  536. assert generate_rules(default_project) == [
  537. {
  538. "condition": {"inner": [], "op": "and"},
  539. "id": 1000,
  540. "samplingValue": {"type": "sampleRate", "value": 1.0},
  541. "type": "trace",
  542. },
  543. ]
  544. _validate_rules(default_project)
  545. @pytest.mark.django_db
  546. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  547. def test_generate_rules_return_uniform_rule_with_non_existent_releases(
  548. get_blended_sample_rate, default_project, latest_release_only
  549. ):
  550. get_blended_sample_rate.return_value = 1.0
  551. redis_client = get_redis_client_for_ds()
  552. redis_client.hset(f"ds::p:{default_project.id}:boosted_releases", f"ds::r:{1234}", time.time())
  553. assert generate_rules(default_project) == [
  554. {
  555. "condition": {"inner": [], "op": "and"},
  556. "id": 1000,
  557. "samplingValue": {"type": "sampleRate", "value": 1.0},
  558. "type": "trace",
  559. },
  560. ]
  561. _validate_rules(default_project)
  562. @pytest.mark.django_db
  563. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  564. def test_generate_rules_with_zero_base_sample_rate(get_blended_sample_rate, default_project):
  565. get_blended_sample_rate.return_value = 0.0
  566. default_project.update_option(
  567. "sentry:dynamic_sampling_biases",
  568. [
  569. {"id": "boostEnvironments", "active": True},
  570. {"id": "ignoreHealthChecks", "active": True},
  571. {"id": "boostLatestRelease", "active": True},
  572. {"id": "boostKeyTransactions", "active": True},
  573. ],
  574. )
  575. assert generate_rules(default_project) == [
  576. {
  577. "condition": {"inner": [], "op": "and"},
  578. "id": 1000,
  579. "samplingValue": {"type": "sampleRate", "value": 0.0},
  580. "type": "trace",
  581. },
  582. ]
  583. get_blended_sample_rate.assert_called_with(default_project)
  584. _validate_rules(default_project)
  585. @pytest.mark.django_db
  586. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  587. @patch(
  588. "sentry.dynamic_sampling.rules.biases.boost_rare_transactions_rule.get_transactions_resampling_rates"
  589. )
  590. def test_generate_rules_return_uniform_rules_and_low_volume_transactions_rules(
  591. get_transactions_resampling_rates, get_blended_sample_rate, default_project, default_team
  592. ):
  593. project_sample_rate = 0.1
  594. t1_rate = 0.7
  595. implicit_rate = 0.037
  596. get_blended_sample_rate.return_value = project_sample_rate
  597. get_transactions_resampling_rates.return_value = {
  598. "t1": t1_rate,
  599. }, implicit_rate
  600. uniform_id = RESERVED_IDS[RuleType.UNIFORM_RULE]
  601. default_project.update_option(
  602. "sentry:dynamic_sampling_biases",
  603. [
  604. {"id": "boostEnvironments", "active": False},
  605. {"id": "ignoreHealthChecks", "active": False},
  606. {"id": "boostLatestRelease", "active": False},
  607. {"id": "boostKeyTransactions", "active": False},
  608. {"id": RuleType.BOOST_LOW_VOLUME_TRANSACTIONS.value, "active": True},
  609. ],
  610. )
  611. default_project.add_team(default_team)
  612. TeamKeyTransaction.objects.create(
  613. organization=default_project.organization,
  614. transaction="/foo",
  615. project_team=ProjectTeam.objects.get(project=default_project, team=default_team),
  616. )
  617. rules = generate_rules(default_project)
  618. implicit_rate /= project_sample_rate
  619. t1_rate /= project_sample_rate
  620. t1_rate /= implicit_rate
  621. assert rules == [
  622. {
  623. "condition": {"inner": [], "op": "and"},
  624. "id": uniform_id,
  625. "samplingValue": {"type": "sampleRate", "value": project_sample_rate},
  626. "type": "trace",
  627. },
  628. ]
  629. get_blended_sample_rate.assert_called_with(default_project)
  630. _validate_rules(default_project)
  631. @pytest.mark.django_db
  632. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  633. @patch(
  634. "sentry.dynamic_sampling.rules.biases.boost_rare_transactions_rule.get_transactions_resampling_rates"
  635. )
  636. def test_low_volume_transactions_rules_not_returned_when_inactive(
  637. get_transactions_resampling_rates, get_blended_sample_rate, default_project, default_team
  638. ):
  639. get_blended_sample_rate.return_value = 0.1
  640. get_transactions_resampling_rates.return_value = {
  641. "t1": 0.7,
  642. }, 0.037
  643. uniform_id = RESERVED_IDS[RuleType.UNIFORM_RULE]
  644. default_project.update_option(
  645. "sentry:dynamic_sampling_biases",
  646. [
  647. {"id": "boostEnvironments", "active": False},
  648. {"id": "ignoreHealthChecks", "active": False},
  649. {"id": "boostLatestRelease", "active": False},
  650. {"id": "boostKeyTransactions", "active": False},
  651. {"id": RuleType.BOOST_LOW_VOLUME_TRANSACTIONS.value, "active": False},
  652. ],
  653. )
  654. default_project.add_team(default_team)
  655. TeamKeyTransaction.objects.create(
  656. organization=default_project.organization,
  657. transaction="/foo",
  658. project_team=ProjectTeam.objects.get(project=default_project, team=default_team),
  659. )
  660. rules = generate_rules(default_project)
  661. # we should have only the uniform rule
  662. assert len(rules) == 1
  663. assert rules[0]["id"] == uniform_id
  664. @pytest.mark.django_db
  665. @freeze_time("2022-10-21T18:50:25Z")
  666. @patch("sentry.dynamic_sampling.rules.base.quotas.get_blended_sample_rate")
  667. def test_generate_rules_return_uniform_rules_and_adj_factor_rule(
  668. get_blended_sample_rate, default_project
  669. ):
  670. get_blended_sample_rate.return_value = 0.1
  671. redis_client = get_redis_client_for_ds()
  672. default_project.update_option(
  673. "sentry:dynamic_sampling_biases",
  674. [
  675. {"id": "boostEnvironments", "active": False},
  676. {"id": "ignoreHealthChecks", "active": False},
  677. {"id": "boostLatestRelease", "active": False},
  678. {"id": "boostKeyTransactions", "active": False},
  679. {"id": RuleType.BOOST_LOW_VOLUME_TRANSACTIONS.value, "active": False},
  680. ],
  681. )
  682. # Set factor
  683. default_factor = 0.5
  684. redis_client.hset(
  685. f"ds::o:{default_project.organization.id}:rate_rebalance_factor",
  686. f"{default_project.id}",
  687. default_factor,
  688. )
  689. assert generate_rules(default_project) == [
  690. DEFAULT_FACTOR_RULE(default_factor),
  691. {
  692. "condition": {"inner": [], "op": "and"},
  693. "id": 1000,
  694. "samplingValue": {"type": "sampleRate", "value": 0.1},
  695. "type": "trace",
  696. },
  697. ]
  698. _validate_rules(default_project)