Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix: disguised-ad-flag replaces entire Reddit feed as one placeholder (#228)#230

Merged
twschiller merged 1 commit into
mainfrom
fix/disguised-ad-flag-feed-wrapper-228
Jun 9, 2026
Merged

Fix: disguised-ad-flag replaces entire Reddit feed as one placeholder (#228)#230
twschiller merged 1 commit into
mainfrom
fix/disguised-ad-flag-feed-wrapper-228

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

Closes #228.

When one card inside a feed momentarily lacks its own heading — different ad creative, mid-hydration burst, or any condition where the per-card heading element isn't visible to querySelector — the walk-up from that card's "Promoted" / "Sponsored" label skipped past the card and reached the surrounding feed. The feed passed isArticleShaped because sibling cards carry headings, images, links, and prose. The rule then replaced the whole feed with one placeholder.

Repro

https://www.reddit.com/: I confirmed the path with Playwright. Reddit's <shreddit-feed> contains many <shreddit-ad-post> cards. Each ad post's title is a <shreddit-dynamic-ad-link class="headline">. Normal <shreddit-post> cards carry no headings at all. With one ad-post's headline class temporarily removed (simulating a different ad variant or hydration race), the walk landed on <shreddit-feed> at hop 5 — the feed had image+link+prose plus other ad posts' headlines, so it passed every check.

Fix

  • New hasOtherCardSubtree(element, labelElement): rejects any candidate that encloses a second image+link subtree outside the label's ancestor chain. The image+link pair is the cheapest stable signal for "card-ness" and is rare in non-card chrome.
  • isPageBoundary now also stops at role="feed" (the ARIA infinite-scroll pattern), as defense-in-depth.

Each individual card still matches on its own walk-up. A wrapper holding multiple cards no longer does. Cards that lack their own heading aren't redacted (degraded, but acceptable — the previous behavior was to hide the entire feed).

Test plan

  • bun run check — clean
  • bun run typecheck — clean
  • bun run knip — clean
  • ./node_modules/.bin/jest — 1916 tests passing, including:
    • 3 new example tests in disguised-ad-flag.test.ts for the multi-card wrapper, the role="feed" boundary, and a section landing
    • new disguised-ad-flag.property.test.ts with fast-check properties asserting that wrappers holding multiple labeled card-shaped children are never replaced (per the memory note about property tests for rule matchers)
  • Manual on reddit.com with the unpacked extension once this lands

🤖 Generated with Claude Code

…#228)

When one card inside a feed momentarily lacks its own heading (different
ad creative, mid-hydration burst), the walk-up from that card's label
skipped past it and reached the surrounding feed. The feed passed
isArticleShaped because *sibling* cards carry headings/images/links —
the rule then replaced the whole feed with one placeholder.

Add a sibling-card guard in isArticleShaped: a candidate fails if it
encloses a second image+link subtree outside the label's ancestor
chain. Also stop the walk at role="feed" (the ARIA infinite-scroll
pattern). Each individual card now matches on its own walk-up; the
feed wrapper stays unmatched.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 9, 2026 2:17pm

Request Review

@twschiller twschiller added the bug Something isn't working label Jun 9, 2026
@twschiller twschiller merged commit e4e96a3 into main Jun 9, 2026
7 checks passed
@twschiller twschiller deleted the fix/disguised-ad-flag-feed-wrapper-228 branch June 9, 2026 14:20

@unblocked unblocked Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found.

About Unblocked

Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.

📖 Documentation — Learn more in our docs.

💬 Ask questions — Mention @unblocked to request a review or summary, or ask follow-up questions.

👍 Give feedback — React to comments with 👍 or 👎 to help us improve.

⚙️ Customize — Adjust settings in your preferences.

Comment on lines +223 to +243
function hasOtherCardSubtree(element: Element, labelElement: Element): boolean {
for (const candidate of element.querySelectorAll("*")) {
if (candidate === labelElement) {
continue;
}
if (labelElement.contains(candidate)) {
continue;
}
if (candidate.contains(labelElement)) {
continue;
}
if (candidate.querySelector("img, picture") === null) {
continue;
}
if (candidate.querySelector("a[href]") === null) {
continue;
}
return true;
}
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasOtherCardSubtree returns true as soon as it finds any descendant outside the label's ancestor chain that contains both an <img>/<picture> and an <a href>. This correctly rejects a feed with multiple cards, but it also rejects a single ad card whenever a wrapper <div> sits between the label and the card's content.

Example — common in the wild but absent from the test suite:

<article>
  <span class="label">Sponsored</span>
  <div class="card-body">
    <h2><a href="/ad">Title</a></h2>
    <img src="ad.jpg" />
    <p>Long body text that exceeds the prose minimum …</p>
  </div>
</article>

Trace: div.card-body is not the label, is not contained by the label, and does not contain the label. div.card-body.querySelector("img, picture")<img> (not null). div.card-body.querySelector("a[href]")<a> (not null). → returns trueisArticleShaped returns false → the legitimate ad card escapes detection.

The comment on line R218-R219 says "a single advertorial card has at most one such subtree (its own)", but that assumption breaks whenever a card's label is a sibling to (rather than inside) a content wrapper.

Consider requiring two non-overlapping matches before rejecting, e.g. count the number of qualifying subtrees and return count >= 2. That way a single card's content wrapper counts as one (tolerated), but a feed with multiple cards still trips the guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entire reddit feed is blocked as sponsored content

1 participant