From 52e40b06e236cb40f34a10d91691f8717f875e7e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:36:40 -0600 Subject: [PATCH] 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. --- frigate/notices/registry.py | 47 +++++++++++++++++----------- frigate/test/test_notice_registry.py | 12 +++++++ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/frigate/notices/registry.py b/frigate/notices/registry.py index 7e47689fa8..f55ab2f76c 100644 --- a/frigate/notices/registry.py +++ b/frigate/notices/registry.py @@ -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, diff --git a/frigate/test/test_notice_registry.py b/frigate/test/test_notice_registry.py index d950bea709..0bc94f2841 100644 --- a/frigate/test/test_notice_registry.py +++ b/frigate/test/test_notice_registry.py @@ -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):