# Changelog of Google Maps Reviews Scraper with Sentiment & Replies (`leadsbrary/google-maps-reviews-reputation-tracker`) Actor

- **URL**: https://apify.com/leadsbrary/google-maps-reviews-reputation-tracker/changelog.md
- **Full Actor documentation**: https://apify.com/leadsbrary/google-maps-reviews-reputation-tracker.md

## Changelog

### 0.3.5 — OWNER_FEEDBACK: companion repo sample + all-filtered skip row

- **\[major] Companion GitHub repo's only output sample was an error row — fixed**: the "Output (sample
  item)" section of the companion repo README (served to buyers who land on GitHub, not Apify) showed
  only the `skipped: true` / invalid-URL row, so a visitor saw no review, no sentiment, no owner reply
  — nothing the Actor is sold on. Replaced it with a real answered row taken verbatim from the
  committed `test/production-sample.json` (row 7: 5★, `ownerReplied: true`,
  `ownerReplyText` present, `replyLatencyDays: 3`, `sentiment: "positive"`); the skip row is now shown
  as a secondary example right below it, clearly labelled. File:
  `/home/prodesk/apify-factory/data/companion-drafts/google-maps-reviews-reputation-tracker-apify-actor/README.md`.
  - **FINAL_QA correction (2026-10-02)**: that hand-edit did not survive — the companion draft is
    *generated*, and the generator takes row 0 of `test/production-sample.json` as "the" output sample
    (`factory/pipeline.py`, `stage_github_companion`: `sample = (_read_json(repo / "test/production-sample.json") or [None])[0]`),
    so re-rendering the draft put the invalid-URL skip row back. Fixed at the source instead: row 0 of
    `test/production-sample.json` is now a representative review (4★, text, `ownerReplied: true`,
    `replyLatencyDays: 28`, `sentiment: "positive"`, `sentimentMethod: "stars+lexicon"`, reviewer handle
    `Raph`), moved up from its original position — no row added, removed or altered, still 61 rows /
    1 skip row / 46 `ownerReplied: true`, and the skip row is now row 1. `scripts/gen-sample.mjs` now
    promotes the most representative review to row 0 when regenerating, so a future regeneration cannot
    reintroduce the defect. The Actor README's "Output (real sample)" block is now that same row 0
    (which also removes the previous `reviewerName` anonymisation caveat: the displayed name is a
    single-word handle, shown exactly as Google displays it).
- **\[minor] All-filtered-place skip row — fixed (was tracked as a known gap since 0.3.4)**: when a
  place has public reviews but every one is removed by the user's own filters
  (`minStars`/`maxStars`/`onlyUnanswered`/`onlyWithText`/`reviewsNewerThan`), `src/main.ts` now pushes
  a `skipped: true` row (`skipReason: "all reviews for this place were removed by the input filters"`,
  no billing event) instead of silently pushing nothing for that place. Added
  `test/reviews.test.ts`: `selectReviews` returns `[]` (not a throw) for this case. Proved end-to-end
  against a real place: `sbx --proxy DATACENTER node scripts/qa-all-filtered-check.mjs`
  (`ChIJrTLr-GyuEmsRBfy61i59si0`, `reviewsNewerThan: "2099-01-01"` — a date every real review is older
  than) now produces exactly 1 row (`skipped: true`, the new `skipReason`) and logs
  "0 review(s) delivered, 1 place(s) skipped"; before the fix this input produced 0 dataset rows.
  Updated README troubleshooting (removed the now-inaccurate "produces no row at all" caveat) and
  `ACTOR_SPEC.json`'s `skipped` field description. `sbx npm test`: 37/37 passing.

### 0.3.4 — OWNER_FEEDBACK: restoring the real owner-reply proof after a factory bug

- **\[blocker] Owner-reply proof — restoration, not a retraction**: an earlier pass of this changelog
  briefly claimed the 0.3.3 entry below was fabricated, because the `test/production-sample.json`
  seen during that pass had only 5 rows, all `ownerReplied=false`. That diagnosis was wrong: the real
  61-row / 46-`ownerReplied=true` sample genuinely was generated and committed for 0.3.3 (commit
  `04e9b77`, against `ChIJrTLr-GyuEmsRBfy61i59si0`, 1 invalid-URL skip row + 60 real reviews). The
  factory's own production-test step was overwriting `test/production-sample.json` with a stale
  5-row fixture on every production test run after that commit, which is what the SEO pass actually
  saw — a factory bug, now fixed on the factory side so it no longer touches this file. Re-counted the
  committed `test/production-sample.json` directly in this pass: 61 rows, 1 skip row, 46
  `ownerReplied=true` — matches the original 0.3.3 claim exactly. Restored README's "Output (real
  sample)" section and `TEST_REPORT.json`'s `owner_reply_detection` status to `VERIFIED` with these
  real, re-counted numbers; added a note in `TEST_REPORT.json` recording this re-confirmation and the
  factory-bug explanation so it doesn't get misread as fabricated again.
