test_event_frequency.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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.group import Group
  10. from sentry.models.project import Project
  11. from sentry.models.rule import Rule
  12. from sentry.rules.conditions.event_frequency import (
  13. EventFrequencyCondition,
  14. EventFrequencyPercentCondition,
  15. EventUniqueUserFrequencyCondition,
  16. )
  17. from sentry.testutils.abstract import Abstract
  18. from sentry.testutils.cases import (
  19. BaseMetricsTestCase,
  20. PerformanceIssueTestCase,
  21. RuleTestCase,
  22. SnubaTestCase,
  23. )
  24. from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
  25. from sentry.testutils.skips import requires_snuba
  26. from sentry.utils.samples import load_data
  27. pytestmark = [pytest.mark.sentry_metrics, requires_snuba]
  28. class BaseEventFrequencyPercentTest(BaseMetricsTestCase):
  29. def _make_sessions(
  30. self, num: int, environment_name: str | None = None, project: Project | None = None
  31. ):
  32. received = time.time()
  33. def make_session(i):
  34. return dict(
  35. distinct_id=uuid4().hex,
  36. session_id=uuid4().hex,
  37. org_id=project.organization_id if project else self.project.organization_id,
  38. project_id=project.id if project else self.project.id,
  39. status="ok",
  40. seq=0,
  41. release="foo@1.0.0",
  42. environment=environment_name if environment_name else "prod",
  43. retention_days=90,
  44. duration=None,
  45. errors=0,
  46. # The line below is crucial to spread sessions throughout the time period.
  47. started=received - i - 1,
  48. received=received,
  49. )
  50. self.bulk_store_sessions([make_session(i) for i in range(num)])
  51. class EventFrequencyQueryTestBase(SnubaTestCase, RuleTestCase, PerformanceIssueTestCase):
  52. def setUp(self):
  53. super().setUp()
  54. self.start = before_now(minutes=1)
  55. self.end = timezone.now()
  56. self.event = self.store_event(
  57. data={
  58. "event_id": "a" * 32,
  59. "environment": self.environment.name,
  60. "timestamp": iso_format(before_now(seconds=30)),
  61. "fingerprint": ["group-1"],
  62. "user": {"id": uuid4().hex},
  63. },
  64. project_id=self.project.id,
  65. )
  66. self.event2 = self.store_event(
  67. data={
  68. "event_id": "b" * 32,
  69. "environment": self.environment.name,
  70. "timestamp": iso_format(before_now(seconds=12)),
  71. "fingerprint": ["group-2"],
  72. "user": {"id": uuid4().hex},
  73. },
  74. project_id=self.project.id,
  75. )
  76. self.environment2 = self.create_environment(name="staging")
  77. self.event3 = self.store_event(
  78. data={
  79. "event_id": "c" * 32,
  80. "environment": self.environment2.name,
  81. "timestamp": iso_format(before_now(seconds=12)),
  82. "fingerprint": ["group-3"],
  83. "user": {"id": uuid4().hex},
  84. },
  85. project_id=self.project.id,
  86. )
  87. fingerprint = f"{PerformanceNPlusOneGroupType.type_id}-something_random"
  88. perf_event_data = load_data(
  89. "transaction-n-plus-one",
  90. timestamp=before_now(seconds=12),
  91. start_timestamp=before_now(seconds=13),
  92. fingerprint=[fingerprint],
  93. )
  94. perf_event_data["user"] = {"id": uuid4().hex}
  95. perf_event_data["environment"] = self.environment.name
  96. # Store a performance event
  97. self.perf_event = self.create_performance_issue(
  98. event_data=perf_event_data,
  99. project_id=self.project.id,
  100. fingerprint=fingerprint,
  101. )
  102. self.data = {"interval": "5m", "value": 30}
  103. self.condition_inst = self.get_rule(
  104. data=self.data,
  105. project=self.event.group.project,
  106. rule=Rule(environment_id=self.environment.id),
  107. )
  108. self.condition_inst2 = self.get_rule(
  109. data=self.data,
  110. project=self.event.group.project,
  111. rule=Rule(environment_id=self.environment2.id),
  112. )
  113. class EventFrequencyQueryTest(EventFrequencyQueryTestBase):
  114. rule_cls = EventFrequencyCondition
  115. def test_batch_query(self):
  116. batch_query = self.condition_inst.batch_query_hook(
  117. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  118. start=self.start,
  119. end=self.end,
  120. environment_id=self.environment.id,
  121. )
  122. assert batch_query == {
  123. self.event.group_id: 1,
  124. self.event2.group_id: 1,
  125. self.perf_event.group_id: 1,
  126. }
  127. batch_query = self.condition_inst2.batch_query_hook(
  128. group_ids=[self.event3.group_id],
  129. start=self.start,
  130. end=self.end,
  131. environment_id=self.environment2.id,
  132. )
  133. assert batch_query == {self.event3.group_id: 1}
  134. def test_get_error_and_generic_group_ids(self):
  135. groups = Group.objects.filter(
  136. id__in=[self.event.group_id, self.event2.group_id, self.perf_event.group_id]
  137. ).values_list("id", "type", "project__organization_id")
  138. error_issue_ids, generic_issue_ids = self.condition_inst.get_error_and_generic_group_ids(
  139. groups
  140. )
  141. assert self.event.group_id in error_issue_ids
  142. assert self.event2.group_id in error_issue_ids
  143. assert self.perf_event.group_id in generic_issue_ids
  144. class EventUniqueUserFrequencyQueryTest(EventFrequencyQueryTestBase):
  145. rule_cls = EventUniqueUserFrequencyCondition
  146. def test_batch_query_user(self):
  147. batch_query = self.condition_inst.batch_query_hook(
  148. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  149. start=self.start,
  150. end=self.end,
  151. environment_id=self.environment.id,
  152. )
  153. assert batch_query == {
  154. self.event.group_id: 1,
  155. self.event2.group_id: 1,
  156. self.perf_event.group_id: 1,
  157. }
  158. batch_query = self.condition_inst2.batch_query_hook(
  159. group_ids=[self.event3.group_id],
  160. start=self.start,
  161. end=self.end,
  162. environment_id=self.environment2.id,
  163. )
  164. assert batch_query == {self.event3.group_id: 1}
  165. class EventFrequencyPercentConditionQueryTest(
  166. EventFrequencyQueryTestBase, BaseEventFrequencyPercentTest
  167. ):
  168. rule_cls = EventFrequencyPercentCondition
  169. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  170. def test_batch_query_percent(self):
  171. self._make_sessions(60, self.environment2.name)
  172. self._make_sessions(60, self.environment.name)
  173. batch_query = self.condition_inst.batch_query_hook(
  174. group_ids=[self.event.group_id, self.event2.group_id, self.perf_event.group_id],
  175. start=self.start,
  176. end=self.end,
  177. environment_id=self.environment.id,
  178. )
  179. percent_of_sessions = 20
  180. assert batch_query == {
  181. self.event.group_id: percent_of_sessions,
  182. self.event2.group_id: percent_of_sessions,
  183. }
  184. batch_query = self.condition_inst2.batch_query_hook(
  185. group_ids=[self.event3.group_id],
  186. start=self.start,
  187. end=self.end,
  188. environment_id=self.environment2.id,
  189. )
  190. assert batch_query == {self.event3.group_id: percent_of_sessions}
  191. class ErrorEventMixin(SnubaTestCase):
  192. def add_event(self, data, project_id, timestamp):
  193. data["timestamp"] = iso_format(timestamp)
  194. # Store an error event
  195. event = self.store_event(
  196. data=data,
  197. project_id=project_id,
  198. )
  199. return event.for_group(event.group)
  200. class PerfIssuePlatformEventMixin(PerformanceIssueTestCase):
  201. def add_event(self, data, project_id, timestamp):
  202. fingerprint = data["fingerprint"][0]
  203. fingerprint = (
  204. fingerprint
  205. if "-" in fingerprint
  206. else f"{PerformanceNPlusOneGroupType.type_id}-{data['fingerprint'][0]}"
  207. )
  208. event_data = load_data(
  209. "transaction-n-plus-one",
  210. timestamp=timestamp,
  211. start_timestamp=timestamp,
  212. fingerprint=[fingerprint],
  213. )
  214. event_data["user"] = {"id": uuid4().hex}
  215. event_data["environment"] = data.get("environment")
  216. for tag in event_data["tags"]:
  217. if tag[0] == "environment":
  218. tag[1] = data.get("environment")
  219. break
  220. else:
  221. event_data["tags"].append(data.get("environment"))
  222. # Store a performance event
  223. event = self.create_performance_issue(
  224. event_data=event_data,
  225. project_id=project_id,
  226. fingerprint=fingerprint,
  227. )
  228. return event
  229. @pytest.mark.snuba_ci
  230. class StandardIntervalTestBase(SnubaTestCase, RuleTestCase, PerformanceIssueTestCase):
  231. __test__ = Abstract(__module__, __qualname__)
  232. def add_event(self, data, project_id, timestamp):
  233. raise NotImplementedError
  234. def increment(self, event, count, environment=None, timestamp=None):
  235. raise NotImplementedError
  236. def _run_test(self, minutes, data, passes, add_events=False):
  237. if not self.environment:
  238. self.environment = self.create_environment(name="prod")
  239. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  240. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  241. event = self.add_event(
  242. data={
  243. "fingerprint": ["something_random"],
  244. "user": {"id": uuid4().hex},
  245. },
  246. project_id=self.project.id,
  247. timestamp=before_now(minutes=minutes),
  248. )
  249. if add_events:
  250. self.increment(
  251. event,
  252. data["value"] + 1,
  253. environment=self.environment.name,
  254. timestamp=timezone.now() - timedelta(minutes=minutes),
  255. )
  256. self.increment(
  257. event,
  258. data["value"] + 1,
  259. timestamp=timezone.now() - timedelta(minutes=minutes),
  260. )
  261. if passes:
  262. self.assertPasses(rule, event, is_new=False)
  263. self.assertPasses(environment_rule, event, is_new=False)
  264. else:
  265. self.assertDoesNotPass(rule, event, is_new=False)
  266. self.assertDoesNotPass(environment_rule, event, is_new=False)
  267. def test_comparison_interval_empty_string(self):
  268. data = {
  269. "interval": "1m",
  270. "value": 16,
  271. "comparisonType": "count",
  272. "comparisonInterval": "",
  273. }
  274. self._run_test(data=data, minutes=1, passes=False)
  275. def test_one_minute_with_events(self):
  276. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  277. self._run_test(data=data, minutes=1, passes=True, add_events=True)
  278. data = {
  279. "interval": "1m",
  280. "value": 16,
  281. "comparisonType": "count",
  282. "comparisonInterval": "5m",
  283. }
  284. self._run_test(data=data, minutes=1, passes=False)
  285. def test_one_hour_with_events(self):
  286. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  287. self._run_test(data=data, minutes=60, passes=True, add_events=True)
  288. data = {
  289. "interval": "1h",
  290. "value": 16,
  291. "comparisonType": "count",
  292. "comparisonInterval": "5m",
  293. }
  294. self._run_test(data=data, minutes=60, passes=False)
  295. def test_one_day_with_events(self):
  296. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  297. self._run_test(data=data, minutes=1440, passes=True, add_events=True)
  298. data = {
  299. "interval": "1d",
  300. "value": 16,
  301. "comparisonType": "count",
  302. "comparisonInterval": "5m",
  303. }
  304. self._run_test(data=data, minutes=1440, passes=False)
  305. def test_one_week_with_events(self):
  306. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  307. self._run_test(data=data, minutes=10080, passes=True, add_events=True)
  308. data = {
  309. "interval": "1w",
  310. "value": 16,
  311. "comparisonType": "count",
  312. "comparisonInterval": "5m",
  313. }
  314. self._run_test(data=data, minutes=10080, passes=False)
  315. def test_one_minute_no_events(self):
  316. data = {"interval": "1m", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  317. self._run_test(data=data, minutes=1, passes=False)
  318. def test_one_hour_no_events(self):
  319. data = {"interval": "1h", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  320. self._run_test(data=data, minutes=60, passes=False)
  321. def test_one_day_no_events(self):
  322. data = {"interval": "1d", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  323. self._run_test(data=data, minutes=1440, passes=False)
  324. def test_one_week_no_events(self):
  325. data = {"interval": "1w", "value": 6, "comparisonType": "count", "comparisonInterval": "5m"}
  326. self._run_test(data=data, minutes=10080, passes=False)
  327. def test_comparison(self):
  328. # Test data is 4 events in the current period and 2 events in the comparison period, so
  329. # a 100% increase.
  330. event = self.add_event(
  331. data={
  332. "fingerprint": ["something_random"],
  333. "user": {"id": uuid4().hex},
  334. },
  335. project_id=self.project.id,
  336. timestamp=before_now(minutes=1),
  337. )
  338. self.increment(
  339. event,
  340. 3,
  341. timestamp=timezone.now() - timedelta(minutes=1),
  342. )
  343. self.increment(
  344. event,
  345. 2,
  346. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  347. )
  348. data = {
  349. "interval": "1h",
  350. "value": 99,
  351. "comparisonType": "percent",
  352. "comparisonInterval": "1d",
  353. }
  354. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  355. self.assertPasses(rule, event, is_new=False)
  356. data = {
  357. "interval": "1h",
  358. "value": 101,
  359. "comparisonType": "percent",
  360. "comparisonInterval": "1d",
  361. }
  362. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  363. self.assertDoesNotPass(rule, event, is_new=False)
  364. def test_comparison_empty_comparison_period(self):
  365. # Test data is 1 event in the current period and 0 events in the comparison period. This
  366. # should always result in 0 and never fire.
  367. event = self.add_event(
  368. data={
  369. "fingerprint": ["something_random"],
  370. "user": {"id": uuid4().hex},
  371. },
  372. project_id=self.project.id,
  373. timestamp=before_now(minutes=1),
  374. )
  375. data = {
  376. "interval": "1h",
  377. "value": 0,
  378. "comparisonType": "percent",
  379. "comparisonInterval": "1d",
  380. }
  381. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  382. self.assertDoesNotPass(rule, event, is_new=False)
  383. data = {
  384. "interval": "1h",
  385. "value": 100,
  386. "comparisonType": "percent",
  387. "comparisonInterval": "1d",
  388. }
  389. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  390. self.assertDoesNotPass(rule, event, is_new=False)
  391. @patch("sentry.rules.conditions.event_frequency.BaseEventFrequencyCondition.get_rate")
  392. def test_is_new_issue_skips_snuba(self, mock_get_rate):
  393. # Looking for more than 1 event
  394. data = {"interval": "1m", "value": 6}
  395. minutes = 1
  396. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  397. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  398. event = self.add_event(
  399. data={
  400. "fingerprint": ["something_random"],
  401. "user": {"id": uuid4().hex},
  402. },
  403. project_id=self.project.id,
  404. timestamp=before_now(minutes=minutes),
  405. )
  406. # Issue is new and is the first event
  407. self.assertDoesNotPass(rule, event, is_new=True)
  408. self.assertDoesNotPass(environment_rule, event, is_new=True)
  409. assert mock_get_rate.call_count == 0
  410. class EventFrequencyConditionTestCase(StandardIntervalTestBase):
  411. __test__ = Abstract(__module__, __qualname__)
  412. rule_cls = EventFrequencyCondition
  413. def increment(self, event, count, environment=None, timestamp=None):
  414. timestamp = timestamp if timestamp else before_now(minutes=1)
  415. data = {"fingerprint": event.data["fingerprint"]}
  416. if environment:
  417. data["environment"] = environment
  418. for _ in range(count):
  419. self.add_event(
  420. data=data,
  421. project_id=self.project.id,
  422. timestamp=timestamp,
  423. )
  424. class EventUniqueUserFrequencyConditionTestCase(StandardIntervalTestBase):
  425. __test__ = Abstract(__module__, __qualname__)
  426. rule_cls = EventUniqueUserFrequencyCondition
  427. def increment(self, event, count, environment=None, timestamp=None):
  428. timestamp = timestamp if timestamp else before_now(minutes=1)
  429. data = {"fingerprint": event.data["fingerprint"]}
  430. if environment:
  431. data["environment"] = environment
  432. for _ in range(count):
  433. event_data = deepcopy(data)
  434. event_data["user"] = {"id": uuid4().hex}
  435. self.add_event(
  436. data=event_data,
  437. project_id=self.project.id,
  438. timestamp=timestamp,
  439. )
  440. class EventFrequencyPercentConditionTestCase(BaseEventFrequencyPercentTest, RuleTestCase):
  441. __test__ = Abstract(__module__, __qualname__)
  442. rule_cls = EventFrequencyPercentCondition
  443. def add_event(self, data, project_id, timestamp):
  444. raise NotImplementedError
  445. def _run_test(self, minutes, data, passes, add_events=False):
  446. if not self.environment or self.environment.name != "prod":
  447. self.environment = self.create_environment(name="prod")
  448. if not hasattr(self, "test_event"):
  449. self.test_event = self.add_event(
  450. data={
  451. "fingerprint": ["something_random"],
  452. "user": {"id": uuid4().hex},
  453. "environment": self.environment.name,
  454. },
  455. project_id=self.project.id,
  456. timestamp=before_now(minutes=minutes),
  457. )
  458. if add_events:
  459. self.increment(
  460. self.test_event,
  461. max(1, int(minutes / 2)) - 1,
  462. environment=self.environment.name,
  463. timestamp=timezone.now() - timedelta(minutes=minutes),
  464. )
  465. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  466. environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id))
  467. if passes:
  468. self.assertPasses(rule, self.test_event, is_new=False)
  469. self.assertPasses(environment_rule, self.test_event, is_new=False)
  470. else:
  471. self.assertDoesNotPass(rule, self.test_event)
  472. self.assertDoesNotPass(environment_rule, self.test_event)
  473. def increment(self, event, count, environment=None, timestamp=None):
  474. data = {
  475. "fingerprint": event.data["fingerprint"],
  476. }
  477. timestamp = timestamp if timestamp else before_now(minutes=1)
  478. if environment:
  479. data["environment"] = environment
  480. for _ in range(count):
  481. event_data = deepcopy(data)
  482. event_data["user"] = {"id": uuid4().hex}
  483. self.add_event(
  484. data=event_data,
  485. project_id=self.project.id,
  486. timestamp=timestamp,
  487. )
  488. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  489. def test_five_minutes_with_events(self):
  490. self._make_sessions(60)
  491. data = {
  492. "interval": "5m",
  493. "value": 39,
  494. "comparisonType": "count",
  495. "comparisonInterval": "5m",
  496. }
  497. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  498. data = {
  499. "interval": "5m",
  500. "value": 41,
  501. "comparisonType": "count",
  502. "comparisonInterval": "5m",
  503. }
  504. self._run_test(data=data, minutes=5, passes=False)
  505. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  506. def test_ten_minutes_with_events(self):
  507. self._make_sessions(60)
  508. data = {
  509. "interval": "10m",
  510. "value": 49,
  511. "comparisonType": "count",
  512. "comparisonInterval": "5m",
  513. }
  514. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  515. data = {
  516. "interval": "10m",
  517. "value": 51,
  518. "comparisonType": "count",
  519. "comparisonInterval": "5m",
  520. }
  521. self._run_test(data=data, minutes=10, passes=False)
  522. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  523. def test_thirty_minutes_with_events(self):
  524. self._make_sessions(60)
  525. data = {
  526. "interval": "30m",
  527. "value": 49,
  528. "comparisonType": "count",
  529. "comparisonInterval": "5m",
  530. }
  531. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  532. data = {
  533. "interval": "30m",
  534. "value": 51,
  535. "comparisonType": "count",
  536. "comparisonInterval": "5m",
  537. }
  538. self._run_test(data=data, minutes=30, passes=False)
  539. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  540. def test_one_hour_with_events(self):
  541. self._make_sessions(60)
  542. data = {
  543. "interval": "1h",
  544. "value": 49,
  545. "comparisonType": "count",
  546. "comparisonInterval": "5m",
  547. }
  548. self._run_test(data=data, minutes=60, add_events=True, passes=True)
  549. data = {
  550. "interval": "1h",
  551. "value": 51,
  552. "comparisonType": "count",
  553. "comparisonInterval": "5m",
  554. }
  555. self._run_test(data=data, minutes=60, passes=False)
  556. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  557. def test_five_minutes_no_events(self):
  558. self._make_sessions(60)
  559. data = {
  560. "interval": "5m",
  561. "value": 39,
  562. "comparisonType": "count",
  563. "comparisonInterval": "5m",
  564. }
  565. self._run_test(data=data, minutes=5, passes=True, add_events=True)
  566. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  567. def test_ten_minutes_no_events(self):
  568. self._make_sessions(60)
  569. data = {
  570. "interval": "10m",
  571. "value": 49,
  572. "comparisonType": "count",
  573. "comparisonInterval": "5m",
  574. }
  575. self._run_test(data=data, minutes=10, passes=True, add_events=True)
  576. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  577. def test_thirty_minutes_no_events(self):
  578. self._make_sessions(60)
  579. data = {
  580. "interval": "30m",
  581. "value": 49,
  582. "comparisonType": "count",
  583. "comparisonInterval": "5m",
  584. }
  585. self._run_test(data=data, minutes=30, passes=True, add_events=True)
  586. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  587. def test_one_hour_no_events(self):
  588. self._make_sessions(60)
  589. data = {
  590. "interval": "1h",
  591. "value": 49,
  592. "comparisonType": "count",
  593. "comparisonInterval": "5m",
  594. }
  595. self._run_test(data=data, minutes=60, passes=False)
  596. @patch("sentry.rules.conditions.event_frequency.MIN_SESSIONS_TO_FIRE", 1)
  597. def test_comparison(self):
  598. self._make_sessions(10)
  599. # Create sessions for previous period
  600. self._make_sessions(10)
  601. # Test data is 2 events in the current period and 1 events in the comparison period.
  602. # Number of sessions is 20 in each period, so current period is 20% of sessions, prev
  603. # is 10%. Overall a 100% increase comparatively.
  604. event = self.add_event(
  605. data={"fingerprint": ["something_random"]},
  606. project_id=self.project.id,
  607. timestamp=before_now(minutes=1),
  608. )
  609. self.increment(
  610. event,
  611. 1,
  612. timestamp=timezone.now() - timedelta(minutes=1),
  613. )
  614. self.increment(
  615. event,
  616. 1,
  617. timestamp=timezone.now() - timedelta(days=1, minutes=20),
  618. )
  619. data = {
  620. "interval": "1h",
  621. "value": 99,
  622. "comparisonType": "percent",
  623. "comparisonInterval": "1d",
  624. }
  625. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  626. self.assertPasses(rule, event, is_new=False)
  627. data = {
  628. "interval": "1h",
  629. "value": 101,
  630. "comparisonType": "percent",
  631. "comparisonInterval": "1d",
  632. }
  633. rule = self.get_rule(data=data, rule=Rule(environment_id=None))
  634. self.assertDoesNotPass(rule, event, is_new=False)
  635. @freeze_time(
  636. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  637. )
  638. class ErrorIssueFrequencyConditionTestCase(ErrorEventMixin, EventFrequencyConditionTestCase):
  639. pass
  640. @freeze_time(
  641. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  642. )
  643. class PerfIssuePlatformIssueFrequencyConditionTestCase(
  644. PerfIssuePlatformEventMixin,
  645. EventFrequencyConditionTestCase,
  646. ):
  647. pass
  648. @freeze_time(
  649. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  650. )
  651. class ErrorIssueUniqueUserFrequencyConditionTestCase(
  652. ErrorEventMixin,
  653. EventUniqueUserFrequencyConditionTestCase,
  654. ):
  655. pass
  656. @freeze_time(
  657. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  658. )
  659. class PerfIssuePlatformIssueUniqueUserFrequencyConditionTestCase(
  660. PerfIssuePlatformEventMixin,
  661. EventUniqueUserFrequencyConditionTestCase,
  662. ):
  663. pass
  664. @freeze_time(
  665. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  666. )
  667. class ErrorIssueEventFrequencyPercentConditionTestCase(
  668. ErrorEventMixin, EventFrequencyPercentConditionTestCase
  669. ):
  670. pass
  671. @freeze_time(
  672. (timezone.now() - timedelta(days=2)).replace(hour=12, minute=40, second=0, microsecond=0)
  673. )
  674. class PerfIssuePlatformIssueEventFrequencyPercentConditionTestCase(
  675. PerfIssuePlatformEventMixin,
  676. EventFrequencyPercentConditionTestCase,
  677. ):
  678. pass