test_unmerge.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. import functools
  2. import hashlib
  3. import itertools
  4. import logging
  5. import uuid
  6. from datetime import datetime, timedelta
  7. from time import sleep
  8. from unittest.mock import patch
  9. import pytz
  10. from django.utils import timezone
  11. from sentry import eventstream, tagstore, tsdb
  12. from sentry.issues.escalating import get_group_hourly_count, query_groups_past_counts
  13. from sentry.issues.escalating_group_forecast import EscalatingGroupForecast
  14. from sentry.issues.forecasts import generate_and_save_forecasts
  15. from sentry.models import (
  16. Environment,
  17. Group,
  18. GroupHash,
  19. GroupRelease,
  20. GroupStatus,
  21. Release,
  22. UserReport,
  23. )
  24. from sentry.similarity import _make_index_backend, features
  25. from sentry.tasks.merge import merge_groups
  26. from sentry.tasks.unmerge import (
  27. get_caches,
  28. get_event_user_from_interface,
  29. get_fingerprint,
  30. get_group_backfill_attributes,
  31. get_group_creation_attributes,
  32. unmerge,
  33. )
  34. from sentry.testutils import SnubaTestCase, TestCase
  35. from sentry.testutils.helpers.datetime import before_now, iso_format
  36. from sentry.testutils.helpers.features import with_feature
  37. from sentry.tsdb.base import TSDBModel
  38. from sentry.types.group import GroupSubStatus
  39. from sentry.utils import redis
  40. from sentry.utils.dates import to_timestamp
  41. # Use the default redis client as a cluster client in the similarity index
  42. index = _make_index_backend(redis.clusters.get("default").get_local_client(0))
  43. @patch("sentry.similarity.features.index", new=index)
  44. class UnmergeTestCase(TestCase, SnubaTestCase):
  45. def create_message_event(
  46. self,
  47. template,
  48. parameters,
  49. environment,
  50. release,
  51. project,
  52. now,
  53. sequence,
  54. tag_values,
  55. user_values,
  56. fingerprint="group1",
  57. ):
  58. i = next(sequence)
  59. event_id = uuid.UUID(fields=(i, 0x0, 0x1000, 0x80, 0x80, 0x808080808080)).hex
  60. tags = [["color", next(tag_values)]]
  61. if release:
  62. tags.append(["sentry:release", release])
  63. event = self.store_event(
  64. data={
  65. "event_id": event_id,
  66. "message": template % parameters,
  67. "type": "default",
  68. "user": next(user_values),
  69. "tags": tags,
  70. "fingerprint": [fingerprint],
  71. "timestamp": iso_format(now + timedelta(seconds=i)),
  72. "environment": environment,
  73. "release": release,
  74. },
  75. project_id=project.id,
  76. )
  77. UserReport.objects.create(
  78. project_id=project.id,
  79. group_id=event.group.id,
  80. event_id=event_id,
  81. name="Log Hat",
  82. email="ceo@corptron.com",
  83. comments="Quack",
  84. )
  85. features.record([event])
  86. return event
  87. def test_get_fingerprint(self):
  88. assert (
  89. get_fingerprint(
  90. self.store_event(data={"message": "Hello world"}, project_id=self.project.id)
  91. )
  92. == hashlib.md5(b"Hello world").hexdigest()
  93. )
  94. assert (
  95. get_fingerprint(
  96. self.store_event(
  97. data={"message": "Hello world", "fingerprint": ["Not hello world"]},
  98. project_id=self.project.id,
  99. )
  100. )
  101. == hashlib.md5(b"Not hello world").hexdigest()
  102. )
  103. def test_get_group_creation_attributes(self):
  104. now = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc)
  105. e1 = self.store_event(
  106. data={
  107. "fingerprint": ["group1"],
  108. "platform": "javascript",
  109. "message": "Hello from JavaScript",
  110. "type": "default",
  111. "level": "info",
  112. "tags": {"logger": "javascript"},
  113. "timestamp": iso_format(now),
  114. },
  115. project_id=self.project.id,
  116. )
  117. e2 = self.store_event(
  118. data={
  119. "fingerprint": ["group1"],
  120. "platform": "python",
  121. "message": "Hello from Python",
  122. "type": "default",
  123. "level": "error",
  124. "tags": {"logger": "python"},
  125. "timestamp": iso_format(now),
  126. },
  127. project_id=self.project.id,
  128. )
  129. e3 = self.store_event(
  130. data={
  131. "fingerprint": ["group1"],
  132. "platform": "java",
  133. "message": "Hello from Java",
  134. "type": "default",
  135. "level": "debug",
  136. "tags": {"logger": "java"},
  137. "timestamp": iso_format(now),
  138. },
  139. project_id=self.project.id,
  140. )
  141. events = [e1, e2, e3]
  142. assert get_group_creation_attributes(get_caches(), events) == {
  143. "active_at": now,
  144. "first_seen": now,
  145. "last_seen": now,
  146. "platform": "java",
  147. "message": "Hello from JavaScript",
  148. "level": logging.INFO,
  149. "score": Group.calculate_score(3, now),
  150. "logger": "java",
  151. "times_seen": 3,
  152. "first_release": None,
  153. "culprit": "",
  154. "data": {
  155. "type": "default",
  156. "last_received": e1.data["received"],
  157. "metadata": {"title": "Hello from JavaScript"},
  158. },
  159. }
  160. def test_get_group_backfill_attributes(self):
  161. now = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc)
  162. assert get_group_backfill_attributes(
  163. get_caches(),
  164. Group(
  165. active_at=now,
  166. first_seen=now,
  167. last_seen=now,
  168. platform="javascript",
  169. message="Hello from JavaScript",
  170. level=logging.INFO,
  171. score=Group.calculate_score(3, now),
  172. logger="javascript",
  173. times_seen=1,
  174. first_release=None,
  175. culprit="",
  176. data={"type": "default", "last_received": to_timestamp(now), "metadata": {}},
  177. ),
  178. [
  179. self.store_event(
  180. data={
  181. "platform": "python",
  182. "message": "Hello from Python",
  183. "timestamp": iso_format(now - timedelta(hours=1)),
  184. "type": "default",
  185. "level": "debug",
  186. "tags": {"logger": "java"},
  187. },
  188. project_id=self.project.id,
  189. ),
  190. self.store_event(
  191. data={
  192. "platform": "java",
  193. "message": "Hello from Java",
  194. "timestamp": iso_format(now - timedelta(hours=2)),
  195. "type": "default",
  196. "level": "debug",
  197. "tags": {"logger": "java"},
  198. },
  199. project_id=self.project.id,
  200. ),
  201. ],
  202. ) == {
  203. "active_at": now - timedelta(hours=2),
  204. "first_seen": now - timedelta(hours=2),
  205. "platform": "java",
  206. "score": Group.calculate_score(3, now),
  207. "logger": "java",
  208. "times_seen": 3,
  209. "first_release": None,
  210. }
  211. @with_feature("projects:similarity-indexing")
  212. @with_feature("organizations:escalating-issues-v2")
  213. def test_unmerge(self):
  214. now = before_now(minutes=5).replace(microsecond=0, tzinfo=pytz.utc)
  215. def time_from_now(offset=0):
  216. return now + timedelta(seconds=offset)
  217. project = self.create_project()
  218. sequence = itertools.count(0)
  219. tag_values = itertools.cycle(["red", "green", "blue"])
  220. user_values = itertools.cycle([{"id": 1}, {"id": 2}])
  221. events = {}
  222. for event in (
  223. self.create_message_event(
  224. "This is message #%s.",
  225. i,
  226. environment="production",
  227. release="version",
  228. project=project,
  229. now=now,
  230. tag_values=tag_values,
  231. user_values=user_values,
  232. sequence=sequence,
  233. )
  234. for i in range(10)
  235. ):
  236. events.setdefault(get_fingerprint(event), []).append(event)
  237. for event in (
  238. self.create_message_event(
  239. "This is message #%s!",
  240. i,
  241. environment="production",
  242. release="version2",
  243. project=project,
  244. now=now,
  245. sequence=sequence,
  246. tag_values=tag_values,
  247. user_values=user_values,
  248. fingerprint="group2",
  249. )
  250. for i in range(10, 16)
  251. ):
  252. events.setdefault(get_fingerprint(event), []).append(event)
  253. event = self.create_message_event(
  254. "This is message #%s!",
  255. 17,
  256. environment="staging",
  257. release="version3",
  258. project=project,
  259. now=now,
  260. sequence=sequence,
  261. tag_values=tag_values,
  262. user_values=user_values,
  263. fingerprint="group3",
  264. )
  265. events.setdefault(get_fingerprint(event), []).append(event)
  266. merge_source, source, destination = list(Group.objects.all())
  267. source.status = GroupStatus.IGNORED
  268. source.substatus = GroupSubStatus.UNTIL_ESCALATING
  269. source.save()
  270. production_environment = Environment.objects.get(
  271. organization_id=project.organization_id, name="production"
  272. )
  273. with self.tasks():
  274. eventstream_state = eventstream.backend.start_merge(
  275. project.id, [merge_source.id], source.id
  276. )
  277. merge_groups.delay(
  278. [merge_source.id],
  279. source.id,
  280. eventstream_state=eventstream_state,
  281. handle_forecasts_ids=[source.id, merge_source.id],
  282. merge_forecasts=True,
  283. )
  284. sleep(1)
  285. single_hour = get_group_hourly_count(source)
  286. single_past = query_groups_past_counts([source])
  287. past = query_groups_past_counts(list(Group.objects.all()))
  288. generate_and_save_forecasts(list(Group.objects.all()))
  289. forecast = EscalatingGroupForecast.fetch(source.project.id, source.id).forecast
  290. # assert single_hour == 16
  291. assert single_past[0]["count()"] == 16
  292. assert past[0]["count()"] == 16
  293. assert past[1]["count()"] == 1
  294. assert forecast == [160] * 14 # change to 40 after default merges
  295. assert {
  296. (gtv.value, gtv.times_seen)
  297. for gtv in tagstore.get_group_tag_values(
  298. source,
  299. production_environment.id,
  300. "color",
  301. tenant_ids={"referrer": "get_tag_values", "organization_id": 1},
  302. )
  303. } == {("red", 6), ("green", 5), ("blue", 5)}
  304. similar_items = features.compare(source)
  305. assert len(similar_items) == 2
  306. assert similar_items[0][0] == source.id
  307. assert similar_items[0][1]["message:message:character-shingles"] == 1.0
  308. assert similar_items[1][0] == destination.id
  309. assert similar_items[1][1]["message:message:character-shingles"] < 1.0
  310. with self.tasks():
  311. unmerge.delay(
  312. project_id=project.id,
  313. source_id=source.id,
  314. destination_id=destination.id,
  315. fingerprints=[list(events.keys())[0]],
  316. actor_id=None,
  317. batch_size=5,
  318. )
  319. sleep(1)
  320. forecast = EscalatingGroupForecast.fetch(source.project.id, source.id).forecast
  321. single_hour = get_group_hourly_count(source)
  322. past = query_groups_past_counts(list(Group.objects.all()))
  323. single_past = query_groups_past_counts([source])
  324. assert single_hour == 6
  325. assert single_past[0]["count()"] == 6
  326. assert past[0]["count()"] == 6
  327. assert past[1]["count()"] == 11
  328. assert forecast == [60] * 14
  329. assert (
  330. list(
  331. Group.objects.filter(id=merge_source.id).values_list(
  332. "times_seen", "first_seen", "last_seen"
  333. )
  334. )
  335. == []
  336. )
  337. assert list(
  338. Group.objects.filter(id=source.id).values_list("times_seen", "first_seen", "last_seen")
  339. ) == [(6, time_from_now(10), time_from_now(15))]
  340. assert list(
  341. Group.objects.filter(id=destination.id).values_list(
  342. "times_seen", "first_seen", "last_seen"
  343. )
  344. ) == [(11, time_from_now(0), time_from_now(16))]
  345. assert source.id != destination.id
  346. assert source.project == destination.project
  347. destination_event_ids = set(map(lambda event: event.event_id, list(events.values())[1]))
  348. assert destination_event_ids == set(
  349. UserReport.objects.filter(group_id=source.id).values_list("event_id", flat=True)
  350. )
  351. assert list(
  352. GroupHash.objects.filter(group_id=source.id).values_list("hash", flat=True)
  353. ) == [list(events.keys())[1]]
  354. assert set(
  355. GroupRelease.objects.filter(group_id=source.id).values_list(
  356. "environment", "first_seen", "last_seen"
  357. )
  358. ) == {("production", time_from_now(10), time_from_now(15))}
  359. assert {
  360. (gtv.value, gtv.times_seen)
  361. for gtv in tagstore.get_group_tag_values(
  362. destination,
  363. production_environment.id,
  364. "color",
  365. tenant_ids={"referrer": "get_tag_values", "organization_id": 1},
  366. )
  367. } == {("red", 4), ("green", 3), ("blue", 3)}
  368. destination_event_ids = set(
  369. map(lambda event: event.event_id, list(events.values())[0] + list(events.values())[2])
  370. )
  371. assert destination_event_ids == set(
  372. UserReport.objects.filter(group_id=destination.id).values_list("event_id", flat=True)
  373. )
  374. assert set(
  375. GroupHash.objects.filter(group_id=destination.id).values_list("hash", flat=True)
  376. ) == {list(events.keys())[0], list(events.keys())[2]}
  377. assert set(
  378. GroupRelease.objects.filter(group_id=destination.id).values_list(
  379. "environment", "first_seen", "last_seen"
  380. )
  381. ) == {
  382. ("production", time_from_now(0), time_from_now(9)),
  383. ("staging", time_from_now(16), time_from_now(16)),
  384. }
  385. assert {
  386. (gtk.value, gtk.times_seen)
  387. for gtk in tagstore.get_group_tag_values(
  388. destination,
  389. production_environment.id,
  390. "color",
  391. tenant_ids={"referrer": "get_tag_values", "organization_id": 1},
  392. )
  393. } == {("red", 4), ("blue", 3), ("green", 3)}
  394. rollup_duration = 3600
  395. time_series = tsdb.get_range(
  396. TSDBModel.group,
  397. [source.id, destination.id],
  398. now - timedelta(seconds=rollup_duration),
  399. time_from_now(17),
  400. rollup_duration,
  401. tenant_ids={"referrer": "get_range", "organization_id": 1},
  402. )
  403. environment_time_series = tsdb.get_range(
  404. TSDBModel.group,
  405. [source.id, destination.id],
  406. now - timedelta(seconds=rollup_duration),
  407. time_from_now(17),
  408. rollup_duration,
  409. environment_ids=[production_environment.id],
  410. tenant_ids={"referrer": "get_range", "organization_id": 1},
  411. )
  412. def get_expected_series_values(rollup, events, function=None):
  413. if function is None:
  414. def function(aggregate, event):
  415. return (aggregate if aggregate is not None else 0) + 1
  416. expected = {}
  417. for event in events:
  418. k = float((to_timestamp(event.datetime) // rollup_duration) * rollup_duration)
  419. expected[k] = function(expected.get(k), event)
  420. return expected
  421. def assert_series_contains(expected, actual, default=0):
  422. actual = dict(actual)
  423. for key, value in expected.items():
  424. assert actual.get(key, 0) == value
  425. for key in set(actual.keys()) - set(expected.keys()):
  426. assert actual.get(key, 0) == default
  427. assert_series_contains(
  428. get_expected_series_values(rollup_duration, list(events.values())[1]),
  429. time_series[source.id],
  430. 0,
  431. )
  432. assert_series_contains(
  433. get_expected_series_values(
  434. rollup_duration, list(events.values())[0] + list(events.values())[2]
  435. ),
  436. time_series[destination.id],
  437. 0,
  438. )
  439. assert_series_contains(
  440. get_expected_series_values(rollup_duration, list(events.values())[1]),
  441. environment_time_series[source.id],
  442. 0,
  443. )
  444. assert_series_contains(
  445. get_expected_series_values(
  446. rollup_duration, list(events.values())[0][:-1] + list(events.values())[2]
  447. ),
  448. environment_time_series[destination.id],
  449. 0,
  450. )
  451. time_series = tsdb.get_distinct_counts_series(
  452. TSDBModel.users_affected_by_group,
  453. [source.id, destination.id],
  454. now - timedelta(seconds=rollup_duration),
  455. time_from_now(17),
  456. rollup_duration,
  457. tenant_ids={"referrer": "r", "organization_id": 1234},
  458. )
  459. environment_time_series = tsdb.get_distinct_counts_series(
  460. TSDBModel.users_affected_by_group,
  461. [source.id, destination.id],
  462. now - timedelta(seconds=rollup_duration),
  463. time_from_now(17),
  464. rollup_duration,
  465. environment_id=production_environment.id,
  466. tenant_ids={"referrer": "r", "organization_id": 1234},
  467. )
  468. def collect_by_user_tag(aggregate, event):
  469. aggregate = aggregate if aggregate is not None else set()
  470. aggregate.add(get_event_user_from_interface(event.data["user"]).tag_value)
  471. return aggregate
  472. for series in [time_series, environment_time_series]:
  473. assert_series_contains(
  474. {
  475. timestamp: len(values)
  476. for timestamp, values in get_expected_series_values(
  477. rollup_duration, list(events.values())[1], collect_by_user_tag
  478. ).items()
  479. },
  480. series[source.id],
  481. )
  482. assert_series_contains(
  483. {
  484. timestamp: len(values)
  485. for timestamp, values in get_expected_series_values(
  486. rollup_duration,
  487. list(events.values())[0] + list(events.values())[2],
  488. collect_by_user_tag,
  489. ).items()
  490. },
  491. time_series[destination.id],
  492. )
  493. def strip_zeroes(data):
  494. for group_id, series in data.items():
  495. for _, values in series:
  496. for key, val in list(values.items()):
  497. if val == 0:
  498. values.pop(key)
  499. return data
  500. def collect_by_release(group, aggregate, event):
  501. aggregate = aggregate if aggregate is not None else {}
  502. release = event.get_tag("sentry:release")
  503. if not release:
  504. return aggregate
  505. release = GroupRelease.objects.get(
  506. group_id=group.id,
  507. environment=event.data["environment"],
  508. release_id=Release.objects.get(
  509. organization_id=project.organization_id, version=release
  510. ).id,
  511. ).id
  512. aggregate[release] = aggregate.get(release, 0) + 1
  513. return aggregate
  514. items = {}
  515. for i in [source.id, destination.id]:
  516. items[i] = list(GroupRelease.objects.filter(group_id=i).values_list("id", flat=True))
  517. time_series = strip_zeroes(
  518. tsdb.get_frequency_series(
  519. TSDBModel.frequent_releases_by_group,
  520. items,
  521. now - timedelta(seconds=rollup_duration),
  522. time_from_now(17),
  523. rollup_duration,
  524. tenant_ids={"referrer": "r", "organization_id": 1234},
  525. )
  526. )
  527. assert_series_contains(
  528. get_expected_series_values(
  529. rollup_duration,
  530. list(events.values())[1],
  531. functools.partial(collect_by_release, source),
  532. ),
  533. time_series[source.id],
  534. {},
  535. )
  536. assert_series_contains(
  537. get_expected_series_values(
  538. rollup_duration,
  539. list(events.values())[0] + list(events.values())[2],
  540. functools.partial(collect_by_release, destination),
  541. ),
  542. time_series[destination.id],
  543. {},
  544. )
  545. items = {}
  546. for i in [source.id, destination.id]:
  547. items[i] = list(Environment.objects.all().values_list("id", flat=True))
  548. time_series = strip_zeroes(
  549. tsdb.get_frequency_series(
  550. TSDBModel.frequent_environments_by_group,
  551. items,
  552. now - timedelta(seconds=rollup_duration),
  553. time_from_now(17),
  554. rollup_duration,
  555. tenant_ids={"referrer": "r", "organization_id": 1234},
  556. )
  557. )
  558. def collect_by_environment(aggregate, event):
  559. aggregate = aggregate if aggregate is not None else {}
  560. environment = Environment.objects.get(
  561. organization_id=project.organization_id, name=event.data["environment"]
  562. ).id
  563. aggregate[environment] = aggregate.get(environment, 0) + 1
  564. return aggregate
  565. assert_series_contains(
  566. get_expected_series_values(
  567. rollup_duration, list(events.values())[1], collect_by_environment
  568. ),
  569. time_series[source.id],
  570. {},
  571. )
  572. assert_series_contains(
  573. get_expected_series_values(
  574. rollup_duration,
  575. list(events.values())[0] + list(events.values())[2],
  576. collect_by_environment,
  577. ),
  578. time_series[destination.id],
  579. {},
  580. )
  581. source_similar_items = features.compare(source)
  582. assert source_similar_items[0] == (
  583. source.id,
  584. {
  585. "exception:message:character-shingles": None,
  586. "exception:stacktrace:application-chunks": None,
  587. "exception:stacktrace:pairs": None,
  588. "message:message:character-shingles": 1.0,
  589. },
  590. )
  591. assert source_similar_items[1][0] == destination.id
  592. assert source_similar_items[1][1]["message:message:character-shingles"] < 1.0
  593. destination_similar_items = features.compare(destination)
  594. assert destination_similar_items[0] == (
  595. destination.id,
  596. {
  597. "exception:message:character-shingles": None,
  598. "exception:stacktrace:application-chunks": None,
  599. "exception:stacktrace:pairs": None,
  600. "message:message:character-shingles": 1.0,
  601. },
  602. )
  603. assert destination_similar_items[1][0] == source.id
  604. assert destination_similar_items[1][1]["message:message:character-shingles"] < 1.0
  605. @with_feature("organizations:escalating-issues-v2")
  606. def test_no_dest(self):
  607. now = before_now(minutes=5).replace(microsecond=0, tzinfo=pytz.utc)
  608. project = self.create_project()
  609. sequence = itertools.count(0)
  610. tag_values = itertools.cycle(["red", "green"])
  611. user_values = itertools.cycle([{"id": 1}, {"id": 2}])
  612. events = {}
  613. # Create 6 events for the child group now
  614. for event in (
  615. self.create_message_event(
  616. "This is message #%s!",
  617. i,
  618. environment="production",
  619. release="version",
  620. project=project,
  621. now=now,
  622. sequence=sequence,
  623. tag_values=tag_values,
  624. user_values=user_values,
  625. )
  626. for i in range(6)
  627. ):
  628. events.setdefault(get_fingerprint(event), []).append(event)
  629. # Create 10 events for the primary group now
  630. for event in (
  631. self.create_message_event(
  632. "This is message #%s.",
  633. i,
  634. environment="production",
  635. release="version2",
  636. project=project,
  637. now=now,
  638. tag_values=tag_values,
  639. user_values=user_values,
  640. sequence=sequence,
  641. fingerprint="group2",
  642. )
  643. for i in range(6, 16)
  644. ):
  645. events.setdefault(get_fingerprint(event), []).append(event)
  646. child, primary = list(Group.objects.all())
  647. primary.status = GroupStatus.IGNORED
  648. primary.substatus = GroupSubStatus.UNTIL_ESCALATING
  649. primary.save()
  650. # The following event counts should be true here:
  651. # get_group_hourly_count(primary) == 10
  652. # get_group_hourly_count(child) == 6
  653. # query_groups_past_counts should show the same counts
  654. # Merge primary and child
  655. with self.tasks():
  656. eventstream_state = eventstream.backend.start_merge(project.id, [child.id], primary.id)
  657. merge_groups.delay(
  658. [child.id],
  659. primary.id,
  660. eventstream_state=eventstream_state,
  661. handle_forecasts_ids=[primary.id, child.id],
  662. merge_forecasts=True,
  663. )
  664. sleep(1)
  665. # The following event counts should be true here:
  666. # get_group_hourly_count(primary) == 16
  667. # query_groups_past_counts should show the same count
  668. # Unmerge primary to create new_child
  669. with self.tasks():
  670. unmerge.delay(
  671. project_id=project.id,
  672. source_id=primary.id,
  673. destination_id=None,
  674. fingerprints=[list(events.keys())[0]],
  675. actor_id=None,
  676. batch_size=5,
  677. )
  678. sleep(1)
  679. # The event counts for primary should be 10; the event counts for new_child should be 6
  680. primary, new_child = list(Group.objects.all())
  681. print("Running queries")
  682. print("=" * 20)
  683. # print("Should be 10")
  684. # primary_unmerge_hour_count = get_group_hourly_count(
  685. # primary
  686. # ) # incorrect: returns 16, which is the count before unmerge
  687. print("Should be 10, 6")
  688. past = query_groups_past_counts(list(Group.objects.all())) # correct
  689. print("Should be 10")
  690. primary_unmerge_past_count = query_groups_past_counts(
  691. [primary]
  692. ) # incorrect: returns 16, which is the count before unmerge
  693. # print("Should be 6")
  694. # child_unmerge_hour_count = get_group_hourly_count(new_child)
  695. # print("Should be 6")
  696. # child_unmerge_past_count = query_groups_past_counts([new_child])
  697. assert past[0]["count()"] == 10
  698. assert past[1]["count()"] == 6
  699. # assert primary_unmerge_hour_count == 10
  700. assert primary_unmerge_past_count[0]["count()"] == 10
  701. # assert child_unmerge_hour_count == 6
  702. # assert child_unmerge_past_count[0]["count()"] == 6