# Changelog of RSS Feed Reader: RSS, Atom & JSON Feeds, Keyword Alerts (`rookdev/rss-feed-monitor`) Actor

- **URL**: https://apify.com/rookdev/rss-feed-monitor/changelog.md
- **Full Actor documentation**: https://apify.com/rookdev/rss-feed-monitor.md

## Changelog

### Unreleased (listing only)

- Store title changed to lead with the plain job, not monitoring (2 Nov 2026; see docs/research/2026-11-02-store-crowding.md). The Actor name in its URL and all behaviour are unchanged.

### 0.3 (2026-10-30), cost protection

Hardening from the cost-abuse audit (docs/research/2026-10-30-cost-abuse-feeds.md), with the template's guards ported
in. Regression tests: `tests/test_cost_abuse.py` (the audit's eight failing RSS tests, plus the guards below).

- **Run cost guard:** a run stops starting new work once its compute and row writes pass what it has earned plus
  $0.02, checked before each feed, request and retry, inside the XML and JSON parse loops and for every item matched
  against keywords. A run that earns nothing now costs about $0.02 instead of $0.20 an hour. Paying runs never stop.
  `RUN_SUMMARY` gains `cost` and `stoppedEarly`.
- **Failure breaker:** 50 failed feeds in a row end the run; a host failing 10 times in a row is not called again.
- **Caps:** at most 1,000 feeds or websites per run, the rest in one free `skipped` row; `keywords` and
  `excludeKeywords` at most 100 entries of up to 200 characters (refused with one free `invalid_input` row, never
  cut silently; `maxItems` in the input schema). Invalid lines go out in one push.
- **Skipped rows:** inputs not checked because of the spending limit, the safety limit or the failure breaker share
  one free `skipped` row per reason (was one row per input).
- **Spending limit:** a feed is read only when the limit covers its check plus one item ($0.0015), counting the feeds
  already being read; a lower limit reads nothing (it used to read every feed and charge for at most one).
- **One parse per feed:** inputs reaching the same final URL (e.g. 50 links redirecting to one 30 MB feed) parse it
  once; the others are duplicates as before, decided before parsing instead of after.
- **Memory:** downloads ask for at most 2 MB first; bigger feeds are read again with the 30 MB feed cap (was the
  runtime's 60 MB) in heavy slots sized to the run's memory (1 at 256 MB, 4 at 1,024 MB), which also hold big and
  gzip bodies while they are parsed. Keyword filtering runs in a worker thread.
- **Requests (runtime from the template):** 10 s connect timeout, 60 s total per request, 2 attempts after a network
  failure, at most 2 requests at a time per host.
- **Private networks refused** at input (127.0.0.1, 127.1, 0x7f.1, 169.254.169.254, 10.x, 192.168.x, `.internal`
  names...), after DNS and on every redirect, with a free row.
- Failed feeds stay free (not found, errors, timeouts, refused addresses, too large).
- `runtime.py`, `monitor.py` and `tests/test_runtime.py` copied unchanged from the template.
- **24-hour cap per Apify account:** the cost guard's $0.02 per-run allowance now comes out of $0.20 of unpaid compute
  per account per 24 hours, across runs (template `AllowanceLedger`, one record in the fixed store
  `rss-feed-monitor-limits`), so runs started back to back cannot add up to a large unpaid bill. A run stopped by it says
  so in plain words in its free `skipped` row and status message ("this Apify account has used today's allowance...;
  it resets within 24 hours"). If the record cannot be read, the run goes ahead.

### 0.2.1 (2026-10-30), first published version

0.2.1 is the first version published on the Store; 0.1 and 0.2 below were internal builds and never released, so no
memory saved by them exists anywhere and nothing reads it.

Removed:

- **Google News.** The search mode (`googleNewsQueries`, `googleNewsLanguage`, `googleNewsCountry`), the cross-search de-duplication
  and the Google-specific rate-limit handling are gone, and the separate Google News Monitor listing is withdrawn. Google's terms allow
  its news feeds for personal, non-commercial use only ("You may not copy, modify, distribute, sell, or lease any part of our services"),
  so a paid Actor must not read them (docs/research/2026-10-30-terms-audit.md). A news.google.com link, pasted as a feed or a page, now
  gives a free `invalid_input` row explaining why; a page that links or redirects to Google News is not followed there. `feeds` is
  required again.

Fixes from the second and third pre-publish reviews (regression tests in `tests/test_review_regressions_2.py` and `_3.py`):

- Monitoring memory is kept per feed (its normalised URL plus the memory scope), not per input. A website entered with its own feed
  URL no longer re-sends the whole feed when the feed URL fails for one run. Each input is registered in rank order before its first
  memory read, so a slow read can never let a website and its own feed URL both read and charge the feed.
- Each website or page input remembers the feed it led to. When its page links no feed on a run (a maintenance page answering 200),
  that feed is read again instead of guessing `/feed` and sending a different feed as new; if that feed is gone, the row is
  `not_found`. Whenever an input reads another feed than last time (the page links another one, its first feed was missing for a
  run, a feed URL redirects), the last feed's memory is merged in, so items with the same ids are not sent again, also when it
  goes back to an earlier feed.
- `maxItemsPerFeed` is no longer part of the memory scope: lowering it re-sent everything, and raising it re-sent everything or,
  with a quiet start, lost new items. The memory now stores its cap; after a raise, unknown items ranked at or beyond the old cap
  and dated before the oldest item of the last read are recorded without being sent. Items published since then are delivered.
- Items sharing one guid (deal-of-the-day and chart feeds reuse one): the item holding the bare guid keeps its link's identity in
  memory, and only that item holds the guid again. So an item does not come back when it leaves and returns or when its title
  changes, and a new item reusing a guid, alone or beside others, is delivered (0.2 and earlier missed it). Identity ignores
  tracking tags and session ids; a page that moves to another address but keeps its guid is sent once more.
- Live-blog posts whose links carry only `utm_` parameters keep their own ids instead of collapsing into one (BBC `at_medium`
  links still collapse).
- A website whose linked feeds all return 404 gives a `not_found` row instead of crashing with a TypeError.
- A duplicate input (a website and its own feed URL) is no longer an `error` row counted among "input(s) had problems" on every run:
  it is counted as `duplicateInputs` in `RUN_SUMMARY`, with no dataset row.

### 0.2 (unreleased, 2026-10-30)

Fixes from the pre-publish review (each has a regression test in `tests/test_review_regressions.py`):

- Monitoring memory now keeps an item until 30 days after it was last in the feed (template `lastSeen` fix), so long-listed items no longer come back as new after one empty or partial read.
- Discovery never switches feeds: usual locations are tried only for a bare domain whose home page loaded and links no feed; a home page that answers 403 or 503, or a feed URL that answers with a web page (maintenance), is an error row.
- A feed reached by two inputs belongs to the same input every run (feed URL over website, then input order), whatever the timing or redirects.
- BBC-style `#N` counters are dropped per entry (clean URL plus counter with a tracking link), never depending on the other entries of the read; live-blog anchors stay distinct; items sharing one guid but linking to different pages are told apart by their links.
- With "Also report updated items", feeds that stamp every item with the same publish date are not reported as updated.
- Keyword filters: an image-only description falls back to the full content; a straight apostrophe matches curly ones.
- Robustness: Windows-1252 bytes in UTF-8 feeds, undeclared namespace prefixes and a malformed URL in one item no longer fail the whole feed; Latin-1 feeds read curly quotes correctly; Atom without a namespace is read; a feed whose entries cannot be read is an error, not an empty list.
- Dates: `+02:00` offsets, 12-hour `PM` times and more zone names (SAST, EAT, PKT, ...); unknown zone names are kept as written instead of read as UTC.
- Ids from links ignore `utm_` parameters and http versus https; an author's photo is never the item image; the max items per feed is part of the monitoring memory's scope.

New:

- (Removed in 0.2.1.) Google News searches (`googleNewsQueries`, `googleNewsLanguage`, `googleNewsCountry`): each search is one feed; rows get `publisher` and `publisherUrl`; the same story found by several searches comes once per run and is never sent again later by another search; at most two requests at a time to Google, with retries and free error rows when it rate-limits. news.google.com search and topic links pasted as feeds are read as their RSS. The same engine is listed separately as Google News Monitor (`actors/google-news-monitor`).
- `publisher` and `publisherUrl` for any RSS feed that names a `<source>`.

### 0.1 (unreleased, 2026-10-29)

- First release: reads RSS 2.0 / 0.9x, RSS 1.0 (RDF), Atom 1.0 / 0.3 and JSON Feed 1.x; finds a website's feed from its `<link rel="alternate">` tags, then `/feed`, `/rss.xml`, `/feed.xml`, `/atom.xml` and `/index.xml`.
- Monitoring of new items (and, optionally, updated titles or dates) with a quiet first run; feeds are treated as rolling windows, so nothing is reported removed and items are remembered for 30 days.
- Whole-word keyword and exclusion filters on title, description and categories; published-within-days filter; newest-N cap per feed; optional plain-text description.
- Stable item ids for feeds whose guids carry a placement counter (seen live on BBC feeds), so one story is one row.
- Safe billing and robustness from the Studio template: feed checks charged only after rows are delivered, free problem rows, duplicates read once, streaming XML with size caps, gzip, odd encodings, entity rejection and repairs for common feed mistakes. Rows carry a one-line `summary`; the status message lists the first new items for Slack alerts.
