test_generate_rules.py 28 KB

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