fix: don't flag content-rich pages that reference reCAPTCHA as bot-blocked - #1690
fix: don't flag content-rich pages that reference reCAPTCHA as bot-blocked#1690ABHA61 wants to merge 4 commits into
Conversation
…ocked detectBotBlocker treated any 200 page whose HTML matched the generic CHALLENGE_PATTERNS (including the bare /captcha/i and /recaptcha/i) as a bot challenge. The "protected by reCAPTCHA" disclosure badge and reCAPTCHA form widgets appear on normal content pages, so real customers (westjet.com, mazdausa.com, repsol.com/.es/.pt) were reported as "blocking Adobe LLM Optimizer" (crawlable:false, confidence 0.7) even though the content scraper accessed them fine (scrapeForbidden:false, thousands of URLs scraped with zero forbidden). Gate the generic-pattern check on an interstitial-shape heuristic: a 200 body is only treated as a challenge when it is content-thin. Content-rich pages that merely reference a captcha now return crawlable:true, while thin challenge interstitials (reCAPTCHA, Press-and-Hold, GeeTest, Arkose, etc.) are still detected. CDN-typed blocks (Cloudflare/Imperva/Akamai/ Fastly/CloudFront) and 403/error-path detection are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on the bot-blocker false-positive fix: - Count characters, not space-delimited words, so the content-thin check is not biased against languages without word spaces (CJK/Thai/etc.). A content-rich Japanese/Korean/Chinese homepage with a reCAPTCHA badge was still misclassified as a thin challenge interstitial under word count. - Evaluate the challenge pattern before the (more expensive) content-shape strip, so the strip only runs when a generic pattern actually matched. - Add regression tests: a thin captcha interstitial still blocks, and a content-rich CJK page that references reCAPTCHA does not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This PR will trigger a patch release when merged. |
- Collapse the duplicated inline rationale in the generic 200 branch; the isLikelyInterstitial helper and its JSDoc already explain the intent. - Drop specific customer names from the test comment (keep it generic). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
tkotthakota-adobe
left a comment
There was a problem hiding this comment.
A couple of non-blocking observations from a review pass. Neither is a confirmed defect; sharing for consideration.
| // Still check for generic challenge patterns | ||
| if (htmlHasChallenge(CHALLENGE_PATTERNS.general)) { | ||
| // Only treat a generic challenge match as a block on a content-thin (interstitial) page. | ||
| if (htmlHasChallenge(CHALLENGE_PATTERNS.general) && isLikelyInterstitial(html)) { |
There was a problem hiding this comment.
The && isLikelyInterstitial(html) thinness gate is applied to the entire CHALLENGE_PATTERNS.general set, not just the captcha/recaptcha/hcaptcha badges that caused the false positive. general also includes vendor-specific tokens (datadome, geetest, arkose, funcaptcha, press/click hold). Those vendors sometimes render the challenge inside a content-rich templated shell (nav, footer, cookie banner) that can exceed 200 visible chars — in which case a genuinely-blocked page would now flip to crawlable: true (a false negative). Historically this file has fixed FPs by narrowing the specific offending regex (e.g. #1649 tightening press.*hold) rather than adding a broad gate. Consider scoping the thinness gate to only the captcha-family patterns, or adding a regression test proving the vendor patterns still fire on content-rich pages. Not confirmed in practice (43-domain validation covered the FP direction, not this FN direction).
| /** | ||
| * Maximum visible-text length (in characters) for a 200-OK body to be treated as a | ||
| * challenge interstitial. A bot-challenge page is content-thin — its entire body is the | ||
| * challenge — whereas a real content page (this only runs against a site's baseURL, i.e. |
There was a problem hiding this comment.
Minor: this comment states the check "only runs against a site's baseURL, i.e. a homepage," but the same analyzeResponse 200-branch is also reachable via the exported analyzeBotProtection, whose JSDoc says it is "Used by content scraper to analyze Puppeteer results" — i.e. per scraped URL, not just the homepage. No functional bug today (the scraper path only treats specific high-confidence CDN types as blocked), but the safety justification here is scoped to homepages and could mislead a future maintainer. Worth correcting the scope note.
Problem
detectBotBlocker(powering the LLMO "Unlock Opportunities" / bot-blocker card viaGET /sites/{siteId}/bot-blocker) reported real customer sites as "blocking Adobe LLM Optimizer from accessing public pages" when they were not.Root cause: on a
200 OK, the generic branch flagged the page as a challenge if its HTML matched any ofCHALLENGE_PATTERNS.general— including the bare/captcha/iand/recaptcha/i. Those match the ubiquitous "This site is protected by reCAPTCHA" disclosure badge / reCAPTCHA form widget, which appears on perfectly normal content pages. Result:{ crawlable: false, type: 'unknown', confidence: 0.7, reason: 'Generic challenge patterns detected' }.Evidence it's a false positive
Confirmed on the affected customer sites by running the shipped function against the live pages and cross-checking the content scraper's own results:
crawlable:false @0.7("Generic challenge patterns detected").recaptcha-texttemplate / "protected by reCAPTCHA" notice) on 100KB+ content homepages.scrapeForbidden:false, thousands of URLs scraped with zero forbidden.So the heuristic disagreed with the empirical scrape: nothing was actually being blocked.
Fix
Gate the generic-pattern check on an interstitial-shape heuristic (
isLikelyInterstitial): on a200, a challenge pattern only counts as a block when the page's visible text is content-thin. Visible text is measured by character length (not word count), so the check is not biased against languages that do not delimit words with spaces (CJK, Thai, etc.). The challenge pattern is evaluated first, so the content-shape strip only runs when a pattern actually matched.crawlable:truecrawlable:false403/error-path detection → unchangedValidation
detectBotBlockeragainst 43 onboarded domains, current vs. fixed: it flipped only the known false positives tocrawlable:trueand changed nothing else — high-confidenceakamai @0.99blocks and non-captcha0.7blocks were untouched (no over-correction, noOK→BLOCK).crawlable:true; a thin captcha interstitial → stillcrawlable:false. All existing challenge tests (thin HTML) still pass.npm test(lint + tests + c8 coverage gate, 100%) passes;bot-blocker-detect.jsat 100%.Notes & limitations
200branch is gated. The CDN-specific challenge patterns (Just a moment…,Access Denied…Akamai, etc.) are precise/low-FP and are intentionally left ungated.GET+ regex. A JS-rendered SPA whose raw homepage shell is content-thin and references reCAPTCHA could still be flagged.🤖 Generated with Claude Code