test_event_frequency.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import time
  2. from copy import deepcopy
  3. from datetime import timedelta, timezone
  4. from unittest.mock import patch
  5. from uuid import uuid4
  6. import pytest
  7. from django.utils.timezone import now
  8. from sentry.issues.grouptype import PerformanceNPlusOneGroupType
  9. from sentry.models.rule import Rule
  10. from sentry.rules.conditions.event_frequency import (
  11. EventFrequencyCondition,
  12. EventFrequencyPercentCondition,
  13. EventUniqueUserFrequencyCondition,
  14. )
  15. from sentry.testutils.abstract import Abstract
  16. from sentry.testutils.cases import PerformanceIssueTestCase, RuleTestCase, SnubaTestCase
  17. from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
  18. from sentry.testutils.silo import region_silo_test
  19. from sentry.testutils.skips import requires_snuba
  20. from sentry.utils.samples import load_data
  21. pytestmark = [requires_snuba]
  22. class ErrorEventMixin(SnubaTestCase):
  23. def add_event(self, data, project_id, timestamp):
  24. data["timestamp"] = iso_format(timestamp)
  25. # Store an error event
  26. event = self.store_event(
  27. data=data,
  28. project_id=project_id,
  29. )
  30. return event.for_group(event.group)
  31. class PerfIssuePlatformEventMixin(PerformanceIssueTestCase):
  32. def add_event(self, data, project_id, timestamp):
  33. fingerprint = data["fingerprint"][0]
  34. fingerprint = (
  35. fingerprint
  36. if "-" in fingerprint
  37. else f"{PerformanceNPlusOneGroupType.type_id}-{data['fingerprint'][0]}"
  38. )
  39. event_data = load_data(
  40. "transaction-n-plus-one",
  41. timestamp=timestamp.replace(tzinfo=timezone.utc),
  42. start_timestamp=timestamp.replace(tzinfo=timezone.utc),
  43. fingerprint=[fingerprint],
  44. )
  45. event_data["user"] = {"id": uuid4().hex}
  46. event_data["environment"] = data.get("environment")
  47. for tag in event_data["tags"]:
  48. if tag[0] == "environment":
  49. tag[1] = data.get("environment")
  50. break
  51. else:
  52. event_data["tags"].append(data.get("environment"))
  53. # Store a performance event
  54. event = self.create_performance_issue(
  55. event_data=event_data,
  56. project_id=project_id,
  57. fingerprint=fingerprint,
  58. )
  59. return event
  60. @pytest.mark.snuba_ci
  61. class StandardIntervalTestBase(SnubaTestCase, RuleTestCase):
  62. __test__ = Abstract(__module__, __qualname__) # type: ignore[name-defined] # python/mypy#10570
  63. def add_event(self, data, project_id, timestamp):
  64. raise NotImplementedError
  65. def increment(self, event, count, environment=None, timestamp=None):
  66. raise NotImplementedError
  67. def _run_test(self, minutes, data, passes, add_events=False):
  68. if not self.environment:
  69. self.environment = self.create_environment(name="prod")
  70. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  71. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  72. event = self.add_event(
  73. data={
  74. "fingerprint": ["something_random"],
  75. "user": {"id": uuid4().hex},
  76. },
  77. project_id=self.project.id,
  78. timestamp=before_now(minutes=minutes),
  79. )
  80. if add_events:
  81. self.increment(
  82. event,
  83. data["value"] + 1,
  84. environment=self.environment.name,
  85. timestamp=now() - timedelta(minutes=minutes),
  86. )
  87. self.increment(
  88. event,
  89. data["value"] + 1,
  90. timestamp=now() - timedelta(minutes=minutes),
  91. )
  92. if passes:
  93. self.assertPasses(rule, event)
  94. self.assertPasses(environment_rule, event)
  95. else:
  96. self.assertDoesNotPass(rule, event)
  97. self.assertDoesNotPass(environment_rule, event)
  98. def test_one_minute_with_events(self):
  99. data = {"interval": "1m", "value": 6}
  100. self._run_test(data=data, minutes=1, passes=True, add_events=True)
  101. data = {"interval": "1m", "value": 16}
  102. self._run_test(data=data, minutes=1, passes=False)
  103. def test_one_hour_with_events(self):
  104. data = {"interval": "1h", "value": 6}
  105. self._run_test(data=data, minutes=60, passes=True, add_events=True)
  106. data = {"interval": "1h", "value": 16}
  107. self._run_test(data=data, minutes=60, passes=False)
  108. def test_one_day_with_events(self):
  109. data = {"interval": "1d", "value": 6}
  110. self._run_test(data=data, minutes=1440, passes=True, add_events=True)
  111. data = {"interval": "1d", "value": 16}
  112. self._run_test(data=data, minutes=1440, passes=False)
  113. def test_one_week_with_events(self):
  114. data = {"interval": "1w", "value": 6}
  115. self._run_test(data=data, minutes=10080, passes=True, add_events=True)
  116. data = {"interval": "1w", "value": 16}
  117. self._run_test(data=data, minutes=10080, passes=False)
  118. def test_one_minute_no_events(self):
  119. data = {"interval": "1m", "value": 6}
  120. self._run_test(data=data, minutes=1, passes=False)
  121. def test_one_hour_no_events(self):
  122. data = {"interval": "1h", "value": 6}
  123. self._run_test(data=data, minutes=60, passes=False)
  124. def test_one_day_no_events(self):
  125. data = {"interval": "1d", "value": 6}
  126. self._run_test(data=data, minutes=1440, passes=False)
  127. def test_one_week_no_events(self):
  128. data = {"interval": "1w", "value": 6}
  129. self._run_test(data=data, minutes=10080, passes=False)
  130. def test_comparison(self):
  131. # Test data is 4 events in the current period and 2 events in the comparison period, so
  132. # a 100% increase.
  133. event = self.add_event(
  134. data={
  135. "fingerprint": ["something_random"],
  136. "user": {"id": uuid4().hex},
  137. },
  138. project_id=self.project.id,
  139. timestamp=before_now(minutes=1),
  140. )
  141. self.increment(
  142. event,
  143. 3,
  144. timestamp=now() - timedelta(minutes=1),
  145. )
  146. self.increment(
  147. event,
  148. 2,
  149. timestamp=now() - timedelta(days=1, minutes=20),
  150. )
  151. data = {
  152. "interval": "1h",
  153. "value": 99,
  154. "comparisonType": "percent",
  155. "comparisonInterval": "1d",
  156. }
  157. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  158. self.assertPasses(rule, event)
  159. data = {
  160. "interval": "1h",
  161. "value": 101,
  162. "comparisonType": "percent",
  163. "comparisonInterval": "1d",
  164. }
  165. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  166. self.assertDoesNotPass(rule, event)
  167. def test_comparison_empty_comparison_period(self):
  168. # Test data is 1 event in the current period and 0 events in the comparison period. This
  169. # should always result in 0 and never fire.
  170. event = self.add_event(
  171. data={
  172. "fingerprint": ["something_random"],
  173. "user": {"id": uuid4().hex},
  174. },
  175. project_id=self.project.id,
  176. timestamp=before_now(minutes=1),
  177. )
  178. data = {
  179. "interval": "1h",
  180. "value": 0,
  181. "comparisonType": "percent",
  182. "comparisonInterval": "1d",
  183. }
  184. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  185. self.assertDoesNotPass(rule, event)
  186. data = {
  187. "interval": "1h",
  188. "value": 100,
  189. "comparisonType": "percent",
  190. "comparisonInterval": "1d",
  191. }
  192. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  193. self.assertDoesNotPass(rule, event)
  194. class EventFrequencyConditionTestCase(StandardIntervalTestBase):
  195. __test__ = Abstract(__module__, __qualname__) # type: ignore[name-defined] # python/mypy#10570
  196. rule_cls = EventFrequencyCondition
  197. def increment(self, event, count, environment=None, timestamp=None):
  198. timestamp = timestamp if timestamp else before_now(minutes=1)
  199. data = {"fingerprint": event.data["fingerprint"]}
  200. if environment:
  201. data["environment"] = environment
  202. for _ in range(count):
  203. self.add_event(
  204. data=data,
  205. project_id=self.project.id,
  206. timestamp=timestamp,
  207. )
  208. class EventUniqueUserFrequencyConditionTestCase(StandardIntervalTestBase):
  209. __test__ = Abstract(__module__, __qualname__) # type: ignore[name-defined] # python/mypy#10570
  210. rule_cls = EventUniqueUserFrequencyCondition
  211. def increment(self, event, count, environment=None, timestamp=None):
  212. timestamp = timestamp if timestamp else before_now(minutes=1)
  213. data = {"fingerprint": event.data["fingerprint"]}
  214. if environment:
  215. data["environment"] = environment
  216. for _ in range(count):
  217. event_data = deepcopy(data)
  218. event_data["user"] = {"id": uuid4().hex}
  219. self.add_event(
  220. data=event_data,
  221. project_id=self.project.id,
  222. timestamp=timestamp,
  223. )
  224. class EventFrequencyPercentConditionTestCase(SnubaTestCase, RuleTestCase):
  225. __test__ = Abstract(__module__, __qualname__) # type: ignore[name-defined] # python/mypy#10570
  226. rule_cls = EventFrequencyPercentCondition
  227. def add_event(self, data, project_id, timestamp):
  228. raise NotImplementedError
  229. def _make_sessions(self, num):
  230. received = time.time()
  231. def make_session(i):
  232. return dict(
  233. distinct_id=uuid4().hex,
  234. session_id=uuid4().hex,
  235. org_id=self.project.organization_id,
  236. project_id=self.project.id,
  237. status="ok",
  238. seq=0,
  239. release="foo@1.0.0",
  240. environment="prod",
  241. retention_days=90,
  242. duration=None,
  243. errors=0,
  244. # The line below is crucial to spread sessions throughout the time period.
  245. started=received - i,
  246. received=received,
  247. )
  248. self.bulk_store_sessions([make_session(i) for i in range(num)])
  249. def _run_test(self, minutes, data, passes, add_events=False):
  250. if not self.environment or self.environment.name != "prod":
  251. self.environment = self.create_environment(name="prod")
  252. if not hasattr(self, "test_event"):
  253. self.test_event = self.add_event(
  254. data={
  255. "fingerprint": ["something_random"],
  256. "user": {"id": uuid4().hex},
  257. "environment": self.environment.name,
  258. },
  259. project_id=self.project.id,
  260. timestamp=before_now(minutes=minutes),
  261. )
  262. if add_events:
  263. self.increment(
  264. self.test_event,
  265. max(1, int(minutes / 2)) - 1,
  266. environment=self.environment.name,
  267. timestamp=now() - timedelta(minutes=minutes),
  268. )
  269. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  270. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  271. if passes:
  272. self.assertPasses(rule, self.test_event)
  273. self.assertPasses(environment_rule, self.test_event)
  274. else:
  275. self.assertDoesNotPass(rule, self.test_event)
  276. self.assertDoesNotPass(environment_rule, self.test_event)
  277. def increment(self, event, count, environment=None, timestamp=None):
  278. data = {
  279. "fingerprint": event.data["fingerprint"],
  280. }
  281. timestamp = timestamp if timestamp else before_now(minutes=1)
  282. if environment:
  283. data["environment"] = environment
  284. for _ in range(count):
  285. event_data = deepcopy(data)
  286. event_data["user"] = {"id": uuid4().hex}
  287. self.add_event(
  288. data=event_data,
  289. project_id=self.project.id,
  290. timestamp=timestamp,
  291. )
  292. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  293. def test_five_minutes_with_events(self):
  294. self._make_sessions(60)
  295. data = {"interval": "5m", "value": 39}
  296. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  297. data = {"interval": "5m", "value": 41}
  298. self._run_test(data=data, minutes=5, passes=False)
  299. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  300. def test_ten_minutes_with_events(self):
  301. self._make_sessions(60)
  302. data = {"interval": "10m", "value": 49}
  303. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  304. data = {"interval": "10m", "value": 51}
  305. self._run_test(data=data, minutes=10, passes=False)
  306. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  307. def test_thirty_minutes_with_events(self):
  308. self._make_sessions(60)
  309. data = {"interval": "30m", "value": 49}
  310. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  311. data = {"interval": "30m", "value": 51}
  312. self._run_test(data=data, minutes=30, passes=False)
  313. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  314. def test_one_hour_with_events(self):
  315. self._make_sessions(60)
  316. data = {"interval": "1h", "value": 49}
  317. self._run_test(data=data, minutes=60, add_events=True, passes=True)
  318. data = {"interval": "1h", "value": 51}
  319. self._run_test(data=data, minutes=60, passes=False)
  320. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  321. def test_five_minutes_no_events(self):
  322. self._make_sessions(60)
  323. data = {"interval": "5m", "value": 39}
  324. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  325. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  326. def test_ten_minutes_no_events(self):
  327. self._make_sessions(60)
  328. data = {"interval": "10m", "value": 49}
  329. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  330. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  331. def test_thirty_minutes_no_events(self):
  332. self._make_sessions(60)
  333. data = {"interval": "30m", "value": 49}
  334. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  335. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  336. def test_one_hour_no_events(self):
  337. self._make_sessions(60)
  338. data = {"interval": "1h", "value": 49}
  339. self._run_test(data=data, minutes=60, passes=False)
  340. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  341. def test_comparison(self):
  342. self._make_sessions(10)
  343. # Create sessions for previous period
  344. self._make_sessions(10)
  345. # Test data is 2 events in the current period and 1 events in the comparison period.
  346. # Number of sessions is 20 in each period, so current period is 20% of sessions, prev
  347. # is 10%. Overall a 100% increase comparitively.
  348. event = self.add_event(
  349. data={"fingerprint": ["something_random"]},
  350. project_id=self.project.id,
  351. timestamp=before_now(minutes=1),
  352. )
  353. self.increment(
  354. event,
  355. 1,
  356. timestamp=now() - timedelta(minutes=1),
  357. )
  358. self.increment(
  359. event,
  360. 1,
  361. timestamp=now() - timedelta(days=1, minutes=20),
  362. )
  363. data = {
  364. "interval": "1h",
  365. "value": 99,
  366. "comparisonType": "percent",
  367. "comparisonInterval": "1d",
  368. }
  369. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  370. self.assertPasses(rule, event)
  371. data = {
  372. "interval": "1h",
  373. "value": 101,
  374. "comparisonType": "percent",
  375. "comparisonInterval": "1d",
  376. }
  377. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  378. self.assertDoesNotPass(rule, event)
  379. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  380. @region_silo_test
  381. class ErrorIssueFrequencyConditionTestCase(ErrorEventMixin, EventFrequencyConditionTestCase):
  382. pass
  383. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  384. @region_silo_test
  385. class PerfIssuePlatformIssueFrequencyConditionTestCase(
  386. PerfIssuePlatformEventMixin,
  387. EventFrequencyConditionTestCase,
  388. ):
  389. pass
  390. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  391. @region_silo_test
  392. class ErrorIssueUniqueUserFrequencyConditionTestCase(
  393. ErrorEventMixin,
  394. EventUniqueUserFrequencyConditionTestCase,
  395. ):
  396. pass
  397. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  398. @region_silo_test
  399. class PerfIssuePlatformIssueUniqueUserFrequencyConditionTestCase(
  400. PerfIssuePlatformEventMixin,
  401. EventUniqueUserFrequencyConditionTestCase,
  402. ):
  403. pass
  404. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  405. @region_silo_test
  406. class ErrorIssueEventFrequencyPercentConditionTestCase(
  407. ErrorEventMixin, EventFrequencyPercentConditionTestCase
  408. ):
  409. pass
  410. @freeze_time((now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0))
  411. @region_silo_test
  412. class PerfIssuePlatformIssueEventFrequencyPercentConditionTestCase(
  413. PerfIssuePlatformEventMixin,
  414. EventFrequencyPercentConditionTestCase,
  415. ):
  416. pass