test_event_frequency.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
  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
  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": before_now(seconds=30).isoformat(),
  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": before_now(seconds=12).isoformat(),
  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": before_now(seconds=12).isoformat(),
  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"] = timestamp.isoformat()
  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. raise AssertionError("expected `environment` tag")
  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. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  266. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  267. event = self.add_event(
  268. data={
  269. "fingerprint": ["something_random"],
  270. "user": {"id": uuid4().hex},
  271. },
  272. project_id=self.project.id,
  273. timestamp=before_now(minutes=minutes),
  274. )
  275. if add_events:
  276. self.increment(
  277. event,
  278. data["value"] + 1,
  279. environment=self.environment.name,
  280. timestamp=timezone.now() - timedelta(minutes=minutes),
  281. )
  282. self.increment(
  283. event,
  284. data["value"] + 1,
  285. timestamp=timezone.now() - timedelta(minutes=minutes),
  286. )
  287. if passes:
  288. self.assertPasses(rule, event, is_new=False)
  289. self.assertPasses(environment_rule, event, is_new=False)
  290. else:
  291. self.assertDoesNotPass(rule, event, is_new=False)
  292. self.assertDoesNotPass(environment_rule, event, is_new=False)
  293. def test_comparison_interval_empty_string(self):
  294. data = {
  295. "interval": "1m",
  296. "value": 16,
  297. "comparisonType": "count",
  298. "comparisonInterval": "",
  299. }
  300. self._run_test(data=data, minutes=1, passes=False)
  301. def test_one_minute_with_events(self):
  302. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  303. self._run_test(data=data, minutes=1, passes=True, add_events=True)
  304. data = {
  305. "interval": "1m",
  306. "value": 16,
  307. "comparisonType": "count",
  308. "comparisonInterval": "5m",
  309. }
  310. self._run_test(data=data, minutes=1, passes=False)
  311. def test_one_hour_with_events(self):
  312. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  313. self._run_test(data=data, minutes=60, passes=True, add_events=True)
  314. data = {
  315. "interval": "1h",
  316. "value": 16,
  317. "comparisonType": "count",
  318. "comparisonInterval": "5m",
  319. }
  320. self._run_test(data=data, minutes=60, passes=False)
  321. def test_one_day_with_events(self):
  322. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  323. self._run_test(data=data, minutes=1440, passes=True, add_events=True)
  324. data = {
  325. "interval": "1d",
  326. "value": 16,
  327. "comparisonType": "count",
  328. "comparisonInterval": "5m",
  329. }
  330. self._run_test(data=data, minutes=1440, passes=False)
  331. def test_one_week_with_events(self):
  332. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  333. self._run_test(data=data, minutes=10080, passes=True, add_events=True)
  334. data = {
  335. "interval": "1w",
  336. "value": 16,
  337. "comparisonType": "count",
  338. "comparisonInterval": "5m",
  339. }
  340. self._run_test(data=data, minutes=10080, passes=False)
  341. def test_one_minute_no_events(self):
  342. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  343. self._run_test(data=data, minutes=1, passes=False)
  344. def test_one_hour_no_events(self):
  345. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  346. self._run_test(data=data, minutes=60, passes=False)
  347. def test_one_day_no_events(self):
  348. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  349. self._run_test(data=data, minutes=1440, passes=False)
  350. def test_one_week_no_events(self):
  351. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  352. self._run_test(data=data, minutes=10080, passes=False)
  353. def test_comparison(self):
  354. # Test data is 4 events in the current period and 2 events in the comparison period, so
  355. # a 100% increase.
  356. event = self.add_event(
  357. data={
  358. "fingerprint": ["something_random"],
  359. "user": {"id": uuid4().hex},
  360. },
  361. project_id=self.project.id,
  362. timestamp=before_now(minutes=1),
  363. )
  364. self.increment(
  365. event,
  366. 3,
  367. timestamp=timezone.now() - timedelta(minutes=1),
  368. )
  369. self.increment(
  370. event,
  371. 2,
  372. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  373. )
  374. data = {
  375. "interval": "1h",
  376. "value": 99,
  377. "comparisonType": "percent",
  378. "comparisonInterval": "1d",
  379. }
  380. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  381. self.assertPasses(rule, event, is_new=False)
  382. data = {
  383. "interval": "1h",
  384. "value": 101,
  385. "comparisonType": "percent",
  386. "comparisonInterval": "1d",
  387. }
  388. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  389. self.assertDoesNotPass(rule, event, is_new=False)
  390. def test_comparison_empty_comparison_period(self):
  391. # Test data is 1 event in the current period and 0 events in the comparison period. This
  392. # should always result in 0 and never fire.
  393. event = self.add_event(
  394. data={
  395. "fingerprint": ["something_random"],
  396. "user": {"id": uuid4().hex},
  397. },
  398. project_id=self.project.id,
  399. timestamp=before_now(minutes=1),
  400. )
  401. data = {
  402. "interval": "1h",
  403. "value": 0,
  404. "comparisonType": "percent",
  405. "comparisonInterval": "1d",
  406. }
  407. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  408. self.assertDoesNotPass(rule, event, is_new=False)
  409. data = {
  410. "interval": "1h",
  411. "value": 100,
  412. "comparisonType": "percent",
  413. "comparisonInterval": "1d",
  414. }
  415. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  416. self.assertDoesNotPass(rule, event, is_new=False)
  417. @patch("sentry.rules.conditions.event_frequency.BaseEventFrequencyCondition.get_rate")
  418. def test_is_new_issue_skips_snuba(self, mock_get_rate):
  419. # Looking for more than 1 event
  420. data = {"interval": "1m", "value": 6}
  421. minutes = 1
  422. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  423. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  424. event = self.add_event(
  425. data={
  426. "fingerprint": ["something_random"],
  427. "user": {"id": uuid4().hex},
  428. },
  429. project_id=self.project.id,
  430. timestamp=before_now(minutes=minutes),
  431. )
  432. # Issue is new and is the first event
  433. self.assertDoesNotPass(rule, event, is_new=True)
  434. self.assertDoesNotPass(environment_rule, event, is_new=True)
  435. assert mock_get_rate.call_count == 0
  436. class EventFrequencyConditionTestCase(StandardIntervalTestBase):
  437. __test__ = Abstract(__module__, __qualname__)
  438. rule_cls = EventFrequencyCondition
  439. def increment(self, event, count, environment=None, timestamp=None):
  440. timestamp = timestamp if timestamp else before_now(minutes=1)
  441. data = {"fingerprint": event.data["fingerprint"]}
  442. if environment:
  443. data["environment"] = environment
  444. for _ in range(count):
  445. self.add_event(
  446. data=data,
  447. project_id=self.project.id,
  448. timestamp=timestamp,
  449. )
  450. class EventUniqueUserFrequencyConditionTestCase(StandardIntervalTestBase):
  451. __test__ = Abstract(__module__, __qualname__)
  452. rule_cls = EventUniqueUserFrequencyCondition
  453. def increment(self, event, count, environment=None, timestamp=None):
  454. timestamp = timestamp if timestamp else before_now(minutes=1)
  455. data = {"fingerprint": event.data["fingerprint"]}
  456. if environment:
  457. data["environment"] = environment
  458. for _ in range(count):
  459. event_data = deepcopy(data)
  460. event_data["user"] = {"id": uuid4().hex}
  461. self.add_event(
  462. data=event_data,
  463. project_id=self.project.id,
  464. timestamp=timestamp,
  465. )
  466. @apply_feature_flag_on_cls("organizations:event-unique-user-frequency-condition-with-conditions")
  467. class EventUniqueUserFrequencyConditionWithConditionsTestCase(StandardIntervalTestBase):
  468. __test__ = Abstract(__module__, __qualname__)
  469. rule_cls = EventUniqueUserFrequencyConditionWithConditions
  470. def increment(self, event, count, environment=None, timestamp=None):
  471. timestamp = timestamp if timestamp else before_now(minutes=1)
  472. data = {"fingerprint": event.data["fingerprint"]}
  473. if environment:
  474. data["environment"] = environment
  475. for _ in range(count):
  476. event_data = deepcopy(data)
  477. event_data["user"] = {"id": uuid4().hex}
  478. self.add_event(
  479. data=event_data,
  480. project_id=self.project.id,
  481. timestamp=timestamp,
  482. )
  483. def test_comparison(self):
  484. # Test data is 4 events in the current period and 2 events in the comparison period, so
  485. # a 100% increase.
  486. event = self.add_event(
  487. data={
  488. "fingerprint": ["something_random"],
  489. "user": {"id": uuid4().hex},
  490. },
  491. project_id=self.project.id,
  492. timestamp=before_now(minutes=1),
  493. )
  494. self.increment(
  495. event,
  496. 3,
  497. timestamp=timezone.now() - timedelta(minutes=1),
  498. )
  499. self.increment(
  500. event,
  501. 2,
  502. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  503. )
  504. data = {
  505. "interval": "1h",
  506. "value": 99,
  507. "comparisonType": "percent",
  508. "comparisonInterval": "1d",
  509. "id": "EventFrequencyConditionWithConditions",
  510. }
  511. rule = self.get_rule(
  512. data=data,
  513. rule=Rule(
  514. environment_id=None,
  515. project_id=self.project.id,
  516. data={
  517. "conditions": [data],
  518. "filter_match": "all",
  519. },
  520. ),
  521. )
  522. self.assertPasses(rule, event, is_new=False)
  523. data = {
  524. "interval": "1h",
  525. "value": 101,
  526. "comparisonType": "percent",
  527. "comparisonInterval": "1d",
  528. "id": "EventFrequencyConditionWithConditions",
  529. }
  530. rule = self.get_rule(
  531. data=data,
  532. rule=Rule(
  533. environment_id=None,
  534. project_id=self.project.id,
  535. data={
  536. "conditions": [data],
  537. "filter_match": "all",
  538. },
  539. ),
  540. )
  541. self.assertDoesNotPass(rule, event, is_new=False)
  542. def test_comparison_empty_comparison_period(self):
  543. # Test data is 1 event in the current period and 0 events in the comparison period. This
  544. # should always result in 0 and never fire.
  545. event = self.add_event(
  546. data={
  547. "fingerprint": ["something_random"],
  548. "user": {"id": uuid4().hex},
  549. },
  550. project_id=self.project.id,
  551. timestamp=before_now(minutes=1),
  552. )
  553. data = {
  554. "filter_match": "all",
  555. "conditions": [
  556. {
  557. "interval": "1h",
  558. "value": 0,
  559. "comparisonType": "percent",
  560. "comparisonInterval": "1d",
  561. }
  562. ],
  563. }
  564. rule = self.get_rule(
  565. data=data, rule=Rule(environment_id=None, project_id=self.project.id, data=data)
  566. )
  567. self.assertDoesNotPass(rule, event, is_new=False)
  568. data = {
  569. "filter_match": "all",
  570. "conditions": [
  571. {
  572. "interval": "1h",
  573. "value": 100,
  574. "comparisonType": "percent",
  575. "comparisonInterval": "1d",
  576. }
  577. ],
  578. }
  579. rule = self.get_rule(
  580. data=data, rule=Rule(environment_id=None, project_id=self.project.id, data=data)
  581. )
  582. self.assertDoesNotPass(rule, event, is_new=False)
  583. def _run_test(self, minutes, data, passes, add_events=False):
  584. data["filter_match"] = "all"
  585. data["conditions"] = data.get("conditions", [])
  586. rule = self.get_rule(
  587. data=data,
  588. rule=Rule(environment_id=None, project_id=self.project.id, data=data),
  589. )
  590. environment_rule = self.get_rule(
  591. data=data,
  592. rule=Rule(
  593. environment_id=self.environment.id,
  594. project_id=self.project.id,
  595. data=data,
  596. ),
  597. )
  598. event = self.add_event(
  599. data={
  600. "fingerprint": ["something_random"],
  601. "user": {"id": uuid4().hex},
  602. },
  603. project_id=self.project.id,
  604. timestamp=before_now(minutes=minutes),
  605. )
  606. if add_events:
  607. self.increment(
  608. event,
  609. data["value"] + 1,
  610. environment=self.environment.name,
  611. timestamp=timezone.now() - timedelta(minutes=minutes),
  612. )
  613. self.increment(
  614. event,
  615. data["value"] + 1,
  616. timestamp=timezone.now() - timedelta(minutes=minutes),
  617. )
  618. if passes:
  619. self.assertPasses(rule, event, is_new=False)
  620. self.assertPasses(environment_rule, event, is_new=False)
  621. else:
  622. self.assertDoesNotPass(rule, event, is_new=False)
  623. self.assertDoesNotPass(environment_rule, event, is_new=False)
  624. def test_convert_rule_condition_to_snuba_condition():
  625. # Test non-TaggedEventFilter condition
  626. condition = {"id": "some.other.condition"}
  627. assert (
  628. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  629. condition
  630. )
  631. is None
  632. )
  633. # Test TaggedEventFilter conditions
  634. base_condition = {
  635. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  636. "key": "test_key",
  637. "value": "test_value",
  638. }
  639. # Test equality
  640. eq_condition = {**base_condition, "match": MatchType.EQUAL}
  641. assert (
  642. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  643. eq_condition
  644. )
  645. == (
  646. "tags[test_key]",
  647. Op.EQ.value,
  648. "test_value",
  649. )
  650. )
  651. # Test inequality
  652. ne_condition = {**base_condition, "match": MatchType.NOT_EQUAL}
  653. assert (
  654. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  655. ne_condition
  656. )
  657. == (
  658. "tags[test_key]",
  659. Op.NEQ.value,
  660. "test_value",
  661. )
  662. )
  663. # Test starts with
  664. sw_condition = {**base_condition, "match": MatchType.STARTS_WITH}
  665. assert (
  666. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  667. sw_condition
  668. )
  669. == (
  670. "tags[test_key]",
  671. Op.LIKE.value,
  672. "test_value%",
  673. )
  674. )
  675. # Test not starts with
  676. nsw_condition = {**base_condition, "match": MatchType.NOT_STARTS_WITH}
  677. assert (
  678. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  679. nsw_condition
  680. )
  681. == (
  682. "tags[test_key]",
  683. Op.NOT_LIKE.value,
  684. "test_value%",
  685. )
  686. )
  687. # Test ends with
  688. ew_condition = {**base_condition, "match": MatchType.ENDS_WITH}
  689. assert (
  690. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  691. ew_condition
  692. )
  693. == (
  694. "tags[test_key]",
  695. Op.LIKE.value,
  696. "%test_value",
  697. )
  698. )
  699. # Test not ends with
  700. new_condition = {**base_condition, "match": MatchType.NOT_ENDS_WITH}
  701. assert (
  702. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  703. new_condition
  704. )
  705. == (
  706. "tags[test_key]",
  707. Op.NOT_LIKE.value,
  708. "%test_value",
  709. )
  710. )
  711. # Test contains
  712. co_condition = {**base_condition, "match": MatchType.CONTAINS}
  713. assert (
  714. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  715. co_condition
  716. )
  717. == (
  718. "tags[test_key]",
  719. Op.LIKE.value,
  720. "%test_value%",
  721. )
  722. )
  723. # Test not contains
  724. nc_condition = {**base_condition, "match": MatchType.NOT_CONTAINS}
  725. assert (
  726. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  727. nc_condition
  728. )
  729. == (
  730. "tags[test_key]",
  731. Op.NOT_LIKE.value,
  732. "%test_value%",
  733. )
  734. )
  735. # Test is set
  736. is_condition = {**base_condition, "match": MatchType.IS_SET}
  737. assert (
  738. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  739. is_condition
  740. )
  741. == (
  742. "tags[test_key]",
  743. Op.IS_NOT_NULL.value,
  744. None,
  745. )
  746. )
  747. # Test not set
  748. ns_condition = {**base_condition, "match": MatchType.NOT_SET}
  749. assert (
  750. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  751. ns_condition
  752. )
  753. == (
  754. "tags[test_key]",
  755. Op.IS_NULL.value,
  756. None,
  757. )
  758. )
  759. # Test is in
  760. in_condition = {
  761. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  762. "key": "test_key",
  763. "value": "test_value_1,test_value_2",
  764. "match": MatchType.IS_IN,
  765. }
  766. assert (
  767. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  768. in_condition
  769. )
  770. == (
  771. "tags[test_key]",
  772. Op.IN.value,
  773. ["test_value_1", "test_value_2"],
  774. )
  775. )
  776. # Test not in
  777. not_in_condition = {
  778. "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
  779. "key": "test_key",
  780. "value": "test_value_1,test_value_2",
  781. "match": MatchType.NOT_IN,
  782. }
  783. assert (
  784. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  785. not_in_condition
  786. )
  787. == (
  788. "tags[test_key]",
  789. Op.NOT_IN.value,
  790. ["test_value_1", "test_value_2"],
  791. )
  792. )
  793. # Test unsupported match type
  794. with pytest.raises(ValueError, match="Unsupported match type: unsupported"):
  795. EventUniqueUserFrequencyConditionWithConditions.convert_rule_condition_to_snuba_condition(
  796. {**base_condition, "match": "unsupported"}
  797. )
  798. class EventFrequencyPercentConditionTestCase(BaseEventFrequencyPercentTest, RuleTestCase):
  799. __test__ = Abstract(__module__, __qualname__)
  800. rule_cls = EventFrequencyPercentCondition
  801. def add_event(self, data, project_id, timestamp):
  802. raise NotImplementedError
  803. def _run_test(self, minutes, data, passes, add_events=False):
  804. if not self.environment or self.environment.name != "prod":
  805. self.environment = self.create_environment(name="prod")
  806. if not hasattr(self, "test_event"):
  807. self.test_event = self.add_event(
  808. data={
  809. "fingerprint": ["something_random"],
  810. "user": {"id": uuid4().hex},
  811. "environment": self.environment.name,
  812. },
  813. project_id=self.project.id,
  814. timestamp=before_now(minutes=minutes),
  815. )
  816. if add_events:
  817. self.increment(
  818. self.test_event,
  819. max(1, int(minutes / 2)) - 1,
  820. environment=self.environment.name,
  821. timestamp=timezone.now() - timedelta(minutes=minutes),
  822. )
  823. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  824. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  825. if passes:
  826. self.assertPasses(rule, self.test_event, is_new=False)
  827. self.assertPasses(environment_rule, self.test_event, is_new=False)
  828. else:
  829. self.assertDoesNotPass(rule, self.test_event)
  830. self.assertDoesNotPass(environment_rule, self.test_event)
  831. def increment(self, event, count, environment=None, timestamp=None):
  832. data = {
  833. "fingerprint": event.data["fingerprint"],
  834. }
  835. timestamp = timestamp if timestamp else before_now(minutes=1)
  836. if environment:
  837. data["environment"] = environment
  838. for _ in range(count):
  839. event_data = deepcopy(data)
  840. event_data["user"] = {"id": uuid4().hex}
  841. self.add_event(
  842. data=event_data,
  843. project_id=self.project.id,
  844. timestamp=timestamp,
  845. )
  846. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  847. def test_five_minutes_with_events(self):
  848. self._make_sessions(60)
  849. data = {
  850. "interval": "5m",
  851. "value": 39,
  852. "comparisonType": "count",
  853. "comparisonInterval": "5m",
  854. }
  855. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  856. data = {
  857. "interval": "5m",
  858. "value": 41,
  859. "comparisonType": "count",
  860. "comparisonInterval": "5m",
  861. }
  862. self._run_test(data=data, minutes=5, passes=False)
  863. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  864. def test_ten_minutes_with_events(self):
  865. self._make_sessions(60)
  866. data = {
  867. "interval": "10m",
  868. "value": 49,
  869. "comparisonType": "count",
  870. "comparisonInterval": "5m",
  871. }
  872. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  873. data = {
  874. "interval": "10m",
  875. "value": 51,
  876. "comparisonType": "count",
  877. "comparisonInterval": "5m",
  878. }
  879. self._run_test(data=data, minutes=10, passes=False)
  880. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  881. def test_thirty_minutes_with_events(self):
  882. self._make_sessions(60)
  883. data = {
  884. "interval": "30m",
  885. "value": 49,
  886. "comparisonType": "count",
  887. "comparisonInterval": "5m",
  888. }
  889. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  890. data = {
  891. "interval": "30m",
  892. "value": 51,
  893. "comparisonType": "count",
  894. "comparisonInterval": "5m",
  895. }
  896. self._run_test(data=data, minutes=30, passes=False)
  897. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  898. def test_one_hour_with_events(self):
  899. self._make_sessions(60)
  900. data = {
  901. "interval": "1h",
  902. "value": 49,
  903. "comparisonType": "count",
  904. "comparisonInterval": "5m",
  905. }
  906. self._run_test(data=data, minutes=60, add_events=True, passes=True)
  907. data = {
  908. "interval": "1h",
  909. "value": 51,
  910. "comparisonType": "count",
  911. "comparisonInterval": "5m",
  912. }
  913. self._run_test(data=data, minutes=60, passes=False)
  914. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  915. def test_five_minutes_no_events(self):
  916. self._make_sessions(60)
  917. data = {
  918. "interval": "5m",
  919. "value": 39,
  920. "comparisonType": "count",
  921. "comparisonInterval": "5m",
  922. }
  923. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  924. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  925. def test_ten_minutes_no_events(self):
  926. self._make_sessions(60)
  927. data = {
  928. "interval": "10m",
  929. "value": 49,
  930. "comparisonType": "count",
  931. "comparisonInterval": "5m",
  932. }
  933. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  934. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  935. def test_thirty_minutes_no_events(self):
  936. self._make_sessions(60)
  937. data = {
  938. "interval": "30m",
  939. "value": 49,
  940. "comparisonType": "count",
  941. "comparisonInterval": "5m",
  942. }
  943. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  944. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  945. def test_one_hour_no_events(self):
  946. self._make_sessions(60)
  947. data = {
  948. "interval": "1h",
  949. "value": 49,
  950. "comparisonType": "count",
  951. "comparisonInterval": "5m",
  952. }
  953. self._run_test(data=data, minutes=60, passes=False)
  954. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  955. def test_comparison(self):
  956. self._make_sessions(10)
  957. # Create sessions for previous period
  958. self._make_sessions(10)
  959. # Test data is 2 events in the current period and 1 events in the comparison period.
  960. # Number of sessions is 20 in each period, so current period is 20% of sessions, prev
  961. # is 10%. Overall a 100% increase comparatively.
  962. event = self.add_event(
  963. data={"fingerprint": ["something_random"]},
  964. project_id=self.project.id,
  965. timestamp=before_now(minutes=1),
  966. )
  967. self.increment(
  968. event,
  969. 1,
  970. timestamp=timezone.now() - timedelta(minutes=1),
  971. )
  972. self.increment(
  973. event,
  974. 1,
  975. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  976. )
  977. data = {
  978. "interval": "1h",
  979. "value": 99,
  980. "comparisonType": "percent",
  981. "comparisonInterval": "1d",
  982. }
  983. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  984. self.assertPasses(rule, event, is_new=False)
  985. data = {
  986. "interval": "1h",
  987. "value": 101,
  988. "comparisonType": "percent",
  989. "comparisonInterval": "1d",
  990. }
  991. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  992. self.assertDoesNotPass(rule, event, is_new=False)
  993. @freeze_time(
  994. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  995. )
  996. class ErrorIssueFrequencyConditionTestCase(ErrorEventMixin, EventFrequencyConditionTestCase):
  997. pass
  998. @freeze_time(
  999. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1000. )
  1001. class PerfIssuePlatformIssueFrequencyConditionTestCase(
  1002. PerfIssuePlatformEventMixin,
  1003. EventFrequencyConditionTestCase,
  1004. ):
  1005. pass
  1006. @freeze_time(
  1007. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1008. )
  1009. class ErrorIssueUniqueUserFrequencyConditionTestCase(
  1010. ErrorEventMixin,
  1011. EventUniqueUserFrequencyConditionTestCase,
  1012. ):
  1013. pass
  1014. @freeze_time(
  1015. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1016. )
  1017. class ErrorIssueUniqueUserFrequencyConditionWithConditionsTestCase(
  1018. ErrorEventMixin,
  1019. EventUniqueUserFrequencyConditionWithConditionsTestCase,
  1020. ):
  1021. pass
  1022. @freeze_time(
  1023. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1024. )
  1025. class PerfIssuePlatformIssueUniqueUserFrequencyConditionTestCase(
  1026. PerfIssuePlatformEventMixin,
  1027. EventUniqueUserFrequencyConditionTestCase,
  1028. ):
  1029. pass
  1030. @freeze_time(
  1031. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1032. )
  1033. class ErrorIssueEventFrequencyPercentConditionTestCase(
  1034. ErrorEventMixin, EventFrequencyPercentConditionTestCase
  1035. ):
  1036. pass
  1037. @freeze_time(
  1038. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  1039. )
  1040. class PerfIssuePlatformIssueEventFrequencyPercentConditionTestCase(
  1041. PerfIssuePlatformEventMixin,
  1042. EventFrequencyPercentConditionTestCase,
  1043. ):
  1044. pass