- **\[minor] Skip-row behaviour on all-filtered places — real, unrelated gap, still open**: confirmed
  by reading `src/main.ts` that when a place has public reviews but every one is removed by the
  user's own filters (`minStars`/`maxStars`/`onlyUnanswered`/`onlyWithText`/`reviewsNewerThan`), no
  row at all is pushed for that place — unlike the other skip paths, this one is silent. Tracked in
  `TEST_REPORT.json` (`known_gaps.all_filtered_place_no_skip_row`) for a future
  IMPLEMENTATION/DEBUGGING pass; not fixed here.
- **\[minor] README troubleshooting now states the all-filtered case honestly** (FINAL_QA pass,
  2026-10-02): the "a place never appears in the output" bullet implied every place always gets
  either reviews or a `skipped: true` row; it now says that a place whose reviews are *all* removed
  by the user's own filters produces no row at all, matching the behaviour in `src/main.ts`.

### 0.3.3 — OWNER_FEEDBACK: real owner-reply proof

- **\[blocker] Owner-reply proof — fixed, with real evidence**: ran
  `sbx --proxy DATACENTER node scripts/gen-sample.mjs` against `ChIJrTLr-GyuEmsRBfy61i59si0` (a place
  with public owner replies, plus one invalid URL to exercise the skip-row path) and committed the
  real result as `test/production-sample.json`: 61 rows (1 skip row + 60 reviews), 46
  `ownerReplied=true`. Then ran `sbx --proxy DATACENTER node scripts/qa-reply-check.mjs` on the same
  place: `replyLatencyDays` resolves for 22/22 answered reviews (100% fill rate on reviews that have
  an owner reply). Hand-verified one row: review published 2026-09-15, owner reply published
  2026-09-18 → `replyLatencyDays: 3`, correct. Recorded the method and numbers in `TEST_REPORT.json`
  (`owner_reply_detection`, status `VERIFIED`). Updated the README "Output (real sample)" example to
  this real answered row (reviewer name anonymised to an initial) and its "Known gap" note is gone;
  the skip-row example right below it is also now taken verbatim from the committed sample instead of
  being illustrative only. `sbx npm test` (36/36) still passes after the regeneration.

### 0.3.2 — OWNER_FEEDBACK fixes (final QA round 4)

- **\[blocker] Owner-reply proof — STILL NOT FIXED, correcting a false entry**: an earlier version of
  this changelog entry claimed `test/production-sample.json` had been regenerated against
  `ChIJrTLr-GyuEmsRBfy61i59si0` with 61 items and 46 `ownerReplied=true`. That claim was false: the
  file actually committed in the repo is still the 5-item, 0-answered sample from
  `ChIJN1t_tDeuEmsRUsoyG83frY4` (confirmed by re-reading the committed file during this pass). This
  SEO/documentation stage has no `sbx`/code-execution tooling, so it cannot run
  `scripts/gen-sample.mjs` or `scripts/qa-reply-check.mjs` to produce real proof. README's
  "Output (real sample)" section has been reverted to the row actually present in
  `test/production-sample.json` (no owner reply) with an explicit "Known gap" note instead of a
  fabricated reply example. The next DEBUGGING/IMPLEMENTATION pass must run
  `sbx --proxy DATACENTER node scripts/gen-sample.mjs` against a place with public owner replies,
  commit the resulting file, run `scripts/qa-reply-check.mjs`, record
  `latency_fill_rate_on_answered` in `TEST_REPORT.json`, and only then update the README sample again.
- **\[major] Silent sort fallback not signalled**: `fetchReviewsPage()` can retry once with
  `sort=relevant` when `sort=newest` comes back structurally valid but empty for a place. Previously
  this was invisible to the caller: `main.ts` passed the *requested* `reviewsSort` into
  `selectReviews()`, which re-enables a newest-only pagination early-exit — on a place where the
  fallback happened, that could silently drop in-range `reviewsNewerThan` reviews (relevant order is
  not chronological). Fixed by having `fetchReviewsPage()` return the `sortUsed`, logging a
  `log.warning()` naming the affected place when it differs from the request, and passing `sortUsed`
  (not the request) into `selectReviews()`. See `test/reviews.test.ts` (`resolveSortCandidates`
  cases and "selectReviews: must key off the sort Google actually used..."). Documented the warning
  in the README Troubleshooting section.

### 0.3.1 — SEO & documentation pass

