Add feed-health monitor: alert when a segment's feed is empty/stale - #700
Merged
Conversation
Delivery-side feed monitor complementing the existing supply-side checks (crawler_liveness_check, feed_health_check). Sweeps the (learned_language, CEFR_level) segment matrix and, per segment, checks that fresh recommendable content exists and that a real active user actually gets a non-empty, fresh feed. Primary alert signal is segment-level fresh INVENTORY, built by reusing the app's own build_elastic_recommender_query with the segment's language + CEFR and no per-user seen-exclusion. A real active user's feed (via the real article_recommendations_for_user) is run as corroboration, since a power-user's personal feed can be legitimately empty. Alerts email via ZeeguuMailer like the other monitors and exit non-zero on failure. Not wired into cron (crontab lives in the ops repo); the daily line is proposed in the PR. Guards against the incident where a broken simplification pipeline left a Danish/B2+ segment silently empty for ~5 days while downloads were fine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
ArchLens - No architecturally relevant changes to the existing views |
We crawl several times a day, so for an alerting segment 'nothing fresh in ~a day' is already the alarm, not a multi-day grace -> STALE_DAYS default 4->1 (assumes the check runs after the day's crawl). Add ALERT_LEVELS (default A1,A2,B1,B2): C1/C2 are evaluated and shown in the report but never trigger the email / non-zero exit, because we don't complexify (generate above-level content) yet, so those levels are empty by design. Resolves the 'must-never-be-empty allow-list' open question. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mircealungu
added a commit
that referenced
this pull request
Aug 14, 2026
Feed-health review fixes (follow-up to #700)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this exists (the incident)
We already monitor the supply/download side:
crawler_liveness_check.py("is the crawler alive at all?") andfeed_health_check.py("which individual RSS feeds went quiet?"). Both watch whether articles get downloaded.But recently a real user's feed was silently EMPTY for ~5 days while downloads were perfectly fine. The simplification pipeline had broken, and that user's segment (Danish, B2+) is filtered toward simplified content — of which there was suddenly none. A supply/download check cannot see this: articles were arriving, they just never survived the per-user CEFR + feature-flag filtering to reach the feed.
This adds a delivery-side check: does a real user in each segment actually see a fresh feed?
Approach
The segment dimension that matters is
(learned_language, CEFR_level)— feed inventory and filters vary along it, and the incident hit one specific corner. For each segment the tool computes two signals:INVENTORY (primary alert trigger). Re-runs the app's own query builder
build_elastic_recommender_queryparameterized by the segment's language + CEFR level, with no per-user seen-exclusion and no topic narrowing. This is "does fresh content this segment could be shown even exist in the index?" It replicates the exactavailable_cefr_levelsfiltering that starved the incident's segment, without being confounded by any one user's reading history. Usesinclude_lower=Falseon purpose — the incident hit users who see only their exact level, so the strict same-level query is the worst-case signal.PER-USER CANARY (corroboration). Picks a real, recently-active user in the segment (
last_seenwithin--active-days, CEFR viacefr_level_for_learned_language) and calls the real feed functionarticle_recommendations_for_user(user, 10). This exercises the full delivery path: CEFR filtering, feature flags (simplified-only vs originals-first, "show easier"), disturbing filter, ES index, seen-exclusion, teacher-upload filtering. No synthetic users are created — only existing active users are read.Why inventory is the primary signal (the seen-history caveat)
A single power-user can legitimately have an empty personal feed — they've already opened everything fresh. That's a false positive for "the segment is broken." Segment-wide inventory has no per-user history, so an empty/stale inventory is the trustworthy alarm; the per-user canary is reported alongside as corroboration and as a reality check on the flags. When the canary feed is empty but inventory is healthy, the report explicitly says so rather than alerting.
Per segment, the status is:
min-articlesrecommendable items at allmin-freshare fresh (the incident shape)stale-daysOn any failing segment it emails a per-segment summary via
ZeeguuMailer(the app's own SMTP — cron stdout goes to an unread log, same rationale as the other monitors), prints a structured report, and exits non-zero so cron surfaces it.Thresholds (starting guesses — tune with real data)
All module constants, each env-overridable so cron can adjust without a code change:
FEED_HEALTH_ACTIVE_DAYSFEED_HEALTH_FRESH_DAYSFEED_HEALTH_MIN_ARTICLESFEED_HEALTH_MIN_FRESHFEED_HEALTH_STALE_DAYSFEED_HEALTH_COUNTFiles
tools/feed_delivery_health_check.py— the monitortools/test_feed_delivery_health_check.py— 11 unit tests for the pure freshness/staleness classification and segment-status decision (python -m pytest tools/test_feed_delivery_health_check.py, all green). The full ES+DB path needs a live index and is not unit-tested; the app context is created lazily insidemain()so the module imports DB-free for tests.Proposed crontab line (for the ops repo — NOT wired here)
Once daily, after the morning crawl has had time to run:
(Adjust to match the
run_task.shinvocation convention the other monitors use.)Open questions
(language, CEFR)segments that page vs. merely log, and to ignore tiny long-tail segments (one user at C2 in a thin-inventory language) that are empty by nature, not by breakage.min-fresh=3/stale-days=4are guesses. A few weeks of per-segment numbers (especially overnight/weekend low-water marks) should calibrate these; the strictinclude_lower=Falseinventory query may be too aggressive for genuinely thin-inventory languages.🤖 Generated with Claude Code