# Changelog of YouTube Channel Change Monitor (`woolly_kingcrab/youtube-channel-change-monitor`) Actor

- **URL**: https://apify.com/woolly_kingcrab/youtube-channel-change-monitor/changelog.md
- **Full Actor documentation**: https://apify.com/woolly_kingcrab/youtube-channel-change-monitor.md

## Changelog

### 0.2 — commercial upgrade (local only, not deployed)

#### The real bug (fixed)

- **Per-channel failure isolation.** One bad/404 channel no longer aborts the whole run — the historical failure where `UCUZHFZ9jIKrLroW8LcyJEQQ` (the only channel in the live scheduled task) 404'd and killed every scheduled run. Each channel now returns `SUCCESS` or a typed `FAILED` row (`error_type` / `error_message`): `invalid_channel`, `channel_not_found`, `feed_not_found`, `timeout`, `fetch_failed`, `parse_failed`, `internal_error`. Reproduced deterministically in `tests/test_bug_repro.py`.
- Feed fetch retries 429/5xx with backoff; 404 → distinct `feed_not_found`.
- Duplicate channel entries de-duplicated within a run.

#### Change detection

- Flat output — one row per change; columns: `channel_id, channel_name, monitor_id, change_type, changed_field, video_id, video_title, video_url, previous_value, current_value, published_at, detected_at, first_seen_at, last_seen_at, error_type, error_message`.
- `NEW_VIDEO`, `TITLE_CHANGED`, `DESCRIPTION_CHANGED` (new — from `media:description`), `VIDEO_UPDATED`, `CHANNEL_METADATA_CHANGED`.
- **CDN noise guard:** a per-video title/description change is only reported when YouTube's `<updated>` timestamp also advanced. YouTube RSS edge caches briefly serve differing title revisions; without this guard those would produce billed false positives (observed live). If `<updated>` did not move, the last known-good snapshot is carried forward.
- `REMOVED_VIDEO` is intentionally **not** emitted — RSS is a rolling ~15-item window and a drop-out is not provable removal.
- `first_seen_at` / `last_seen_at` tracked per video; `detected_at` on every row.
- `outputMode`: `CHANGES_ONLY` (default) / `ALL_CHECKS`.

#### State

- State schema v2 (`channels[cid] = {first_seen_at, channelTitle, videos:{videoId: {...}}}`), keyed by `account + monitorId + channelId`. Automatic migration from v1 (`{snapshot:{videos:[...]}}`); unmigratable records dropped → re-baseline, no fabricated event.
- KVS name unchanged (`youtube-channel-monitor-{monitorId}`).

#### Monetization

- `src/billing.py`: `channel-checked` charged **only after** fetch + parse + compare succeed. Never for invalid/404/timeout/parse/internal failures.
- Per-run billing guard prevents double-charging on run resurrection.
- Candidate pricing ($0.00005 synthetic start + $0.02 / channel checked) in `pricing.candidate.json` / `PRICING.md`. Default + max memory 512 MB (`minMemoryMbytes` 256 / `maxMemoryMbytes` 1024 in actor.json).

#### Backward compatibility

- `emitBaseline` kept in `input_schema.json` as a **deprecated, inert boolean** (`default: false`). `additionalProperties` stays `false`. The v0.2 runtime never reads it. Keeps the live scheduled task input (`qqkP7bKnavyHW4QA7`, which sends `channels` + `emitBaseline` + `monitorId`) valid after deployment.
- **Deployment follow-up (not done here — live untouched):** task `qqkP7bKnavyHW4QA7` pins `memoryMbytes: 4096`; after a v0.2 smoke test change it to `512` and preferably drop `emitBaseline` from its stored input.

#### Tests

- `tests/test_core.py` — parse, baseline, unchanged, new video, title change (requires `updated` bump), title-flap suppression, channel metadata, no-false-removed, ALL_CHECKS, activity metrics, state migration.
- `tests/test_feed.py` — 200 OK, 404→feed_not_found, timeout→timeout, malformed XML→parse_failed, empty feed valid.
- `tests/test_billing.py` — charge once per channel, resurrection dedup, charge-failure tolerance, budget limit.
- `tests/test_schema_compat.py` — legacy task input validates; `emitBaseline` true/false validate; inert at runtime; unknown field rejected.
- `tests/test_bug_repro.py` — the `UCUZHFZ9jIKrLroW8LcyJEQQ` scenario: bad channel fails individually, valid channel still baselined + billed, run exits 0, failed channel not billed.
- `tests/test_demo.py` — deterministic fixture walk-through (baseline → NEW_VIDEO + TITLE_CHANGED → no duplicate on repeat).