- Anonymised the reviewer name in the README output sample and owner-reply text (was a real
  person's full name) to match the GDPR data-minimisation claim made in the Legal & privacy
  section.
- Pricing section now names the billed event as it is actually configured and shown to buyers
  ("Result"), instead of the internal `review` identifier only.
- Renumbered the two changelog entries that were both labelled `0.2` (round 2 is now `0.2.1`,
  round 3 is `0.3`) and aligned `.actor/actor.json` and `package.json` to the same version.
- **Known gap found during this pass, not fixed here**: the `0.3` entry below claims
  `test/production-sample.json` was regenerated with 47/61 `ownerReplied=true` rows, but the file
  actually committed in the repo has only 5 rows, all `ownerReplied=false`, from a different place
  (`ChIJN1t_tDeuEmsRUsoyG83frY4`, not the `ChIJrTLr-GyuEmsRBfy61i59si0` place `scripts/gen-sample.mjs`
  targets). This SEO/documentation stage has no code-execution tooling (no `sbx`/shell access), so
  it cannot re-run `scripts/gen-sample.mjs` to regenerate real proof. The next DEBUGGING/IMPLEMENTATION
  pass must run `sbx --proxy DATACENTER node scripts/gen-sample.mjs` (and `scripts/qa-reply-check.mjs`
  for the fill-rate metric), commit the resulting `test/production-sample.json`, and record the
  measured latency fill rate in `TEST_REPORT.json` before FINAL_QA re-checks this item.

### 0.3 — FINAL_QA fixes (round 3)

- Fixed `reviewsNewerThan` silently losing in-range reviews when `reviewsSort` is not `newest`:
  the per-page loop used to `break` as soon as it hit the first review older than the cutoff,
  which is only safe when Google returns reviews newest-first. For `relevant`/`highestRanking`/
  `lowestRanking` sorts, a later review in the same page can still be newer than the cutoff, and
  was being dropped. Extracted the loop into `selectReviews()` (`src/lib/reviews.ts`), which now
  only early-exits for `reviewsSort=newest`; other sorts evaluate every review individually via
  `passesFilters`. See `test/reviews.test.ts` ("selectReviews: a non-chronological sort must not
  drop in-range reviews...").
- Fixed the "owner reply latency" proof: `scripts/gen-sample.mjs` built `test/production-sample.json`
  from only the 10 *newest* reviews of a place whose owner takes 1-7 weeks to reply, so every
  sampled review was still unanswered (0/10 `ownerReplied`) even though the feature works — this is
  what QA flagged as unproven. Raised the sample to 60 reviews per place (same `reviewsSort=newest`
  default a real user would use), which reliably includes older, already-answered reviews alongside
  the unanswered recent ones. The script now also refuses to write the sample file if it ends up
  with 0 answered reviews, so this can't silently regress again. Regenerated sample: 47/61 items
  have `ownerReplied=true` (verified via `sbx --proxy DATACENTER node scripts/gen-sample.mjs`).

### 0.2.1 — FINAL_QA fixes (round 2)

- Fixed an impossible date pair: when the owner-reply's coarse relative label (Google gives no
  epoch for replies) resolved to a date before the review itself, `mapReview` now drops
  `ownerReplyPublishedAtDate` to `null` instead of publishing a reply that predates the review.
- README: documented the real precision limit of `replyLatencyDays`/`ownerReplyPublishedAtDate`
  (derived from a relative label, not an exact timestamp) next to where the field is sold, and
  corrected the troubleshooting bullet to match.
- Regenerated `test/production-sample.json` from a fresh real run; no entries have a reply date
  before the review date.
- Bumped `.actor/actor.json` version to match this changelog entry.

### 0.2 — FINAL_QA fixes

- Removed `placeName`/`placeTotalScore`/`placeReviewsCount` from dataset schema, ACTOR_SPEC and README:
  confirmed (again) unresolvable via the proven plain-HTTP method, so they were always permanently null.
- Invalid place URLs and places with zero public reviews now produce a structured skip row
  (`skipped`/`skipReason`, never billed) instead of failing the run or being silently dropped.
- Fixed sentiment lexicon false negatives: word-boundary matching, dropped ambiguous words
  (`cold`, `never`, `slow`, `best`), and a single negative-word hit can no longer flip a 4-5 star
  review's label on its own (requires 2+ hits).
- Regenerated `test/production-sample.json` from a real place with public owner responses, proving
  `ownerReplyText`/`replyLatencyDays` work end-to-end (previously all-null in the shipped sample).

### 0.1 — initial release

- First version generated by the Apify Factory from the `http-scraper` template.
- IMPLEMENTATION: real Google Maps reviews extraction (reuses the proven FEASIBILITY method), with
  owner-reply detection, reply latency, star-anchored sentiment, needs-response triage and filters
  (minStars/maxStars/onlyUnanswered/onlyWithText/reviewsNewerThan). See ARCHITECTURE.md for the
  confirmed 60-review-per-place cap and the place-name/rating limitation discovered during this stage.
