test_event_frequency.py 37 KB

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