# Changelog of AliExpress Scraper — Products & Prices (`devilscrapes/aliexpress-products-scraper`) Actor

- **URL**: https://apify.com/devilscrapes/aliexpress-products-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/aliexpress-products-scraper.md

## AliExpress Products Scraper -- Changelog

### Unreleased -- 2026-09-01

Un-shelve verification pass. Build 0.1.2 (cloud QA run `YSBcad4xebfIygDll`,
5 rows) was produced from a commit that had never been fast-forwarded onto
`main` -- this release lands that source and adds one hardening change on
top of it:

- **`ResultRow` delivery assertion**: `product_id` must be numeric (8-16
  digits), `price` must be `> 0` (was `>= 0`), `title` must be at least 3
  chars, and `product_url` must embed the row's own `product_id`. This is
  the guard the `ziprecruiter-jobs-scraper` fabrication incident
  (2026-08-30 -- a SUCCEEDED run with 3 rows built from nav links and the
  search page's own URL) proved this fleet needs on every Actor: a
  blocked-but-200 page can no longer produce a row that passes.
- Independently verified 3/5 dataset rows from `YSBcad4xebfIygDll` against
  live AliExpress product pages (og:title + og:image byte-for-byte match);
  all three are real, currently-listed products.

### 0.1.0 -- 2026-08-31

First genuinely working release. The scaffold shipped 2026-08-02 was never
functional (`window.runParams` was the wrong variable on both surfaces --
see notes.md for the full recon trail) and every previous cloud QA attempt
returned 0 real product rows.

- **Rewrote `parser.py` against the wire format confirmed live 2026-08-31**
  (local curl-cffi + Apify RESIDENTIAL(US) probe for search, cloud Camoufox
  render for detail):
  - Search results live in `window._dida_config_._init_data_`, not
    `window.runParams` -- a plain curl-cffi fetch over RESIDENTIAL gets the
    full result list (title/price/currency/rating/order-count-hint/images)
    with no browser needed.
  - Detail pages are architecturally client-side-rendered (confirmed with
    zero exceptions across every product ID tried, any proxy tier); real
    data only appears in an `application/ld+json` schema.org Product block
    after a Camoufox render, never in a window variable.
- **`search_keywords` rows no longer need a detail-page fetch at all** --
  built directly from the search page's own fields. This mode stays on the
  cheap curl-cffi path end to end.
- **`product_urls`/`product_ids` rows now go through a real Camoufox
  render** (new `src/detail_browser.py`) since the CSR shell never carries
  data over plain HTTP. One browser per run, opened lazily only when these
  modes are used, so search-only runs never pay for it.
- **Proxy now defaults to RESIDENTIAL pinned to `shippingDestinationCountry`**
  (previously unset) -- AliExpress returns a plausible-looking wrong-locale
  page from a mismatched exit (HTTP 200, not an error), and datacenter IPs
  hit a JS-redirect challenge on the search surface. Added a currency-
  mismatch guard that reports `geo-mismatch` instead of ingesting corrupted
  rows.
- `order_count`/`seller_name`/`category`/`variants` are honestly `null`/`[]`
  for `product_urls`/`product_ids` rows -- no live-confirmed source carries
  them yet (the ld+json block doesn't). `shipping_cost_estimate` stays
  `null` for every row -- the guessed freight endpoint 404s (confirmed
  again 2026-08-31); a real endpoint was never found.
- Regenerated `uv.lock` -- `camoufox`/`playwright` were added to
  `pyproject.toml` on 2026-08-26 but the lock was never refreshed, so every
  Camoufox-dependent build silently relied on stale Docker layer cache.
- Corrected `.actor/pay_per_event.json`'s `actor-start` price to the fleet
  standard $0.05 flat (was $0.005).
- **Fixed a REQ-10 zero-rows bug found in the pre-publish gate check**:
  `_finish()` used to fail the whole run whenever total rows was zero,
  including a keyword that legitimately matched nothing on AliExpress --
  the exact anti-pattern that got `reverb-sold-listings` delisted
  (DEVILSCRAPES-CONTEXT.md "an empty result is not a failure"). Now only
  fails loud when every single input is genuinely unresolved (fetch/render
  failure, geo-pin mismatch, bad URL) -- a "no-results" search SUCCEEDS
  with zero rows and an honest status message.
- Typed `main.py`'s internal session/detail-browser parameters against
  narrow `Protocol`s (`SearchPageFetcher`/`ShippingCostFetcher`/
  `SearchSession`/`DetailFetcher`) instead of the concrete
  `AliExpressSession`/`DetailBrowser` classes, fixing 11 `pyright` errors
  where `tests/test_main.py`'s plain stub doubles weren't structurally
  assignable to the concrete types.
- Added `tests/test_smoke.py` (`-m smoke`, opt-in, skips-not-fails on
  network/anti-bot noise) covering both live fetch paths -- the publish
  gate requires a smoke layer and none existed for this Actor.
