test_tasks.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. from __future__ import annotations
  2. import time
  3. from unittest import mock
  4. import pytest
  5. from django.db.models import F
  6. from django.utils import timezone
  7. from sentry.models.grouprelease import GroupRelease
  8. from sentry.models.project import Project
  9. from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
  10. from sentry.models.repository import Repository
  11. from sentry.release_health.release_monitor.base import BaseReleaseMonitorBackend
  12. from sentry.release_health.release_monitor.metrics import MetricReleaseMonitorBackend
  13. from sentry.release_health.release_monitor.sessions import SessionReleaseMonitorBackend
  14. from sentry.release_health.tasks import monitor_release_adoption, process_projects_with_sessions
  15. from sentry.testutils.abstract import Abstract
  16. from sentry.testutils.cases import BaseMetricsTestCase, SnubaTestCase, TestCase
  17. from sentry.testutils.helpers.datetime import before_now, iso_format
  18. pytestmark = pytest.mark.sentry_metrics
  19. class BaseTestReleaseMonitor(TestCase, SnubaTestCase):
  20. __test__ = Abstract(__module__, __qualname__) # type: ignore[name-defined] # python/mypy#10570
  21. backend_class: type[BaseReleaseMonitorBackend]
  22. def setUp(self):
  23. super().setUp()
  24. backend = self.backend_class()
  25. self.backend = mock.patch("sentry.release_health.tasks.release_monitor", backend)
  26. self.backend.__enter__()
  27. self.project = self.create_project()
  28. self.project1 = self.create_project()
  29. self.project2 = self.create_project()
  30. self.project1.update(flags=F("flags").bitor(Project.flags.has_releases))
  31. self.project2.update(flags=F("flags").bitor(Project.flags.has_releases))
  32. self.repo = Repository.objects.create(
  33. organization_id=self.organization.id, name=self.organization.id
  34. )
  35. self.release = self.create_release(project=self.project, version="foo@1.0.0")
  36. self.release2 = self.create_release(project=self.project, version="foo@2.0.0")
  37. self.release3 = self.create_release(project=self.project2, version="bar@1.0.0")
  38. self.environment = self.create_environment(name="prod", project=self.project1)
  39. self.environment2 = self.create_environment(name="canary", project=self.project2)
  40. self.group = self.create_group(
  41. project=self.project, message="Kaboom!", first_release=self.release
  42. )
  43. self.rpe = ReleaseProjectEnvironment.objects.create(
  44. project_id=self.project1.id,
  45. release_id=self.release.id,
  46. environment_id=self.environment.id,
  47. )
  48. self.rpe1 = ReleaseProjectEnvironment.objects.create(
  49. project_id=self.project1.id,
  50. release_id=self.release2.id,
  51. environment_id=self.environment.id,
  52. )
  53. self.rpe2 = ReleaseProjectEnvironment.objects.create(
  54. project_id=self.project1.id,
  55. release_id=self.release3.id,
  56. environment_id=self.environment.id,
  57. )
  58. self.rpe3 = ReleaseProjectEnvironment.objects.create(
  59. project_id=self.project2.id,
  60. release_id=self.release.id,
  61. environment_id=self.environment2.id,
  62. )
  63. GroupRelease.objects.create(
  64. group_id=self.group.id, release_id=self.release.id, project_id=self.project.id
  65. )
  66. self.event = self.store_event(
  67. data={
  68. "message": "Kaboom!",
  69. "platform": "python",
  70. "timestamp": iso_format(before_now(seconds=10)),
  71. "stacktrace": {
  72. "frames": [
  73. {
  74. "function": "handle_set_commits",
  75. "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
  76. "module": "sentry.tasks",
  77. "in_app": True,
  78. "lineno": 30,
  79. "filename": "sentry/tasks.py",
  80. },
  81. {
  82. "function": "set_commits",
  83. "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
  84. "module": "sentry.models.release",
  85. "in_app": True,
  86. "lineno": 39,
  87. "filename": "sentry/models/release.py",
  88. },
  89. ]
  90. },
  91. "tags": {"sentry:release": self.release.version},
  92. "fingerprint": ["finterpring"],
  93. },
  94. project_id=self.project.id,
  95. )
  96. GroupRelease.objects.create(
  97. group_id=self.event.group.id, project_id=self.project.id, release_id=self.release.id
  98. )
  99. def tearDown(self):
  100. self.backend.__exit__(None, None, None)
  101. def test_simple(self):
  102. self.bulk_store_sessions([self.build_session(project_id=self.project1) for _ in range(11)])
  103. self.bulk_store_sessions(
  104. [
  105. self.build_session(project_id=self.project2, environment=self.environment2.name)
  106. for _ in range(1)
  107. ]
  108. )
  109. assert not self.project1.flags.has_sessions
  110. now = timezone.now()
  111. assert ReleaseProjectEnvironment.objects.filter(
  112. project_id=self.project1.id,
  113. release_id=self.release.id,
  114. environment_id=self.environment.id,
  115. adopted=None,
  116. ).exists()
  117. assert ReleaseProjectEnvironment.objects.filter(
  118. project_id=self.project2.id,
  119. release_id=self.release.id,
  120. environment_id=self.environment2.id,
  121. adopted=None,
  122. ).exists()
  123. assert not ReleaseProjectEnvironment.objects.filter(
  124. project_id=self.project1.id,
  125. release_id=self.release.id,
  126. environment_id=self.environment.id,
  127. adopted__gte=now,
  128. ).exists()
  129. assert not ReleaseProjectEnvironment.objects.filter(
  130. project_id=self.project2.id,
  131. release_id=self.release.id,
  132. environment_id=self.environment2.id,
  133. adopted__gte=now,
  134. ).exists()
  135. test_data = [
  136. {
  137. "org_id": [self.organization.id],
  138. "project_id": [self.project2.id, self.project1.id],
  139. },
  140. ]
  141. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  142. self.project1.refresh_from_db()
  143. assert self.project1.flags.has_sessions
  144. assert not ReleaseProjectEnvironment.objects.filter(
  145. project_id=self.project1.id,
  146. release_id=self.release.id,
  147. environment_id=self.environment.id,
  148. adopted=None,
  149. ).exists()
  150. assert ReleaseProjectEnvironment.objects.filter(
  151. project_id=self.project1.id,
  152. release_id=self.release.id,
  153. environment_id=self.environment.id,
  154. adopted__gte=now,
  155. ).exists()
  156. assert ReleaseProjectEnvironment.objects.filter(
  157. project_id=self.project2.id,
  158. release_id=self.release.id,
  159. environment_id=self.environment2.id,
  160. adopted__gte=now,
  161. ).exists()
  162. def test_simple_no_sessions(self):
  163. now = timezone.now()
  164. assert ReleaseProjectEnvironment.objects.filter(
  165. project_id=self.project1.id,
  166. release_id=self.release.id,
  167. environment_id=self.environment.id,
  168. adopted=None,
  169. ).exists()
  170. assert ReleaseProjectEnvironment.objects.filter(
  171. project_id=self.project2.id,
  172. release_id=self.release.id,
  173. environment_id=self.environment2.id,
  174. adopted=None,
  175. ).exists()
  176. assert not ReleaseProjectEnvironment.objects.filter(
  177. project_id=self.project1.id,
  178. release_id=self.release.id,
  179. environment_id=self.environment.id,
  180. adopted__gte=now,
  181. ).exists()
  182. assert not ReleaseProjectEnvironment.objects.filter(
  183. project_id=self.project2.id,
  184. release_id=self.release.id,
  185. environment_id=self.environment2.id,
  186. adopted__gte=now,
  187. ).exists()
  188. test_data = [
  189. {
  190. "org_id": [self.organization.id],
  191. "project_id": [self.project2.id, self.project1.id],
  192. },
  193. ]
  194. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  195. assert ReleaseProjectEnvironment.objects.filter(
  196. project_id=self.project1.id,
  197. release_id=self.release.id,
  198. environment_id=self.environment.id,
  199. adopted=None,
  200. ).exists()
  201. assert ReleaseProjectEnvironment.objects.filter(
  202. project_id=self.project2.id,
  203. release_id=self.release.id,
  204. environment_id=self.environment2.id,
  205. adopted=None,
  206. ).exists()
  207. assert not ReleaseProjectEnvironment.objects.filter(
  208. project_id=self.project1.id,
  209. release_id=self.release.id,
  210. environment_id=self.environment.id,
  211. adopted__gte=now,
  212. ).exists()
  213. assert not ReleaseProjectEnvironment.objects.filter(
  214. project_id=self.project2.id,
  215. release_id=self.release.id,
  216. environment_id=self.environment2.id,
  217. adopted__gte=now,
  218. ).exists()
  219. def test_release_is_unadopted_with_sessions(self):
  220. # Releases that are returned with sessions but no longer meet the threshold get unadopted
  221. self.bulk_store_sessions([self.build_session(project_id=self.project1) for _ in range(1)])
  222. self.bulk_store_sessions(
  223. [
  224. self.build_session(project_id=self.project2, environment=self.environment2)
  225. for _ in range(11)
  226. ]
  227. )
  228. self.bulk_store_sessions(
  229. [self.build_session(project_id=self.project1, release=self.release2) for _ in range(20)]
  230. )
  231. now = timezone.now()
  232. self.rpe.update(adopted=now)
  233. self.rpe1.update(adopted=now)
  234. assert ReleaseProjectEnvironment.objects.filter(
  235. project_id=self.project1.id,
  236. release_id=self.release.id,
  237. environment_id=self.environment.id,
  238. adopted=now,
  239. unadopted=None,
  240. ).exists()
  241. assert ReleaseProjectEnvironment.objects.filter(
  242. project_id=self.project1.id,
  243. release_id=self.release2.id,
  244. environment_id=self.environment.id,
  245. adopted=now,
  246. unadopted=None,
  247. ).exists()
  248. test_data = [
  249. {
  250. "org_id": [self.organization.id],
  251. "project_id": [self.project2.id, self.project1.id],
  252. },
  253. ]
  254. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  255. assert ReleaseProjectEnvironment.objects.filter(
  256. project_id=self.project1.id,
  257. release_id=self.release.id,
  258. environment_id=self.environment.id,
  259. adopted=now,
  260. unadopted__gte=now,
  261. ).exists()
  262. assert not ReleaseProjectEnvironment.objects.filter(
  263. project_id=self.project1.id,
  264. release_id=self.release2.id,
  265. environment_id=self.environment.id,
  266. adopted=now,
  267. unadopted__gte=now,
  268. ).exists()
  269. assert not ReleaseProjectEnvironment.objects.filter(
  270. project_id=self.project2.id,
  271. release_id=self.release.id,
  272. environment_id=self.environment2.id,
  273. adopted=now,
  274. unadopted__gte=now,
  275. ).exists()
  276. # Make sure re-adopting works
  277. self.bulk_store_sessions([self.build_session(project_id=self.project1) for _ in range(50)])
  278. time.sleep(1)
  279. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  280. assert ReleaseProjectEnvironment.objects.filter(
  281. project_id=self.project1.id,
  282. release_id=self.release.id,
  283. environment_id=self.environment.id,
  284. adopted=now, # doesn't get updated, unadopted just gets set to null
  285. unadopted=None,
  286. ).exists()
  287. def test_release_is_unadopted_without_sessions(self):
  288. # This test should verify that releases that have no sessions (i.e. no result from snuba)
  289. # get marked as unadopted
  290. now = timezone.now()
  291. self.rpe.update(adopted=now)
  292. assert ReleaseProjectEnvironment.objects.filter(
  293. project_id=self.project1.id,
  294. release_id=self.release.id,
  295. environment_id=self.environment.id,
  296. adopted=now,
  297. unadopted=None,
  298. ).exists()
  299. test_data = [
  300. {
  301. "org_id": [self.organization.id],
  302. "project_id": [self.project2.id, self.project1.id],
  303. },
  304. ]
  305. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  306. assert ReleaseProjectEnvironment.objects.filter(
  307. project_id=self.project1.id,
  308. release_id=self.release.id,
  309. environment_id=self.environment.id,
  310. adopted=now,
  311. unadopted__gte=now,
  312. ).exists()
  313. def test_multi_proj_env_release_counter(self):
  314. self.bulk_store_sessions(
  315. [
  316. self.build_session(
  317. project_id=self.project1,
  318. )
  319. for _ in range(11)
  320. ]
  321. )
  322. self.bulk_store_sessions(
  323. [
  324. self.build_session(project_id=self.project2, environment=self.environment2)
  325. for _ in range(1)
  326. ]
  327. )
  328. self.bulk_store_sessions(
  329. [self.build_session(project_id=self.project1, release=self.release2) for _ in range(1)]
  330. )
  331. self.bulk_store_sessions(
  332. [self.build_session(project_id=self.project1, release=self.release3) for _ in range(1)]
  333. )
  334. now = timezone.now()
  335. assert ReleaseProjectEnvironment.objects.filter(
  336. project_id=self.project1.id,
  337. release_id=self.release.id,
  338. environment_id=self.environment.id,
  339. adopted=None,
  340. ).exists()
  341. assert ReleaseProjectEnvironment.objects.filter(
  342. project_id=self.project2.id,
  343. release_id=self.release.id,
  344. environment_id=self.environment2.id,
  345. adopted=None,
  346. ).exists()
  347. assert not ReleaseProjectEnvironment.objects.filter(
  348. project_id=self.project1.id,
  349. release_id=self.release.id,
  350. environment_id=self.environment.id,
  351. adopted__gte=now,
  352. ).exists()
  353. assert not ReleaseProjectEnvironment.objects.filter(
  354. project_id=self.project2.id,
  355. release_id=self.release.id,
  356. environment_id=self.environment2.id,
  357. adopted__gte=now,
  358. ).exists()
  359. test_data = [
  360. {
  361. "org_id": [self.organization.id],
  362. "project_id": [self.project2.id, self.project1.id],
  363. },
  364. ]
  365. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  366. assert not ReleaseProjectEnvironment.objects.filter(
  367. project_id=self.project1.id,
  368. release_id=self.release.id,
  369. environment_id=self.environment.id,
  370. adopted=None,
  371. ).exists()
  372. assert ReleaseProjectEnvironment.objects.filter(
  373. project_id=self.project1.id,
  374. release_id=self.release.id,
  375. environment_id=self.environment.id,
  376. adopted__gte=now,
  377. ).exists()
  378. assert ReleaseProjectEnvironment.objects.filter(
  379. project_id=self.project2.id,
  380. release_id=self.release.id,
  381. environment_id=self.environment2.id,
  382. adopted__gte=now,
  383. ).exists()
  384. def test_monitor_release_adoption(self):
  385. now = timezone.now()
  386. self.org2 = self.create_organization(
  387. name="Yet Another Test Org",
  388. owner=self.user,
  389. )
  390. self.org2_project = self.create_project(organization=self.org2)
  391. self.org2_project.update(flags=F("flags").bitor(Project.flags.has_releases))
  392. self.org2_release = self.create_release(project=self.org2_project, version="org@2.0.0")
  393. self.org2_environment = self.create_environment(name="yae", project=self.org2_project)
  394. self.org2_rpe = ReleaseProjectEnvironment.objects.create(
  395. project_id=self.org2_project.id,
  396. release_id=self.org2_release.id,
  397. environment_id=self.org2_environment.id,
  398. )
  399. self.bulk_store_sessions(
  400. [
  401. self.build_session(
  402. org_id=self.org2,
  403. project_id=self.org2_project,
  404. release=self.org2_release,
  405. environment=self.org2_environment,
  406. )
  407. for _ in range(20)
  408. ]
  409. )
  410. # Tests the scheduled task to ensure it properly processes each org
  411. self.bulk_store_sessions(
  412. [
  413. self.build_session(
  414. project_id=self.project1,
  415. )
  416. for _ in range(11)
  417. ]
  418. )
  419. self.bulk_store_sessions(
  420. [
  421. self.build_session(project_id=self.project2, environment=self.environment2)
  422. for _ in range(1)
  423. ]
  424. )
  425. with self.tasks():
  426. monitor_release_adoption()
  427. assert ReleaseProjectEnvironment.objects.filter(
  428. project_id=self.project1.id,
  429. release_id=self.release.id,
  430. environment_id=self.environment.id,
  431. adopted__gte=now,
  432. unadopted=None,
  433. ).exists()
  434. assert ReleaseProjectEnvironment.objects.filter(
  435. project_id=self.org2_project.id,
  436. release_id=self.org2_release.id,
  437. environment_id=self.org2_environment.id,
  438. adopted__gte=now,
  439. unadopted=None,
  440. ).exists()
  441. def test_missing_rpe_is_created(self):
  442. self.bulk_store_sessions(
  443. [
  444. self.build_session(
  445. project_id=self.project1, release=self.release2, environment="somenvname"
  446. )
  447. for _ in range(20)
  448. ]
  449. )
  450. self.bulk_store_sessions(
  451. [
  452. self.build_session(project_id=self.project1, release=self.release2, environment="")
  453. for _ in range(20)
  454. ]
  455. )
  456. now = timezone.now()
  457. assert not ReleaseProjectEnvironment.objects.filter(
  458. project_id=self.project1.id,
  459. release_id=self.release.id,
  460. environment__name="somenvname",
  461. ).exists()
  462. assert not ReleaseProjectEnvironment.objects.filter(
  463. project_id=self.project1.id,
  464. release_id=self.release2.id,
  465. environment__name="",
  466. ).exists()
  467. test_data = [
  468. {
  469. "org_id": [self.organization.id],
  470. "project_id": [self.project2.id, self.project1.id],
  471. },
  472. ]
  473. # This will make the appropriate models (Environment, ReleaseProject, ReleaseEnvironment and ReleaseProjectEnvironment)
  474. process_projects_with_sessions(test_data[0]["org_id"][0], test_data[0]["project_id"])
  475. assert ReleaseProjectEnvironment.objects.filter(
  476. project_id=self.project1.id,
  477. release_id=self.release2.id,
  478. environment__name="somenvname",
  479. adopted__gte=now,
  480. ).exists()
  481. assert not ReleaseProjectEnvironment.objects.filter(
  482. project_id=self.project1.id,
  483. release_id=self.release2.id,
  484. environment__name="",
  485. ).exists()
  486. def test_has_releases_is_set(self):
  487. no_release_project = self.create_project()
  488. assert not no_release_project.flags.has_releases
  489. self.bulk_store_sessions(
  490. [
  491. self.build_session(
  492. project_id=no_release_project, release=self.release2, environment="somenvname"
  493. )
  494. ]
  495. )
  496. process_projects_with_sessions(no_release_project.organization_id, [no_release_project.id])
  497. no_release_project.refresh_from_db()
  498. assert no_release_project.flags.has_releases
  499. def test_no_env(self):
  500. no_env_project = self.create_project()
  501. assert not no_env_project.flags.has_releases
  502. # If environment is None, we shouldn't make any changes
  503. self.bulk_store_sessions(
  504. [self.build_session(project_id=no_env_project, release=self.release2, environment=None)]
  505. )
  506. process_projects_with_sessions(no_env_project.organization_id, [no_env_project.id])
  507. no_env_project.refresh_from_db()
  508. assert not no_env_project.flags.has_releases
  509. class TestSessionReleaseMonitor(BaseTestReleaseMonitor):
  510. backend_class = SessionReleaseMonitorBackend
  511. class TestMetricReleaseMonitor(BaseTestReleaseMonitor, BaseMetricsTestCase):
  512. backend_class = MetricReleaseMonitorBackend