write held notice repeats before the analytics baseline reset

Repeats of batched kinds like failed_login wait in memory until the stats emitter flushes them, so a baseline reset taken while some were held left them out, and the next flush counted those pre-opt-in repeats as post-consent. The reset now writes held repeats to the notice history under the same lock before copying the counts.
This commit is contained in:
Josh Hawkins
2026-11-01 12:36:40 -06:00
parent b4ff802ddf
commit 52e40b06e2
2 changed files with 41 additions and 18 deletions
+29 -18
View File
@@ -132,24 +132,7 @@ class NoticeRegistry:
def flush(self) -> None:
"""Write the repeats that batch_repeats kinds held back."""
with self._lock:
held, self._held = self._held, {}
written = False
for row_id, repeats in held.items():
row = Notice.get_or_none(Notice.id == row_id)
# resolved while its repeats waited
if row is None:
continue
self._write_repeats(
row,
repeats.kind,
repeats.count,
repeats.last_seen,
repeats.params,
)
written = True
written = self._write_held()
if written:
self._notify()
@@ -388,12 +371,40 @@ class NoticeRegistry:
def reset_analytics_baseline(self) -> None:
"""Count analytics notices from now on, for a new analytics install ID."""
with self._lock:
# held repeats were raised before the reset, so they join the baseline
written = self._write_held()
NoticeStats.update(
reported_occurrences=NoticeStats.occurrences,
reported_acknowledgements=NoticeStats.acknowledgements,
reported_mutes=NoticeStats.mutes,
).execute()
if written:
self._notify()
def _write_held(self) -> bool:
# called with the lock held
held, self._held = self._held, {}
written = False
for row_id, repeats in held.items():
row = Notice.get_or_none(Notice.id == row_id)
# resolved while its repeats waited
if row is None:
continue
self._write_repeats(
row,
repeats.kind,
repeats.count,
repeats.last_seen,
repeats.params,
)
written = True
return written
def _write_repeats(
self,
row: Notice,
+12
View File
@@ -379,6 +379,18 @@ class TestAnalyticsBaseline(RegistryTestCase):
)
self.assertEqual(rows["skipped_detections"]["reported_occurrences"], 0)
@patch.dict(NOTICE_KINDS, TEST_KINDS)
def test_reset_puts_held_repeats_in_the_baseline(self):
for _ in range(3):
self.registry.raise_notice("batched")
self.registry.reset_analytics_baseline()
self.registry.flush()
[row] = self.registry.stats()
self.assertEqual((row["occurrences"], row["reported_occurrences"]), (3, 3))
self.assertEqual(self.registry.active()[0]["count"], 3)
class TestApply(RegistryTestCase):
def test_each_action_reaches_its_method(self):