test_event_frequency.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. import time
  2. from copy import deepcopy
  3. from datetime import timedelta
  4. from unittest.mock import patch
  5. from uuid import uuid4
  6. import pytest
  7. from django.utils import timezone
  8. from snuba_sdk import Op
  9. from sentry.issues.grouptype import PerformanceNPlusOneGroupType
  10. from sentry.models.group import Group
  11. from sentry.models.project import Project
  12. from sentry.models.rule import Rule
  13. from sentry.rules.conditions.event_frequency import (
  14. EventFrequencyCondition,
  15. EventFrequencyPercentCondition,
  16. EventUniqueUserFrequencyCondition,
  17. EventUniqueUserFrequencyConditionWithConditions,
  18. )
  19. from sentry.rules.match import MatchType
  20. from sentry.testutils.abstract import Abstract
  21. from sentry.testutils.cases import (
  22. BaseMetricsTestCase,
  23. PerformanceIssueTestCase,
  24. RuleTestCase,
  25. SnubaTestCase,
  26. )
  27. from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
  28. from sentry.testutils.helpers.features import apply_feature_flag_on_cls
  29. from sentry.testutils.skips import requires_snuba
  30. from sentry.utils.samples import load_data
  31. pytestmark = [pytest.mark.sentry_metrics, requires_snuba]
  32. class BaseEventFrequencyPercentTest(BaseMetricsTestCase):
  33. def _make_sessions(
  34. self, num: int, environment_name: str | None = None, project: Project | None = None
  35. ):
  36. received = time.time()
  37. def make_session(i):
  38. return dict(
  39. distinct_id=uuid4().hex,
  40. session_id=uuid4().hex,
  41. org_id=project.organization_id if project else self.project.organization_id,
  42. project_id=project.id if project else self.project.id,
  43. status="ok",
  44. seq=0,
  45. release="foo@1.0.0",
  46. environment=environment_name if environment_name else "prod",
  47. retention_days=90,
  48. duration=None,
  49. errors=0,
  50. # The line below is crucial to spread sessions throughout the time period.
  51. started=received - i - 1,
  52. received=received,
  53. )
  54. self.bulk_store_sessions([make_session(i) for i in range(num)])
  55. class EventFrequencyQueryTestBase(SnubaTestCase, RuleTestCase, PerformanceIssueTestCase):
  56. def setUp(self):
  57. super().setUp()
  58. self.start = before_now(minutes=1)
  59. self.end = timezone.now()
  60. self.event = self.store_event(
  61. data={
  62. "event_id": "a" * 32,
  63. "environment": self.environment.name,
  64. "timestamp": iso_format(before_now(seconds=30)),
  65. "fingerprint": ["group-1"],
  66. "user": {"id": uuid4().hex},
  67. },
  68. project_id=self.project.id,
  69. )
  70. self.event2 = self.store_event(
  71. data={
  72. "event_id": "b" * 32,
  73. "environment": self.environment.name,
  74. "timestamp": iso_format(before_now(seconds=12)),
  75. "fingerprint": ["group-2"],
  76. "user": {"id": uuid4().hex},
  77. },
  78. project_id=self.project.id,
  79. )
  80. self.environment2 = self.create_environment(name="staging")
  81. self.event3 = self.store_event(
  82. data={
  83. "event_id": "c" * 32,
  84. "environment": self.environment2.name,
  85. "timestamp": iso_format(before_now(seconds=12)),
  86. "fingerprint": ["group-3"],
  87. "user": {"id": uuid4().hex},
  88. },
  89. project_id=self.project.id,
  90. )
  91. fingerprint = f"{PerformanceNPlusOneGroupType.type_id}-something_random"
  92. perf_event_data = load_data(
  93. "transaction-n-plus-one",
  94. timestamp=before_now(seconds=12),
  95. start_timestamp=before_now(seconds=13),
  96. fingerprint=[fingerprint],
  97. )
  98. perf_event_data["user"] = {"id": uuid4().hex}
  99. perf_event_data["environment"] = self.environment.name
  100. # Store a performance event
  101. self.perf_event = self.create_performance_issue(
  102. event_data=perf_event_data,
  103. project_id=self.project.id,
  104. fingerprint=fingerprint,
  105. )
  106. self.data = {"interval": "5m", "value": 30}
  107. self.condition_inst = self.get_rule(
  108. data=self.data,
  109. project=self.event.group.project,
  110. rule=Rule(environment_id=self.environment.id),
  111. )
  112. self.condition_inst2 = self.get_rule(
  113. data=self.data,
  114. project=self.event.group.project,
  115. rule=Rule(environment_id=self.environment2.id),
  116. )
  117. class EventFrequencyQueryTest(EventFrequencyQueryTestBase):
  118. rule_cls = EventFrequencyCondition
  119. def test_batch_query(self):
  120. batch_query = self.condition_inst.batch_query_hook(
  121. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  122. start=self.start,
  123. end=self.end,
  124. environment_id=self.environment.id,
  125. )
  126. assert batch_query == {
  127. self.event.group_id: 1,
  128. self.event2.group_id: 1,
  129. self.perf_event.group_id: 1,
  130. }
  131. batch_query = self.condition_inst2.batch_query_hook(
  132. group_ids=[self.event3.group_id],
  133. start=self.start,
  134. end=self.end,
  135. environment_id=self.environment2.id,
  136. )
  137. assert batch_query == {self.event3.group_id: 1}
  138. def test_get_error_and_generic_group_ids(self):
  139. groups = Group.objects.filter(
  140. id__in=[self.event.group_id, self.event2.group_id, self.perf_event.group_id]
  141. ).values("id", "type", "project__organization_id")
  142. error_issue_ids, generic_issue_ids = self.condition_inst.get_error_and_generic_group_ids(
  143. groups
  144. )
  145. assert self.event.group_id in error_issue_ids
  146. assert self.event2.group_id in error_issue_ids
  147. assert self.perf_event.group_id in generic_issue_ids
  148. class EventUniqueUserFrequencyQueryTest(EventFrequencyQueryTestBase):
  149. rule_cls = EventUniqueUserFrequencyCondition
  150. def test_batch_query_user(self):
  151. batch_query = self.condition_inst.batch_query_hook(
  152. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  153. start=self.start,
  154. end=self.end,
  155. environment_id=self.environment.id,
  156. )
  157. assert batch_query == {
  158. self.event.group_id: 1,
  159. self.event2.group_id: 1,
  160. self.perf_event.group_id: 1,
  161. }
  162. batch_query = self.condition_inst2.batch_query_hook(
  163. group_ids=[self.event3.group_id],
  164. start=self.start,
  165. end=self.end,
  166. environment_id=self.environment2.id,
  167. )
  168. assert batch_query == {self.event3.group_id: 1}
  169. class EventFrequencyPercentConditionQueryTest(
  170. EventFrequencyQueryTestBase, BaseEventFrequencyPercentTest
  171. ):
  172. rule_cls = EventFrequencyPercentCondition
  173. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  174. def test_batch_query_percent(self):
  175. self._make_sessions(60, self.environment2.name)
  176. self._make_sessions(60, self.environment.name)
  177. batch_query = self.condition_inst.batch_query_hook(
  178. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  179. start=self.start,
  180. end=self.end,
  181. environment_id=self.environment.id,
  182. )
  183. percent_of_sessions = 20
  184. assert batch_query == {
  185. self.event.group_id: percent_of_sessions,
  186. self.event2.group_id: percent_of_sessions,
  187. self.perf_event.group_id: 0,
  188. }
  189. batch_query = self.condition_inst2.batch_query_hook(
  190. group_ids=[self.event3.group_id],
  191. start=self.start,
  192. end=self.end,
  193. environment_id=self.environment2.id,
  194. )
  195. assert batch_query == {self.event3.group_id: percent_of_sessions}
  196. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 100)
  197. def test_batch_query_percent_no_avg_sessions_in_interval(self):
  198. self._make_sessions(60, self.environment2.name)
  199. self._make_sessions(60, self.environment.name)
  200. batch_query = self.condition_inst.batch_query_hook(
  201. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  202. start=self.start,
  203. end=self.end,
  204. environment_id=self.environment.id,
  205. )
  206. percent = 0
  207. assert batch_query == {
  208. self.event.group_id: percent,
  209. self.event2.group_id: percent,
  210. self.perf_event.group_id: percent,
  211. }
  212. batch_query = self.condition_inst2.batch_query_hook(
  213. group_ids=[self.event3.group_id],
  214. start=self.start,
  215. end=self.end,
  216. environment_id=self.environment2.id,
  217. )
  218. assert batch_query == {self.event3.group_id: percent}
  219. class ErrorEventMixin(SnubaTestCase):
  220. def add_event(self, data, project_id, timestamp):
  221. data["timestamp"] = iso_format(timestamp)
  222. # Store an error event
  223. event = self.store_event(
  224. data=data,
  225. project_id=project_id,
  226. )
  227. return event.for_group(event.group)
  228. class PerfIssuePlatformEventMixin(PerformanceIssueTestCase):
  229. def add_event(self, data, project_id, timestamp):
  230. fingerprint = data["fingerprint"][0]
  231. fingerprint = (
  232. fingerprint
  233. if "-" in fingerprint
  234. else f"{PerformanceNPlusOneGroupType.type_id}-{data['fingerprint'][0]}"
  235. )
  236. event_data = load_data(
  237. "transaction-n-plus-one",
  238. timestamp=timestamp,
  239. start_timestamp=timestamp,
  240. fingerprint=[fingerprint],
  241. )
  242. event_data["user"] = {"id": uuid4().hex}
  243. event_data["environment"] = data.get("environment")
  244. for tag in event_data["tags"]:
  245. if tag[0] == "environment":
  246. tag[1] = data.get("environment")
  247. break
  248. else:
  249. event_data["tags"].append(data.get("environment"))
  250. # Store a performance event
  251. event = self.create_performance_issue(
  252. event_data=event_data,
  253. project_id=project_id,
  254. fingerprint=fingerprint,
  255. )
  256. return event
  257. @pytest.mark.snuba_ci
  258. class StandardIntervalTestBase(SnubaTestCase, RuleTestCase, PerformanceIssueTestCase):
  259. __test__ = Abstract(__module__, __qualname__)
  260. def add_event(self, data, project_id, timestamp):
  261. raise NotImplementedError
  262. def increment(self, event, count, environment=None, timestamp=None):
  263. raise NotImplementedError
  264. def _run_test(self, minutes, data, passes, add_events=False):
  265. if not self.environment:
  266. self.environment = self.create_environment(name="prod")
  267. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  268. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  269. event = self.add_event(
  270. data={
  271. "fingerprint": ["something_random"],
  272. "user": {"id": uuid4().hex},
  273. },
  274. project_id=self.project.id,
  275. timestamp=before_now(minutes=minutes),
  276. )
  277. if add_events:
  278. self.increment(
  279. event,
  280. data["value"] + 1,
  281. environment=self.environment.name,
  282. timestamp=timezone.now() - timedelta(minutes=minutes),
  283. )
  284. self.increment(
  285. event,
  286. data["value"] + 1,
  287. timestamp=timezone.now() - timedelta(minutes=minutes),
  288. )
  289. if passes:
  290. self.assertPasses(rule, event, is_new=False)
  291. self.assertPasses(environment_rule, event, is_new=False)
  292. else:
  293. self.assertDoesNotPass(rule, event, is_new=False)
  294. self.assertDoesNotPass(environment_rule, event, is_new=False)
  295. def test_comparison_interval_empty_string(self):
  296. data = {
  297. "interval": "1m",
  298. "value": 16,
  299. "comparisonType": "count",
  300. "comparisonInterval": "",
  301. }
  302. self._run_test(data=data, minutes=1, passes=False)
  303. def test_one_minute_with_events(self):
  304. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  305. self._run_test(data=data, minutes=1, passes=True, add_events=True)
  306. data = {
  307. "interval": "1m",
  308. "value": 16,
  309. "comparisonType": "count",
  310. "comparisonInterval": "5m",
  311. }
  312. self._run_test(data=data, minutes=1, passes=False)
  313. def test_one_hour_with_events(self):
  314. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  315. self._run_test(data=data, minutes=60, passes=True, add_events=True)
  316. data = {
  317. "interval": "1h",
  318. "value": 16,
  319. "comparisonType": "count",
  320. "comparisonInterval": "5m",
  321. }
  322. self._run_test(data=data, minutes=60, passes=False)
  323. def test_one_day_with_events(self):
  324. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  325. self._run_test(data=data, minutes=1440, passes=True, add_events=True)
  326. data = {
  327. "interval": "1d",
  328. "value": 16,
  329. "comparisonType": "count",
  330. "comparisonInterval": "5m",
  331. }
  332. self._run_test(data=data, minutes=1440, passes=False)
  333. def test_one_week_with_events(self):
  334. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  335. self._run_test(data=data, minutes=10080, passes=True, add_events=True)
  336. data = {
  337. "interval": "1w",
  338. "value": 16,
  339. "comparisonType": "count",
  340. "comparisonInterval": "5m",
  341. }
  342. self._run_test(data=data, minutes=10080, passes=False)
  343. def test_one_minute_no_events(self):
  344. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  345. self._run_test(data=data, minutes=1, passes=False)
  346. def test_one_hour_no_events(self):
  347. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  348. self._run_test(data=data, minutes=60, passes=False)
  349. def test_one_day_no_events(self):
  350. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  351. self._run_test(data=data, minutes=1440, passes=False)
  352. def test_one_week_no_events(self):
  353. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  354. self._run_test(data=data, minutes=10080, passes=False)
  355. def test_comparison(self):
  356. # Test data is 4 events in the current period and 2 events in the comparison period, so
  357. # a 100% increase.
  358. event = self.add_event(
  359. data={
  360. "fingerprint": ["something_random"],
  361. "user": {"id": uuid4().hex},
  362. },
  363. project_id=self.project.id,
  364. timestamp=before_now(minutes=1),
  365. )
  366. self.increment(
  367. event,
  368. 3,
  369. timestamp=timezone.now() - timedelta(minutes=1),
  370. )
  371. self.increment(
  372. event,
  373. 2,
  374. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  375. )
  376. data = {
  377. "interval": "1h",
  378. "value": 99,
  379. "comparisonType": "percent",
  380. "comparisonInterval": "1d",
  381. }
  382. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  383. self.assertPasses(rule, event, is_new=False)
  384. data = {
  385. "interval": "1h",
  386. "value": 101,
  387. "comparisonType": "percent",
  388. "comparisonInterval": "1d",
  389. }
  390. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  391. self.assertDoesNotPass(rule, event, is_new=False)
  392. def test_comparison_empty_comparison_period(self):
  393. # Test data is 1 event in the current period and 0 events in the comparison period. This
  394. # should always result in 0 and never fire.
  395. event = self.add_event(
  396. data={
  397. "fingerprint": ["something_random"],
  398. "user": {"id": uuid4().hex},
  399. },
  400. project_id=self.project.id,
  401. timestamp=before_now(minutes=1),
  402. )
  403. data = {
  404. "interval": "1h",
  405. "value": 0,
  406. "comparisonType": "percent",
  407. "comparisonInterval": "1d",
  408. }
  409. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  410. self.assertDoesNotPass(rule, event, is_new=False)
  411. data = {
  412. "interval": "1h",
  413. "value": 100,
  414. "comparisonType": "percent",
  415. "comparisonInterval": "1d",
  416. }
  417. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  418. self.assertDoesNotPass(rule, event, is_new=False)
  419. @patch("sentry.rules.conditions.event_frequency.BaseEventFrequencyCondition.get_rate")
  420. def test_is_new_issue_skips_snuba(self, mock_get_rate):
  421. # Looking for more than 1 event
  422. data = {"interval": "1m", "value": 6}
  423. minutes = 1
  424. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  425. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  426. event = self.add_event(
  427. data={
  428. "fingerprint": ["something_random"],
  429. "user": {"id": uuid4().hex},
  430. },
  431. project_id=self.project.id,
  432. timestamp=before_now(minutes=minutes),
  433. )
  434. # Issue is new and is the first event
  435. self.assertDoesNotPass(rule, event, is_new=True)
  436. self.assertDoesNotPass(environment_rule, event, is_new=True)
  437. assert mock_get_rate.call_count == 0
  438. class EventFrequencyConditionTestCase(StandardIntervalTestBase):
  439. __test__ = Abstract(__module__, __qualname__)
  440. rule_cls = EventFrequencyCondition
  441. def increment(self, event, count, environment=None, timestamp=None):
  442. timestamp = timestamp if timestamp else before_now(minutes=1)
  443. data = {"fingerprint": event.data["fingerprint"]}
  444. if environment:
  445. data["environment"] = environment
  446. for _ in range(count):
  447. self.add_event(
  448. data=data,
  449. project_id=self.project.id,
  450. timestamp=timestamp,
  451. )
  452. class EventUniqueUserFrequencyConditionTestCase(StandardIntervalTestBase):
  453. __test__ = Abstract(__module__, __qualname__)
  454. rule_cls = EventUniqueUserFrequencyCondition
  455. def increment(self, event, count, environment=None, timestamp=None):
  456. timestamp = timestamp if timestamp else before_now(minutes=1)
  457. data = {"fingerprint": event.data["fingerprint"]}
  458. if environment:
  459. data["environment"] = environment
  460. for _ in range(count):
  461. event_data = deepcopy(data)
  462. event_data["user"] = {"id": uuid4().hex}
  463. self.add_event(
  464. data=event_data,
  465. project_id=self.project.id,
  466. timestamp=timestamp,
  467. )
  468. @apply_feature_flag_on_cls("organizations:event-unique-user-frequency-condition-with-conditions")
  469. class EventUniqueUserFrequencyConditionWithConditionsTestCase(StandardIntervalTestBase):
  470. __test__ = Abstract(__module__, __qualname__)
  471. rule_cls = EventUniqueUserFrequencyConditionWithConditions
  472. def increment(self, event, count, environment=None, timestamp=None):
  473. timestamp = timestamp if timestamp else before_now(minutes=1)
  474. data = {"fingerprint": event.data["fingerprint"]}
  475. if environment:
  476. data["environment"] = environment
  477. for _ in range(count):
  478. event_data = deepcopy(data)
  479. event_data["user"] = {"id": uuid4().hex}
  480. self.add_event(
  481. data=event_data,
  482. project_id=self.project.id,
  483. timestamp=timestamp,
  484. )
  485. def test_comparison(self):
  486. # Test data is 4 events in the current period and 2 events in the comparison period, so
  487. # a 100% increase.
  488. event = self.add_event(
  489. data={
  490. "fingerprint": ["something_random"],
  491. "user": {"id": uuid4().hex},
  492. },
  493. project_id=self.project.id,
  494. timestamp=before_now(minutes=1),
  495. )
  496. self.increment(
  497. event,
  498. 3,
  499. timestamp=timezone.now() - timedelta(minutes=1),
  500. )
  501. self.increment(
  502. event,
  503. 2,
  504. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  505. )
  506. data = {
  507. "interval": "1h",
  508. "value": 99,
  509. "comparisonType": "percent",
  510. "comparisonInterval": "1d",
  511. "id": "EventFrequencyConditionWithConditions",
  512. }
  513. rule = self.get_rule(
  514. data=data,
  515. rule=Rule(
  516. environment_id=None,
  517. project_id=self.project.id,
  518. data={
  519. "conditions": [data],
  520. "filter_match": "all",
  521. },
  522. ),
  523. )
  524. self.assertPasses(rule, event, is_new=False)
  525. data = {
  526. "interval": "1h",
  527. "value": 101,
  528. "comparisonType": "percent",
  529. "comparisonInterval": "1d",
  530. "id": "EventFrequencyConditionWithConditions",
  531. }
  532. rule = self.get_rule(
  533. data=data,
  534. rule=Rule(
  535. environment_id=None,
  536. project_id=self.project.id,
  537. data={
  538. "conditions": [data],
  539. "filter_match": "all",
  540. },
  541. ),
  542. )
  543. self.assertDoesNotPass(rule, event, is_new=False)
  544. def test_comparison_empty_comparison_period(self):
  545. # Test data is 1 event in the current period and 0 events in the comparison period. This
  546. # should always result in 0 and never fire.
  547. event = self.add_event(
  548. data={
  549. "fingerprint": ["something_random"],
  550. "user": {"id": uuid4().hex},
  551. },
  552. project_id=self.project.id,
  553. timestamp=before_now(minutes=1),
  554. )
  555. data = {
  556. "filter_match": "all",
  557. "conditions": [
  558. {
  559. "interval": "1h",
  560. "value": 0,
  561. "comparisonType": "percent",
  562. "comparisonInterval": "1d",
  563. }
  564. ],
  565. }
  566. rule = self.get_rule(
  567. data=data, rule=Rule(environment_id=None, project_id=self.project.id, data=data)
  568. )
  569. self.assertDoesNotPass(rule, event, is_new=False)
  570. data = {
  571. "filter_match": "all",
  572. "conditions": [
  573. {
  574. "interval": "1h",
  575. "value": 100,
  576. "comparisonType": "percent",
  577. "comparisonInterval": "1d",
  578. }
  579. ],
  580. }
  581. rule = self.get_rule(
  582. data=data, rule=Rule(environment_id=None, project_id=self.project.id, data=data)
  583. )
  584. self.assertDoesNotPass(rule, event, is_new=False)
  585. def _run_test(self, minutes, data, passes, add_events=False):
  586. if not self.environment:
  587. self.environment = self.create_environment(name="prod")
  588. data["filter_match"] = "all"
  589. data["conditions"] = data.get("conditions", [])
  590. rule = self.get_rule(
  591. data=data,
  592. rule=Rule(environment_id=None, project_id=self.project.id, data=data),
  593. )
  594. environment_rule = self.get_rule(
  595. data=data,
  596. rule=Rule(
  597. environment_id=self.environment.id,
  598. project_id=self.project.id,
  599. data=data,
  600. ),
  601. )
  602. event = self.add_event(
  603. data={
  604. "fingerprint": ["something_random"],
  605. "user": {"id": uuid4().hex},
  606. },
  607. project_id=self.project.id,
  608. timestamp=before_now(minutes=minutes),
  609. )
  610. if add_events:
  611. self.increment(
  612. event,
  613. data["value"] + 1,
  614. environment=self.environment.name,
  615. timestamp=timezone.now() - timedelta(minutes=minutes),
  616. )
  617. self.increment(
  618. event,
  619. data["value"] + 1,
  620. timestamp=timezone.now() - timedelta(minutes=minutes),
  621. )
  622. if passes:
  623. self.assertPasses(rule, event, is_new=False)
  624. self.assertPasses(environment_rule, event, is_new=False)
  625. else:
  626. self.assertDoesNotPass(rule, event, is_new=False)
  627. self.assertDoesNotPass(environment_rule, event, is_new=False)
  628. def test_convert_rule_condition_to_snuba_condition():
  629. # Test non-TaggedEventFilter condition
  630. condition = {"id": "some.other.condition"}
  631. assert (
  632. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  633. condition
  634. )
  635. is None
  636. )
  637. # Test TaggedEventFilter conditions
  638. base_condition = {
  639. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  640. "key": "test_key",
  641. "value": "test_value",
  642. }
  643. # Test equality
  644. eq_condition = {**base_condition, "match": MatchType.EQUAL}
  645. assert (
  646. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  647. eq_condition
  648. )
  649. == (
  650. "tags[test_key]",
  651. Op.EQ.value,
  652. "test_value",
  653. )
  654. )
  655. # Test inequality
  656. ne_condition = {**base_condition, "match": MatchType.NOT_EQUAL}
  657. assert (
  658. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  659. ne_condition
  660. )
  661. == (
  662. "tags[test_key]",
  663. Op.NEQ.value,
  664. "test_value",
  665. )
  666. )
  667. # Test starts with
  668. sw_condition = {**base_condition, "match": MatchType.STARTS_WITH}
  669. assert (
  670. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  671. sw_condition
  672. )
  673. == (
  674. "tags[test_key]",
  675. Op.LIKE.value,
  676. "test_value%",
  677. )
  678. )
  679. # Test not starts with
  680. nsw_condition = {**base_condition, "match": MatchType.NOT_STARTS_WITH}
  681. assert (
  682. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  683. nsw_condition
  684. )
  685. == (
  686. "tags[test_key]",
  687. Op.NOT_LIKE.value,
  688. "test_value%",
  689. )
  690. )
  691. # Test ends with
  692. ew_condition = {**base_condition, "match": MatchType.ENDS_WITH}
  693. assert (
  694. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  695. ew_condition
  696. )
  697. == (
  698. "tags[test_key]",
  699. Op.LIKE.value,
  700. "%test_value",
  701. )
  702. )
  703. # Test not ends with
  704. new_condition = {**base_condition, "match": MatchType.NOT_ENDS_WITH}
  705. assert (
  706. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  707. new_condition
  708. )
  709. == (
  710. "tags[test_key]",
  711. Op.NOT_LIKE.value,
  712. "%test_value",
  713. )
  714. )
  715. # Test contains
  716. co_condition = {**base_condition, "match": MatchType.CONTAINS}
  717. assert (
  718. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  719. co_condition
  720. )
  721. == (
  722. "tags[test_key]",
  723. Op.LIKE.value,
  724. "%test_value%",
  725. )
  726. )
  727. # Test not contains
  728. nc_condition = {**base_condition, "match": MatchType.NOT_CONTAINS}
  729. assert (
  730. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  731. nc_condition
  732. )
  733. == (
  734. "tags[test_key]",
  735. Op.NOT_LIKE.value,
  736. "%test_value%",
  737. )
  738. )
  739. # Test is set
  740. is_condition = {**base_condition, "match": MatchType.IS_SET}
  741. assert (
  742. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  743. is_condition
  744. )
  745. == (
  746. "tags[test_key]",
  747. Op.IS_NOT_NULL.value,
  748. None,
  749. )
  750. )
  751. # Test not set
  752. ns_condition = {**base_condition, "match": MatchType.NOT_SET}
  753. assert (
  754. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  755. ns_condition
  756. )
  757. == (
  758. "tags[test_key]",
  759. Op.IS_NULL.value,
  760. None,
  761. )
  762. )
  763. # Test is in
  764. in_condition = {
  765. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  766. "key": "test_key",
  767. "value": "test_value_1,test_value_2",
  768. "match": MatchType.IS_IN,
  769. }
  770. assert (
  771. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  772. in_condition
  773. )
  774. == (
  775. "tags[test_key]",
  776. Op.IN.value,
  777. ["test_value_1", "test_value_2"],
  778. )
  779. )
  780. # Test not in
  781. not_in_condition = {
  782. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  783. "key": "test_key",
  784. "value": "test_value_1,test_value_2",
  785. "match": MatchType.NOT_IN,
  786. }
  787. assert (
  788. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  789. not_in_condition
  790. )
  791. == (
  792. "tags[test_key]",
  793. Op.NOT_IN.value,
  794. ["test_value_1", "test_value_2"],
  795. )
  796. )
  797. # Test unsupported match type
  798. with pytest.raises(ValueError, match="Unsupported match type: unsupported"):
  799. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  800. {**base_condition, "match": "unsupported"}
  801. )
  802. class EventFrequencyPercentConditionTestCase(BaseEventFrequencyPercentTest, RuleTestCase):
  803. __test__ = Abstract(__module__, __qualname__)
  804. rule_cls = EventFrequencyPercentCondition
  805. def add_event(self, data, project_id, timestamp):
  806. raise NotImplementedError
  807. def _run_test(self, minutes, data, passes, add_events=False):
  808. if not self.environment or self.environment.name != "prod":
  809. self.environment = self.create_environment(name="prod")
  810. if not hasattr(self, "test_event"):
  811. self.test_event = self.add_event(
  812. data={
  813. "fingerprint": ["something_random"],
  814. "user": {"id": uuid4().hex},
  815. "environment": self.environment.name,
  816. },
  817. project_id=self.project.id,
  818. timestamp=before_now(minutes=minutes),
  819. )
  820. if add_events:
  821. self.increment(
  822. self.test_event,
  823. max(1, int(minutes / 2)) - 1,
  824. environment=self.environment.name,
  825. timestamp=timezone.now() - timedelta(minutes=minutes),
  826. )
  827. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  828. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  829. if passes:
  830. self.assertPasses(rule, self.test_event, is_new=False)
  831. self.assertPasses(environment_rule, self.test_event, is_new=False)
  832. else:
  833. self.assertDoesNotPass(rule, self.test_event)
  834. self.assertDoesNotPass(environment_rule, self.test_event)
  835. def increment(self, event, count, environment=None, timestamp=None):
  836. data = {
  837. "fingerprint": event.data["fingerprint"],
  838. }
  839. timestamp = timestamp if timestamp else before_now(minutes=1)
  840. if environment:
  841. data["environment"] = environment
  842. for _ in range(count):
  843. event_data = deepcopy(data)
  844. event_data["user"] = {"id": uuid4().hex}
  845. self.add_event(
  846. data=event_data,
  847. project_id=self.project.id,
  848. timestamp=timestamp,
  849. )
  850. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  851. def test_five_minutes_with_events(self):
  852. self._make_sessions(60)
  853. data = {
  854. "interval": "5m",
  855. "value": 39,
  856. "comparisonType": "count",
  857. "comparisonInterval": "5m",
  858. }
  859. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  860. data = {
  861. "interval": "5m",
  862. "value": 41,
  863. "comparisonType": "count",
  864. "comparisonInterval": "5m",
  865. }
  866. self._run_test(data=data, minutes=5, passes=False)
  867. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  868. def test_ten_minutes_with_events(self):
  869. self._make_sessions(60)
  870. data = {
  871. "interval": "10m",
  872. "value": 49,
  873. "comparisonType": "count",
  874. "comparisonInterval": "5m",
  875. }
  876. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  877. data = {
  878. "interval": "10m",
  879. "value": 51,
  880. "comparisonType": "count",
  881. "comparisonInterval": "5m",
  882. }
  883. self._run_test(data=data, minutes=10, passes=False)
  884. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  885. def test_thirty_minutes_with_events(self):
  886. self._make_sessions(60)
  887. data = {
  888. "interval": "30m",
  889. "value": 49,
  890. "comparisonType": "count",
  891. "comparisonInterval": "5m",
  892. }
  893. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  894. data = {
  895. "interval": "30m",
  896. "value": 51,
  897. "comparisonType": "count",
  898. "comparisonInterval": "5m",
  899. }
  900. self._run_test(data=data, minutes=30, passes=False)
  901. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  902. def test_one_hour_with_events(self):
  903. self._make_sessions(60)
  904. data = {
  905. "interval": "1h",
  906. "value": 49,
  907. "comparisonType": "count",
  908. "comparisonInterval": "5m",
  909. }
  910. self._run_test(data=data, minutes=60, add_events=True, passes=True)
  911. data = {
  912. "interval": "1h",
  913. "value": 51,
  914. "comparisonType": "count",
  915. "comparisonInterval": "5m",
  916. }
  917. self._run_test(data=data, minutes=60, passes=False)
  918. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  919. def test_five_minutes_no_events(self):
  920. self._make_sessions(60)
  921. data = {
  922. "interval": "5m",
  923. "value": 39,
  924. "comparisonType": "count",
  925. "comparisonInterval": "5m",
  926. }
  927. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  928. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  929. def test_ten_minutes_no_events(self):
  930. self._make_sessions(60)
  931. data = {
  932. "interval": "10m",
  933. "value": 49,
  934. "comparisonType": "count",
  935. "comparisonInterval": "5m",
  936. }
  937. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  938. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  939. def test_thirty_minutes_no_events(self):
  940. self._make_sessions(60)
  941. data = {
  942. "interval": "30m",
  943. "value": 49,
  944. "comparisonType": "count",
  945. "comparisonInterval": "5m",
  946. }
  947. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  948. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  949. def test_one_hour_no_events(self):
  950. self._make_sessions(60)
  951. data = {
  952. "interval": "1h",
  953. "value": 49,
  954. "comparisonType": "count",
  955. "comparisonInterval": "5m",
  956. }
  957. self._run_test(data=data, minutes=60, passes=False)
  958. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  959. def test_comparison(self):
  960. self._make_sessions(10)
  961. # Create sessions for previous period
  962. self._make_sessions(10)
  963. # Test data is 2 events in the current period and 1 events in the comparison period.
  964. # Number of sessions is 20 in each period, so current period is 20% of sessions, prev
  965. # is 10%. Overall a 100% increase comparatively.
  966. event = self.add_event(
  967. data={"fingerprint": ["something_random"]},
  968. project_id=self.project.id,
  969. timestamp=before_now(minutes=1),
  970. )
  971. self.increment(
  972. event,
  973. 1,
  974. timestamp=timezone.now() - timedelta(minutes=1),
  975. )
  976. self.increment(
  977. event,
  978. 1,
  979. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  980. )
  981. data = {
  982. "interval": "1h",
  983. "value": 99,
  984. "comparisonType": "percent",
  985. "comparisonInterval": "1d",
  986. }
  987. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  988. self.assertPasses(rule, event, is_new=False)
  989. data = {
  990. "interval": "1h",
  991. "value": 101,
  992. "comparisonType": "percent",
  993. "comparisonInterval": "1d",
  994. }
  995. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  996. self.assertDoesNotPass(rule, event, is_new=False)
  997. @freeze_time(
  998. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  999. )
  1000. class ErrorIssueFrequencyConditionTestCase(ErrorEventMixin, EventFrequencyConditionTestCase):
  1001. pass
  1002. @freeze_time(
  1003. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1004. )
  1005. class PerfIssuePlatformIssueFrequencyConditionTestCase(
  1006. PerfIssuePlatformEventMixin,
  1007. EventFrequencyConditionTestCase,
  1008. ):
  1009. pass
  1010. @freeze_time(
  1011. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1012. )
  1013. class ErrorIssueUniqueUserFrequencyConditionTestCase(
  1014. ErrorEventMixin,
  1015. EventUniqueUserFrequencyConditionTestCase,
  1016. ):
  1017. pass
  1018. @freeze_time(
  1019. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1020. )
  1021. class ErrorIssueUniqueUserFrequencyConditionWithConditionsTestCase(
  1022. ErrorEventMixin,
  1023. EventUniqueUserFrequencyConditionWithConditionsTestCase,
  1024. ):
  1025. pass
  1026. @freeze_time(
  1027. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1028. )
  1029. class PerfIssuePlatformIssueUniqueUserFrequencyConditionTestCase(
  1030. PerfIssuePlatformEventMixin,
  1031. EventUniqueUserFrequencyConditionTestCase,
  1032. ):
  1033. pass
  1034. @freeze_time(
  1035. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1036. )
  1037. class ErrorIssueEventFrequencyPercentConditionTestCase(
  1038. ErrorEventMixin, EventFrequencyPercentConditionTestCase
  1039. ):
  1040. pass
  1041. @freeze_time(
  1042. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1043. )
  1044. class PerfIssuePlatformIssueEventFrequencyPercentConditionTestCase(
  1045. PerfIssuePlatformEventMixin,
  1046. EventFrequencyPercentConditionTestCase,
  1047. ):
  1048. pass