test_event_frequency.py 18 KB

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