# Changelog of Review & Rating Monitor (`al_mansouri/review-rating-monitor`) Actor

- **URL**: https://apify.com/al_mansouri/review-rating-monitor/changelog.md
- **Full Actor documentation**: https://apify.com/al_mansouri/review-rating-monitor.md

## Changelog

All notable changes to the Review & Rating Monitor are recorded here.

The record shape, the error codes, and the limits quoted in the README are a published contract.
Adding, renaming, or restatusing any of them is a breaking change and must reach the README, the
dataset schema, and this file together.

### \[0.1.0] — unreleased

First working version. Free; charge events are declared but no prices are set.

#### Added

- Per-location review and rating monitoring for Google Business listings, publishing a row only
  when the reputation actually moved.
- **Star-level change detection without reading review text.** A listing publishes how many
  reviews sit at each star rating; subtracting the last check's breakdown from this one says how
  many arrived *and at what rating*, from a single page read. A row can say "two new reviews, one
  of them 1-star" without ever opening a review.
- `negativeAdded` and a `negative-review` change kind, so a dataset filters down to the rows that
  need answering today without parsing anything.
- A per-location baseline in a **named** key-value store on the customer's own account, holding
  only the rating, the count and the breakdown — never review text.
- Charge events `location-checked` and `reviews-changed`, declared but not priced.

#### Decisions worth knowing

- **One browser for the whole run.** Locations are read sequentially inside a single
  `MapsSession`. The launch is the expensive part of a Chromium check, so a session per location
  would make a scheduled monitor uneconomic — this is what makes a long watch list affordable.
- **The baseline is advanced only after the row is published.** Advancing first and failing to
  publish loses an alert silently and forever; publishing first and failing to save repeats one,
  which is visible and chaseable. Same order, and same reasoning, as the other two monitors here.
- **A first sighting is charged as a check, not as a change.** Billing it at the alert price would
  make adding a location to a watch list feel like a trap.
- **Three stars is not a complaint.** It is what people leave when a place was fine, and alerting
  on it would make the alerts worth ignoring. `negativeAdded` counts one and two stars only.
- **An empty read is a failure, not "no change".** `maps_harvest.fetch_detail` returns empty
  findings rather than raising when the panel never renders — right for a harvester, dangerous
  here, because empty findings look exactly like a location with no reviews. A reading with
  neither a count nor a breakdown is reported as `LOCATION_UNREADABLE`, so a monitor that has
  silently stopped seeing the listing says so instead of reporting calm forever.
- **No business name on the row.** `DetailFindings` carries the address but not the title, so a
  `name` field would always be null. Publishing one and letting a customer build a report around
  it before discovering it never fills in is worse than omitting it.

#### Fixed

- **A run whose baseline store cannot be opened no longer dies.** It used to call `Actor.fail()`,
  so a customer — or Apify's automated quality test, which runs this Actor on *their* account —
  saw a failed run with no output at all. That is what flagged the Website Change Monitor under
  maintenance on 2026-08-22 and the App Store Review Monitor on 2026-08-28, and this Actor's public
  runs show the same signature. It now falls back to the run's own key-value store, publishes every
  location as a first sighting, puts the new `STATE_STORE_UNAVAILABLE` warning on every row, and says
  so in the run's status message. It never claims a change. `STATE_UNAVAILABLE` is still raised
  when the named store *and* the run's own store both refuse.

- **A first run now produces rows.** With `onlyReportChanges` on — the default — a first sighting
  was treated as "nothing changed" and nothing was pushed, so a customer's first run ended with an
  empty dataset. That contradicted this Actor's README ("every location is a `baseline` row") and its
  charge tiering, which prices a baseline as a check because "the customer got a starting point out
  of it". The Store's daily auto-test is the sharper consequence: it flags an Actor whose default
  input yields an empty dataset three days running as under maintenance. A first sighting is now
  published and charged as a check; a genuinely quiet check still publishes nothing and still
  advances the baseline.

- **The alert charge tier could never fire.** `event_for_record()` compared `record.event` to the
  enum member with `is`. `PublicModel` sets `use_enum_values=True`, so an enum field passed
  explicitly to the constructor holds the plain string — and the runner always passes `event`
  explicitly — making the comparison false for every row this Actor can produce. A real reputation move was
  therefore charged at the cheap check tier. No customer was affected, because this Actor is free
  and no prices are set; pricing it without this fix would have set a tier that silently earned
  nothing, which is exactly the failure that importing the event names into
  `scripts/set_pricing.py` is meant to prevent and cannot see.

- Hardened the same comparison in `runner.py`, which tested `published.status` against
  `ManifestStatus.FAILED` with `is not`. That one was **not** live: the failure manifests are built
  with `model_copy`, which skips validation and so keeps the enum member, and the check behaved
  correctly. It was one refactor away from not doing — any path that re-validates the record yields
  the string instead — and a baseline that advances after a failed publish loses the change
  permanently.

- Added `tests/test_storage.py`, which drives `event_for_record()` with real rows rather than
  asserting the `CHARGED_EVENTS` tuple, and `tests/test_runner_ordering.py`, which pins the
  publish-then-advance ordering against both representations. Asserting the tuple is what let this
  survive: it proves the two names exist, not that either is reachable.

#### Known gaps

- No review text, by design. The star breakdown is the part that can be read cheaply enough to run
  on a schedule.
- Rename detection is not implemented: only the permanent-closure flag is reliable across reads.
- Every check needs a browser, so this Actor costs more per run than the HTTP-only monitors here.
