Skip to content

v0.8.1

Choose a tag to compare

@github-actions github-actions released this 15 May 06:43
· 1 commit to main since this release

v0.8.1 — Preserve zero values in discover_keywords output

A small, targeted patch fixing one data-fidelity bug introduced in v0.8.0's gRPC → REST migration of discover_keywords.

The bug

discover_keywords used falsy checks (int(v) if v else None) to normalise REST int64 fields. The pattern silently mapped a legitimate 0 to None and lost real data — most visibly on the bid micros bounds:

low_bid_int = int(low_bid_micros) if low_bid_micros else None
...
"low_top_of_page_bid": (
    round(low_bid_int / 1_000_000, 2) if low_bid_int else None
),

REST sends int64 fields as JSON strings per the proto3 JSON spec, so the string "0" is truthy and survives the first conversion. But once parsed to int 0, it's falsy and the second step swaps it for None. The result: a keyword with lowTopOfPageBidMicros = "0" (Google's representation when no bid data is available, or when the low bound of a competitive range is genuinely 0) came out the other end as low_top_of_page_bid = None — indistinguishable from "field absent". Users couldn't tell missing data apart from "Google has data and the value is 0".

The same defect was present on the high bid bound, and the same fragile first-step pattern lived on avgMonthlySearches and competitionIndex — those happened to round-trip correctly today because their intermediate _int locals are stored directly into the output dict without a second falsy gate, but any future code that re-used them would inherit the bug.

The fix

Two small helpers replace the inline conversions, both using explicit is not None rather than falsy checks:

  • _maybe_int(value) — proto3-JSON int64 parser. Treats only None and empty string as "missing"; preserves 0 exactly; falls back to None on malformed input rather than crashing the whole response.
  • _micros_to_currency(micros) — micros → 2-dp float. Preserves 0 (becomes 0.0, not None) and None.

The output dict now reads:

ideas.append({
    "keyword": idea.get("text", ""),
    "avg_monthly_searches": _maybe_int(metrics.get("avgMonthlySearches")),
    "competition": competition,
    "competition_index": _maybe_int(metrics.get("competitionIndex")),
    "low_top_of_page_bid": _micros_to_currency(
        _maybe_int(metrics.get("lowTopOfPageBidMicros"))
    ),
    "high_top_of_page_bid": _micros_to_currency(
        _maybe_int(metrics.get("highTopOfPageBidMicros"))
    ),
})

This pattern can't regress to the original bug by accident — the helpers are the only path that constructs the output, and both are is not None.

Action for maintainers

estimate_budget (same module, gRPC path) has the same falsy-check pattern on clicks / impressions / cost_micros / avg_cpc_micros / total_cost. A legitimate clicks=0 or cost_micros=0 would collapse to None there for the same reason. This is pre-existing behaviour, not regressed by v0.8.0, so this patch is intentionally scoped to discover_keywords (the reported bug). File a separate issue if you want the same defensive treatment for estimate_budget.

Verification

  • 258 tests pass (243 v0.8.0 baseline + 15 new in this release)
  • New coverage:
    • TestZeroValuePreservation — end-to-end checks that low_bid=0, high_bid=0, competition_index=0, and avg_monthly=0 all survive the round-trip
    • TestZeroValuePreservation::test_missing_fields_still_return_none — guard against over-correction
    • TestMaybeIntHelper — unit coverage including the specific wire format "0" regression
    • TestMicrosToCurrency — unit coverage including _micros_to_currency(0) == 0.0

Install / upgrade

pipx upgrade adloop
# or
pip install --upgrade adloop
# or
uvx adloop@0.8.1

Restart your MCP host after upgrading.

Full Changelog: v0.8.0...v0.8.1