Skip to content

marketingFilter is bypassed on bulk ingest: cap fails open on backfill, absent entirely on /batch #2183

Description

@zachdunn

Noticed while auditing marketingFilter sources for #2168 (item 5c). No action needed today — filing so the interaction is written down before someone runs a backfill and is surprised by it.

The observation

clickhouse-blog's RSS window exposes far more history than we've ingested:

source feed items stored gap
clickhouse-blog 822 379 443
val-town-blog 109 174
pulumi-blog 50 100
dbt-blog 25 71

It's the only one of the six active feed/video marketingFilter sources with a gap; the others have stored ≥ feed window. The newest 8 ClickHouse items are all stored and there are no marketing-filter-cap-tripped events in 14 days, so steady-state ingest is healthy — the 443 are old backlog outside the ingest window, not a stuck fetch. Nothing is broken.

I did not check whether this generalizes beyond the marketingFilter set. If someone wants to know how much feed history we're leaving on the floor registry-wide, that's the sweep to run.

Why it's worth writing down: backfilling this source would bypass the marketing filter

clickhouse-blog has marketingFilter on for a reason — its feed mixes product news with newsletters, competency announcements, and customer stories. Both backfill routes drop that protection:

  1. Bulk fetch trips the cap and inserts unfiltered. MARKETING_CLASSIFIER_MAX_PER_FIRE = 20 (poll-fetch.ts:1147). Above it, classifyMarketingForReleases logs marketing-filter-cap-tripped and returns an empty map (:1226-1233). Downstream, marketingMap.get(index) is then undefined for every row (:1498), so nothing is suppressed and everything inserts visibly. The cap is a cost guard, and its failure mode is fail-open by design — fine for a 0–5 item feed delta, wrong for a 443-item backfill.
  2. The /batch and workflow paths never classify at all. Neither routes/releases-batch.ts, workflows/backfill-source.ts, nor adapters/scrape-persister.ts reference the classifier — it lives only in the fetchOne path. So a local-ingest or backfill-source run skips it outright.

Net: any obvious way to pull those 443 in lands ~443 unfiltered marketing posts in the index — the exact outcome the filter exists to prevent. The existing escape hatch is the one named in the code comment: insert, then suppress after the fact via the suppress API.

If we ever do want this history

Options, roughly in order of preference:

  • Leave it. 443 mostly-marketing posts from a source we already track live is low value against the cleanup risk. This is my recommendation.
  • Backfill with an explicit classification pass — raise the per-fire cap for a one-off, or run the classifier over the candidate set before insert. Costs ~443 google/gemini-2.5-flash-lite calls, which is cents, but needs the code path to exist.
  • Backfill unfiltered into suppressed=1, then unsuppress the real product news. Safe default, manual review.

Reproducing the numbers

['releases-cloudflare-logs']
| where ['body'] contains 'marketing-filter-cap-tripped'
| extend p = parse_json(['body'])
| project _time, slug = tostring(p['sourceSlug']), candidates = toint(p['candidateCount'])

The gap itself is feed-vs-DB: pull the source's metadata.feedUrl, extract item-level <link>s, and diff against SELECT url FROM releases WHERE source_id = .... Note a feed's <link> set includes non-item links — scope the regex to inside <item> / <entry> blocks or the count comes out ~2× too high.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions