test_event_frequency.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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. from django.utils.timezone import now
  7. from freezegun import freeze_time
  8. from sentry.models import Rule
  9. from sentry.rules.conditions.event_frequency import (
  10. EventFrequencyCondition,
  11. EventFrequencyPercentCondition,
  12. EventUniqueUserFrequencyCondition,
  13. )
  14. from sentry.testutils.cases import RuleTestCase, SnubaTestCase
  15. from sentry.testutils.helpers.datetime import before_now, iso_format
  16. from sentry.testutils.silo import region_silo_test
  17. class FrequencyConditionMixin:
  18. def increment(self, event, count, environment=None, timestamp=None):
  19. raise NotImplementedError
  20. def _run_test(self, minutes, data, passes, add_events=False):
  21. if not self.environment:
  22. self.environment = self.create_environment(name="prod")
  23. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  24. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  25. event = self.store_event(
  26. data={
  27. "fingerprint": ["something_random"],
  28. "timestamp": iso_format(before_now(minutes=minutes)),
  29. "user": {"id": uuid4().hex},
  30. },
  31. project_id=self.project.id,
  32. )
  33. if add_events:
  34. self.increment(
  35. event,
  36. data["value"] + 1,
  37. environment=self.environment.name,
  38. timestamp=now() - timedelta(minutes=minutes),
  39. )
  40. self.increment(
  41. event,
  42. data["value"] + 1,
  43. timestamp=now() - timedelta(minutes=minutes),
  44. )
  45. if passes:
  46. self.assertPasses(rule, event)
  47. self.assertPasses(environment_rule, event)
  48. else:
  49. self.assertDoesNotPass(rule, event)
  50. self.assertDoesNotPass(environment_rule, event)
  51. class StandardIntervalMixin:
  52. def test_one_minute_with_events(self):
  53. data = {"interval": "1m", "value": 6}
  54. self._run_test(data=data, minutes=1, passes=True, add_events=True)
  55. data = {"interval": "1m", "value": 16}
  56. self._run_test(data=data, minutes=1, passes=False)
  57. def test_one_hour_with_events(self):
  58. data = {"interval": "1h", "value": 6}
  59. self._run_test(data=data, minutes=60, passes=True, add_events=True)
  60. data = {"interval": "1h", "value": 16}
  61. self._run_test(data=data, minutes=60, passes=False)
  62. def test_one_day_with_events(self):
  63. data = {"interval": "1d", "value": 6}
  64. self._run_test(data=data, minutes=1440, passes=True, add_events=True)
  65. data = {"interval": "1d", "value": 16}
  66. self._run_test(data=data, minutes=1440, passes=False)
  67. def test_one_week_with_events(self):
  68. data = {"interval": "1w", "value": 6}
  69. self._run_test(data=data, minutes=10080, passes=True, add_events=True)
  70. data = {"interval": "1w", "value": 16}
  71. self._run_test(data=data, minutes=10080, passes=False)
  72. def test_one_minute_no_events(self):
  73. data = {"interval": "1m", "value": 6}
  74. self._run_test(data=data, minutes=1, passes=False)
  75. def test_one_hour_no_events(self):
  76. data = {"interval": "1h", "value": 6}
  77. self._run_test(data=data, minutes=60, passes=False)
  78. def test_one_day_no_events(self):
  79. data = {"interval": "1d", "value": 6}
  80. self._run_test(data=data, minutes=1440, passes=False)
  81. def test_one_week_no_events(self):
  82. data = {"interval": "1w", "value": 6}
  83. self._run_test(data=data, minutes=10080, passes=False)
  84. def test_comparison(self):
  85. # Test data is 4 events in the current period and 2 events in the comparison period, so
  86. # a 100% increase.
  87. event = self.store_event(
  88. data={
  89. "fingerprint": ["something_random"],
  90. "timestamp": iso_format(before_now(minutes=1)),
  91. "user": {"id": uuid4().hex},
  92. },
  93. project_id=self.project.id,
  94. )
  95. self.increment(
  96. event,
  97. 3,
  98. timestamp=now() - timedelta(minutes=1),
  99. )
  100. self.increment(
  101. event,
  102. 2,
  103. timestamp=now() - timedelta(days=1, minutes=20),
  104. )
  105. data = {
  106. "interval": "1h",
  107. "value": 99,
  108. "comparisonType": "percent",
  109. "comparisonInterval": "1d",
  110. }
  111. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  112. self.assertPasses(rule, event)
  113. data = {
  114. "interval": "1h",
  115. "value": 101,
  116. "comparisonType": "percent",
  117. "comparisonInterval": "1d",
  118. }
  119. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  120. self.assertDoesNotPass(rule, event)
  121. def test_comparison_empty_comparison_period(self):
  122. # Test data is 1 event in the current period and 0 events in the comparison period. This
  123. # should always result in 0 and never fire.
  124. event = self.store_event(
  125. data={
  126. "fingerprint": ["something_random"],
  127. "timestamp": iso_format(before_now(minutes=1)),
  128. "user": {"id": uuid4().hex},
  129. },
  130. project_id=self.project.id,
  131. )
  132. data = {
  133. "interval": "1h",
  134. "value": 0,
  135. "comparisonType": "percent",
  136. "comparisonInterval": "1d",
  137. }
  138. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  139. self.assertDoesNotPass(rule, event)
  140. data = {
  141. "interval": "1h",
  142. "value": 100,
  143. "comparisonType": "percent",
  144. "comparisonInterval": "1d",
  145. }
  146. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  147. self.assertDoesNotPass(rule, event)
  148. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  149. @region_silo_test
  150. class EventFrequencyConditionTestCase(
  151. FrequencyConditionMixin, StandardIntervalMixin, RuleTestCase, SnubaTestCase
  152. ):
  153. rule_cls = EventFrequencyCondition
  154. def increment(self, event, count, environment=None, timestamp=None):
  155. data = {
  156. "fingerprint": event.data["fingerprint"],
  157. "timestamp": iso_format(timestamp) if timestamp else iso_format(before_now(minutes=1)),
  158. }
  159. if environment:
  160. data["environment"] = environment
  161. for _ in range(count):
  162. self.store_event(
  163. data=data,
  164. project_id=self.project.id,
  165. )
  166. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  167. @region_silo_test
  168. class EventUniqueUserFrequencyConditionTestCase(
  169. FrequencyConditionMixin,
  170. StandardIntervalMixin,
  171. RuleTestCase,
  172. SnubaTestCase,
  173. ):
  174. rule_cls = EventUniqueUserFrequencyCondition
  175. def increment(self, event, count, environment=None, timestamp=None):
  176. data = {
  177. "fingerprint": event.data["fingerprint"],
  178. "timestamp": iso_format(timestamp) if timestamp else iso_format(before_now(minutes=1)),
  179. }
  180. if environment:
  181. data["environment"] = environment
  182. for _ in range(count):
  183. event_data = deepcopy(data)
  184. event_data["user"] = {"id": uuid4().hex}
  185. self.store_event(
  186. data=event_data,
  187. project_id=self.project.id,
  188. )
  189. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  190. @region_silo_test
  191. class EventFrequencyPercentConditionTestCase(
  192. RuleTestCase,
  193. SnubaTestCase,
  194. ):
  195. rule_cls = EventFrequencyPercentCondition
  196. def _make_sessions(self, num):
  197. received = time.time()
  198. def make_session(i):
  199. return dict(
  200. distinct_id=uuid4().hex,
  201. session_id=uuid4().hex,
  202. org_id=self.project.organization_id,
  203. project_id=self.project.id,
  204. status="ok",
  205. seq=0,
  206. release="foo@1.0.0",
  207. environment="prod",
  208. retention_days=90,
  209. duration=None,
  210. errors=0,
  211. # The line below is crucial to spread sessions throughout the time period.
  212. started=received - i,
  213. received=received,
  214. )
  215. self.bulk_store_sessions([make_session(i) for i in range(num)])
  216. def _run_test(self, minutes, data, passes, add_events=False):
  217. if not self.environment or self.environment.name != "prod":
  218. self.environment = self.create_environment(name="prod")
  219. if not hasattr(self, "test_event"):
  220. self.test_event = self.store_event(
  221. data={
  222. "fingerprint": ["something_random"],
  223. "timestamp": iso_format(before_now(minutes=minutes)),
  224. "user": {"id": uuid4().hex},
  225. "environment": self.environment.name,
  226. },
  227. project_id=self.project.id,
  228. )
  229. if add_events:
  230. self.increment(
  231. self.test_event,
  232. max(1, int(minutes / 2)) - 1,
  233. environment=self.environment.name,
  234. timestamp=now() - timedelta(minutes=minutes),
  235. )
  236. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  237. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  238. if passes:
  239. self.assertPasses(rule, self.test_event)
  240. self.assertPasses(environment_rule, self.test_event)
  241. else:
  242. self.assertDoesNotPass(rule, self.test_event)
  243. self.assertDoesNotPass(environment_rule, self.test_event)
  244. def increment(self, event, count, environment=None, timestamp=None):
  245. data = {
  246. "fingerprint": event.data["fingerprint"],
  247. "timestamp": iso_format(timestamp) if timestamp else iso_format(before_now(minutes=1)),
  248. }
  249. if environment:
  250. data["environment"] = environment
  251. for _ in range(count):
  252. event_data = deepcopy(data)
  253. event_data["user"] = {"id": uuid4().hex}
  254. self.store_event(
  255. data=event_data,
  256. project_id=self.project.id,
  257. )
  258. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  259. def test_five_minutes_with_events(self):
  260. self._make_sessions(60)
  261. data = {"interval": "5m", "value": 39}
  262. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  263. data = {"interval": "5m", "value": 41}
  264. self._run_test(data=data, minutes=5, passes=False)
  265. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  266. def test_ten_minutes_with_events(self):
  267. self._make_sessions(60)
  268. data = {"interval": "10m", "value": 49}
  269. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  270. data = {"interval": "10m", "value": 51}
  271. self._run_test(data=data, minutes=10, passes=False)
  272. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  273. def test_thirty_minutes_with_events(self):
  274. self._make_sessions(60)
  275. data = {"interval": "30m", "value": 49}
  276. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  277. data = {"interval": "30m", "value": 51}
  278. self._run_test(data=data, minutes=30, passes=False)
  279. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  280. def test_one_hour_with_events(self):
  281. self._make_sessions(60)
  282. data = {"interval": "1h", "value": 49}
  283. self._run_test(data=data, minutes=60, add_events=True, passes=True)
  284. data = {"interval": "1h", "value": 51}
  285. self._run_test(data=data, minutes=60, passes=False)
  286. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  287. def test_five_minutes_no_events(self):
  288. self._make_sessions(60)
  289. data = {"interval": "5m", "value": 39}
  290. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  291. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  292. def test_ten_minutes_no_events(self):
  293. self._make_sessions(60)
  294. data = {"interval": "10m", "value": 49}
  295. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  296. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  297. def test_thirty_minutes_no_events(self):
  298. self._make_sessions(60)
  299. data = {"interval": "30m", "value": 49}
  300. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  301. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  302. def test_one_hour_no_events(self):
  303. self._make_sessions(60)
  304. data = {"interval": "1h", "value": 49}
  305. self._run_test(data=data, minutes=60, passes=False)
  306. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  307. def test_comparison(self):
  308. self._make_sessions(10)
  309. # Create sessions for previous period
  310. self._make_sessions(10)
  311. # Test data is 2 events in the current period and 1 events in the comparison period.
  312. # Number of sessions is 20 in each period, so current period is 20% of sessions, prev
  313. # is 10%. Overall a 100% increase comparitively.
  314. event = self.store_event(
  315. data={
  316. "fingerprint": ["something_random"],
  317. "timestamp": iso_format(before_now(minutes=1)),
  318. },
  319. project_id=self.project.id,
  320. )
  321. self.increment(
  322. event,
  323. 1,
  324. timestamp=now() - timedelta(minutes=1),
  325. )
  326. self.increment(
  327. event,
  328. 1,
  329. timestamp=now() - timedelta(days=1, minutes=20),
  330. )
  331. data = {
  332. "interval": "1h",
  333. "value": 99,
  334. "comparisonType": "percent",
  335. "comparisonInterval": "1d",
  336. }
  337. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  338. self.assertPasses(rule, event)
  339. data = {
  340. "interval": "1h",
  341. "value": 101,
  342. "comparisonType": "percent",
  343. "comparisonInterval": "1d",
  344. }
  345. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  346. self.assertDoesNotPass(rule, event)