All notable changes to this actor are documented here.
[0.3] - 2026-08-30
Fixed — text field returned generic page boilerplate instead of the real post caption
Root cause (confirmed live, via temporary debug instrumentation deployed and run on
the actual actor):_FB_MESSAGE_TEXT_PATTERN ("(?:message|text)"\s*:\s*"...") matches
the first bare "text":"..." (or "message":"...") JSON key it finds anywhere in the
entire page bootstrap payload — which contains many unrelated UI-microcopy strings keyed
"text" before the actual post content ever appears. Captured, on real live posts, the
full ordered list of candidates this pattern matches; the first one satisfying the old
filter (length > 10, doesn't start with "facebook") was consistently one of: a
"text":"See more about <PageName>" UI prompt, a follower/following count (
"2.8M followers"
), the Page's own bio/about text (identical to its og:description), or a
"Page · <category>" label — never the post's caption. Example observed live: for a
real post whose actual caption began "JOÃO PESSOA CONHECEU OS CAMPEÕES! 🏆🏀 …", the
actor's text field contained only "See more about Basquete Brasil - CBB".
The real caption is present on the same page in Facebook's structured message object,
"message":{"text":"<real caption>"}, which the old pattern's "stop at the first
match" loop never reached. A second, independent bug in the same code path: the manual
unescaping (.replace("\\n","\n").replace('\\"', '"')) does not decode \uXXXX
escapes or UTF-16 surrogate pairs, so any caption containing an emoji (extremely common)
would have come through with literal garbled \ud83c\udfc6-style sequences even once
pointed at the right field.
Fix (src/main.py): added _FB_MESSAGE_OBJECT_TEXT_PATTERN (matches the structured
"message":{"text":"..."} shape) and tries it first; the old loose pattern is now only a
fallback for the rare page layout where that structured object isn't present, and is
additionally filtered through a new _GENERIC_TEXT_BOILERPLATE check so it can no longer
surface a "See more about", follower/following count, or "Page · category" string as if
it were the caption. Added _decode_json_string_fragment(), which properly JSON-decodes
the captured string (handling \uXXXX/surrogate-pair emoji correctly) instead of the old
two-.replace() approach, and switched both the new and fallback extraction to use it.
Verification: live-ran the fix (build with temporary debug fields first, to capture
real evidence of which JSON shape holds the caption; then the actual fix, redeployed and
re-run). Before: "text": "See more about Basquete Brasil - CBB". After the fix,
re-verified live with a fresh minimal run — text now contains the real, full post
caption with correctly-decoded emoji and line breaks (see the live verification note in
the project report for the exact before/after row).
[0.2] - 2026-08-30
Fixed — 0 posts discovered/saved on every run
Root cause (confirmed live): the actor discovers candidate Facebook posts by
scraping Google's site:facebook.com search results through Apify's GOOGLE_SERP
proxy group, then parsing the returned SERP HTML for outbound links. That parsing
(_parse_facebook_urls_from_serp_html in src/main.py) matched only the old Google
redirect format — <a href="/url?q=<real_url>..."> — via _SERP_REDIRECT_PATTERN
(/url\?q=) and a generic <a href> scan.
Google no longer emits that format. Live-tested by fetching a real SERP page through
the account's actual GOOGLE_SERP proxy (football site:facebook.com): every
organic result link is now wrapped behind an opaque, undecodable redirect —
href="/goto?url=CAESXAHrOzAV..."> — where the url= value is an encrypted
protobuf-style token, not a URL. Confirmed there is no longer a single /url?q=
occurrence, and no plain <a href="https://..."> to any external site, anywhere in
the returned page. Also confirmed the token cannot be resolved another way: requesting
Google's /goto (or /url) endpoint directly through the GOOGLE_SERP proxy returns
HTTP 400 "Unsupported Google service or resource" — that proxy group only forwards
/search. Because of this, _parse_facebook_urls_from_serp_html matched zero URLs on
every single query, _fetch_posts never had a URL to fetch, and the actor produced an
empty dataset on every run regardless of searchQueries, maxPosts, or proxy settings.
Fix (src/main.py): the real destination URL turns out to still be present as
plain, unwrapped text elsewhere on the same SERP page — Google emits an adjacent
embedded JS data array (used for its own client-side hover-preview-card hydration)
shaped like
. _parse_facebook_urls_from_serp_html was rewritten to extract the
URL from that structure (via the already-present-but-unused _FACEBOOK_URL_PATTERN)
instead of from <a href> markup. The dead _SERP_REDIRECT_PATTERN, _HREF_PATTERN,
and _extract_url_from_serp_href were removed along with the now-unused unquote
import.
Verification: fetched real, multi-page SERP HTML for football site:facebook.com
through the account's actual GOOGLE_SERP proxy session and ran it through the new
parser: 12 distinct, real facebook.com page/post/video URLs extracted from page 1
(facebook.com/NFL/, facebook.com/PumaFootball/, facebook.com/SkySportsFootball/,
etc.), 8 more distinct URLs from page 2 (start=10) — pagination continues to work.
No /login, /help, or /policy junk URLs in the output.
Hardened — Facebook login-wall/block responses silently produced near-empty rows
While live-testing the fix above, fetching a discovered post/Page URL (phase 2, via a
shared Apify datacenter proxy session) was observed to be inconsistent: the identical
URL with identical headers returned real page content on one attempt and, on the very
next attempt, either a redirect to /login or Facebook's small generic "Sorry,
something went wrong" error page — ordinary per-request IP-reputation noise on a
shared proxy pool, not a permanent block on the URL itself. The existing code treated
any HTTP-200 response (including a login-wall/error page) as success, so a blocked
fetch would still get pushed to the dataset as a charged row with almost every field
null/empty.
Fix (src/main.py): added _looks_blocked_or_login() — a real post/Page document
is always tens of KB and references fbcdn.net; Facebook's login-wall/error responses
are a few KB of boilerplate with neither. Added _fetch_post_html(), which retries a
blocked fetch on a freshly-rotated Apify proxy session (up to 3 attempts total) before
giving up. _fetch_posts now uses this helper and skips (logs a warning, does not
push or charge) a URL that is still blocked after retries, instead of silently saving
a near-empty row. Proxy rotation is skipped (matching prior behavior) when the user
explicitly disabled proxyConfiguration.useApifyProxy.
[0.1] - Initial version
Search Facebook by keyword via Google site:facebook.com discovery (GOOGLE_SERP
proxy group) and extract post text, engagement counts, media, and page identity.
postTimeRange recency filter enforced before a row is saved/charged.
Optional per-post top-level comment fetch to an uncharged child dataset
(maxCommentsPerPost).
Optional page contact/lead enrichment (enableContactLookup): email/phone/website/
address from each post's own Page, never fabricated.
targetCountry / targetLanguage geo/language biasing of the discovery search.