Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1476,10 +1476,11 @@ boto3 위에 얹은, 얇고 로깅이 잘 된 래퍼들입니다. 우아한 성
(게이트 → 필터 → 라인업 선정 → 요약). 글 목록, 날짜 suffix,
제외 목록, 그리고 `CrawlReport`를 돌려줍니다.
4. `_log_run_summary`가 실행 퍼널을 JSON 한 줄(`RUN_SUMMARY {...}`)로 남깁니다.
소스 ok/empty/failed 수, 수집 수, 탈락 수, 최종 라인업(소스·점수·제목). 수백 줄의 산문
소스 ok/empty/failed 수와 글 페이지를 못 읽은 소스 수, 수집 수, 탈락 수, 최종 라인업(소스·점수·제목). 수백 줄의 산문
로그를 읽어 퍼널을 재구성하는 대신 CloudWatch Logs Insights로 바로 질의·추세화할 수
있습니다.
5. 소스가 실패했거나 후보를 내지 못했다면 크롤 헬스 알림을 발행합니다(§7).
5. 소스가 실패했거나, 후보를 내지 못했거나, 글 페이지를 하나도 읽지 못했다면 크롤 헬스 알림을
발행합니다(§7).
6. 살아남은 글이 하나도 없으면 200으로 종료합니다. 수집은 됐는데 전량 탈락했다면 그 전에
빈 다이제스트 알림을 발행합니다. 호가 비지 않았어도 일부 글을 모델 호출 실패로 잃었다면
모델 실패 알림을 보냅니다.
Expand Down Expand Up @@ -1515,7 +1516,7 @@ boto3 위에 얹은, 얇고 로깅이 잘 된 래퍼들입니다. 우아한 성

| 이벤트 | 함수 | 언제 | 왜 따로 알리는가 |
| --- | --- | --- | --- |
| `Crawl Health` — ALERT | `_send_crawl_health_alert` | 예상 밖 `FAILED` 소스나 후보 0개 소스가 있을 때 | 깨진 소스는 다이제스트에서 조용히 빠질 뿐입니다(§7) |
| `Crawl Health` — ALERT | `_send_crawl_health_alert` | 예상 밖 `FAILED` 소스, 후보 0개 소스, 글 페이지를 전부 못 읽은 소스가 있을 때 | 깨진 소스는 다이제스트에서 조용히 빠질 뿐입니다(§7) |
| `Empty Digest` — ALERT | `_maybe_send_empty_digest_alert` | 수집은 됐는데 살아남은 글이 0편일 때 | 실행은 200으로 끝나고 메일만 안 나갑니다. 탈락 사유 집계를 실어 대량 모델 실패와 엄격했을 뿐인 한 주를 구분하게 합니다 |
| `Model Failures` — WARN | `_maybe_send_model_failure_alert` | 호는 나가지만 일부 글을 모델 호출 실패로 잃었을 때 | 실패한 자리는 백필되므로 호가 얇아진 것만 남습니다 |
| `Newsletter Clipped` — ALERT | `_maybe_send_clipped_newsletter_alert` | 렌더 크기가 `GMAIL_CLIP_BYTES`를 넘을 때 | 발송은 성공하고 뒤쪽 카드만 안 보입니다(§13) |
Expand Down
39 changes: 25 additions & 14 deletions tests/test_main_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,11 +525,31 @@ def test_empty_digest_after_filtering_publishes_alert(self, monkeypatch):
assert len(session.sns.published) == 1
assert "Empty Digest" in session.sns.published[0]["Subject"]

def test_a_likely_broken_source_alone_raises_the_crawl_health_alert(
self, monkeypatch
@pytest.mark.parametrize(
"source",
[
# Fetched fine but offered nothing (candidates == 0): dead, not quiet.
SourceHealth(
url="https://moved.example/feed",
fetcher="RssFetcher",
status=SourceStatus.EMPTY,
candidates=0,
),
# Fetched fine, but no post's article page could be read.
SourceHealth(
url="https://blocked.example/feed",
fetcher="RssFetcher",
status=SourceStatus.OK,
post_count=3,
unreadable_pages=3,
),
],
ids=["likely_broken", "pages_unreadable"],
)
def test_a_source_that_did_not_fail_alone_raises_the_crawl_health_alert(
self, monkeypatch, source
):
# No source FAILED; one fetched fine but offered nothing (candidates == 0).
# That is the "dead vs quiet" signal, and the handler must page on it.
# No source FAILED; the handler must still page on either signal.
self._patch_common(monkeypatch)

class _Cfg:
Expand All @@ -548,16 +568,7 @@ class scraping:
session = _FakeSession()
monkeypatch.setattr(main, "_boto_sessions", lambda _cfg: (session, session))
monkeypatch.setattr(main, "_setup_aws_env", lambda *a, **k: None)
report = CrawlReport(
sources=[
SourceHealth(
url="https://moved.example/feed",
fetcher="RssFetcher",
status=SourceStatus.EMPTY,
candidates=0,
)
]
)
report = CrawlReport(sources=[source])
monkeypatch.setattr(
main,
"_fetch_and_filter_posts",
Expand Down
Loading