# Changelog of Standvirtual Portugal Car Scraper (`devilscrapes/standvirtual-portugal-cars`) Actor

- **URL**: https://apify.com/devilscrapes/standvirtual-portugal-cars/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/standvirtual-portugal-cars.md

## Standvirtual Portugal Car Scraper — Changelog

### 0.3.0 — 2026-09-02

- Fix: 44% customer FAILED rate (24/54 30-day runs, live build 0.2.1) traced
  to a second, independent bug beyond the 0.2.0 proxy-session fix —
  `_fetch_search_page` collapsed "genuinely blocked" and "search parsed fine
  but matched zero listings" into the same empty result, and `_iter_listings`
  raised `RuntimeError("possible block")` on page 1 for both. A customer
  applying a narrow `searchUrl` filter (specific make/model/price band with
  no current matches) got a hard FAILED run instead of a SUCCEEDED run with
  zero rows — the exact anti-pattern in `ops/os/EMPTY-IS-NOT-A-FAILURE-2026-08-19.md`,
  which this Actor's bespoke error string wasn't covered by.
- Fix: `_fetch_search_page` now returns `None` only for genuinely unreadable
  pages (retries exhausted, or no parseable `__NEXT_DATA__`), and uses the
  payload's own `totalCount` to confirm a real empty search vs. an anomalous
  "claims results but zero edges" page (still treated as a block signal).
  `_iter_listings` raises loud only on `None`; a confirmed-empty page 1 now
  finishes the run normally with zero rows.
- Added `tests/test_empty_search_vs_block.py` — regression test that fails
  on the old code and passes on the fix; also locks in that unreadable pages
  and inconsistent totalCount/edges pages still fail loud.

### 0.2.0 — 2026-08-26

- Fix: 51% customer success rate (23/51 30-day FAILED + 2 TIMED-OUT) traced to
  a sticky-proxy-session bug. Apify Proxy pins one exit IP per `session_id`;
  the Actor resolved a single proxy URL (one session\_id) once at startup and
  reused it across every block retry, so a flagged exit failed all 5 retries
  identically and the run raised on page 1 ("possible block"). Reproduced
  live (run `aj7e8X1AB00HBi2im`): HTTP 403 on all 4 logged attempts despite
  "session rotation" that only rotated the curl-cffi TLS fingerprint, not the
  proxy exit. Fix: `proxy_url_factory` mints a genuinely new `session_id` —
  and therefore exit IP — on every retry, in both `main.py` (proxy
  resolution) and `scraper.py` (the retry loop).
- Hardening: pinned `apifyProxyCountry: "PT"` in the default
  `proxyConfiguration` (model + input schema). A Portugal-only classifieds
  site is more likely to flag exits from outside PT; empirically 0/2 blocked
  vs 1/3 blocked on random-country residential exits during reproduction.
- Added `tests/test_proxy_rotation.py` — regression test that fails on the
  old single-proxy-url code path (`_new_session` was synchronous and never
  re-consulted a factory) and passes once every retry mints a distinct exit.
- Fix: `actor-start` was charged before input validation ran. `scraper.run()`
  is an async generator, so `ActorInput.model_validate()` only executed
  lazily on the first `__anext__()` — after `main()` had already billed the
  customer. Input is now validated synchronously in `main()` before any
  charge fires.
- Added a regression test asserting the proxy `session_id` template matches
  Apify's `^[\w._~]+$` regex, is unique per call, and that the proxy-url
  factory mints a fresh one on every invocation.
- Published as build 0.2.1, QA PASS (5/5 rows, `actor-start`×1 +
  `result-row`×5 charged correctly, no 403/block on the search page).

### 0.1.0 — 2026-06-05

- First publish to Apify Store (EU car-scraper fleet rollout).
- RESIDENTIAL proxy defeated DataDome on standvirtual.com (PT) — 5/5 QA rows.
- curl-cffi browser impersonation, Pydantic v2 models, PPE (actor-start + result-row).
- Initial scaffolded release (2026-05-15).
