From 9fae2a7f6e96e5f1f1bc315925c1c2c96cbe1c4e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 29 Oct 2026 20:24:00 +0000 Subject: [PATCH] fix(examples): stop zero-filling impressions and flat multi-standard viewability in the reference seller The reference seller's delivery totals reported impressions as 0 when no delivery measured them, set viewable_rate to 0 with nothing measurable, and combined viewability reported under different standards into one flat total. AdCP 3.3 forbids all three: an absent count means not measured, receivers never derive an impression rate from it, and a metric reported under more than one qualifier omits its flat carrier from buy totals. Totals now carry impressions only when a delivery measured them, omit viewable_rate without measurable impressions, and omit the flat viewability block when deliveries used more than one standard. Toward callers served below 3.3, format breakdown rows without impressions are omitted rather than sent with a zero. Refs #1669 Co-Authored-By: Claude Opus 5.5 (1M context) --- examples/seller_agent.py | 27 ++++++++++++-- tests/test_seller_agent_storyboard.py | 54 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/examples/seller_agent.py b/examples/seller_agent.py index a296becc7d..29b95497e4 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -702,21 +702,24 @@ def _package_budget_total(packages: list[dict[str, Any]]) -> Decimal: def _delivery_totals(events: list[dict[str, Any]]) -> dict[str, Any]: totals: dict[str, Any] = { - "impressions": sum(event.get("impressions", 0) for event in events), "spend": float(sum((Decimal(str(event.get("spend", 0))) for event in events), Decimal(0))), } - for metric in ("clicks", "conversions"): + # AdCP 3.3: an impression count nobody measured is absent, never zero. + for metric in ("impressions", "clicks", "conversions"): if any(metric in event for event in events): totals[metric] = sum(event.get(metric, 0) for event in events) views = [event["viewability"] for event in events if event.get("viewability")] - if views: + # AdCP 3.3: viewability reported under more than one standard has no flat + # total; one row is one (metric, qualifier) key. + if views and len({view.get("standard") for view in views}) == 1: measurable = sum(view.get("measurable_impressions", 0) for view in views) viewable = sum(view.get("viewable_impressions", 0) for view in views) totals["viewability"] = { "measurable_impressions": measurable, "viewable_impressions": viewable, - "viewable_rate": viewable / measurable if measurable else 0, } + if measurable: + totals["viewability"]["viewable_rate"] = viewable / measurable timed = [view for view in views if "viewed_seconds" in view] weight = sum(view.get("measurable_impressions", 0) for view in timed) if weight: @@ -756,6 +759,20 @@ def _split_delivery(totals: dict[str, Any], weights: list[Decimal]) -> list[dict return results +def _serves_impressionless_rows(context: Any) -> bool: + """Whether the caller's release (AdCP 3.3+) allows rows without ``impressions``. + + Below 3.3 a dimensional row with no impression count is omitted, never + sent as ``impressions: 0``. + """ + version = getattr(context, "resolved_adcp_version", None) + if not version: + return True + major, _, rest = str(version).partition(".") + minor = rest.split(".", 1)[0].split("-", 1)[0] + return not (major.isdigit() and minor.isdigit()) or (int(major), int(minor)) >= (3, 3) + + def _narrow_metrics(metrics: dict[str, Any], requested: list[str] | None) -> dict[str, Any]: if not requested: return metrics @@ -1813,6 +1830,8 @@ async def get_media_buy_delivery( "currency": mb.get("currency", "USD"), **_narrow_metrics(metrics, requested), } + if not _serves_impressionless_rows(context): + rows = [row for row in rows if "impressions" in row] if format_request is not None and reporting.get("supports_format_breakdown"): sort_by = format_request.get("sort_by", "spend") direction = format_request.get("sort_direction", "desc") diff --git a/tests/test_seller_agent_storyboard.py b/tests/test_seller_agent_storyboard.py index 8d07e217e8..377063fe7d 100644 --- a/tests/test_seller_agent_storyboard.py +++ b/tests/test_seller_agent_storyboard.py @@ -1234,3 +1234,57 @@ async def test_compact_catalog_ignores_outcome_target_and_paginates_fields() -> assert "products" not in unchanged invalid = await _seller().list_products({"cursor": "invalid"}) assert invalid["errors"][0]["code"] == "INVALID_REQUEST" + + +@pytest.mark.asyncio +async def test_delivery_without_impressions_omits_them_and_rates() -> None: + """AdCP 3.3: absence means not measured; never fill zero or derive a rate.""" + store = _store() + await store.seed_media_buy( + media_buy_id="unmeasured", fixture={"packages": [{"package_id": "p", "budget": 10}]} + ) + cumulative = await store.simulate_delivery( + "unmeasured", + reported_spend={"amount": 5, "currency": "USD"}, + viewability={"measurable_impressions": 0, "viewable_impressions": 0, "standard": "mrc"}, + ) + assert "impressions" not in cumulative["cumulative"] + report = await _seller().get_media_buy_delivery({"media_buy_ids": ["unmeasured"]}) + buy = report["media_buy_deliveries"][0] + assert "impressions" not in buy["totals"] + assert "impressions" not in buy["by_package"][0] + assert "viewable_rate" not in buy["totals"]["viewability"] + + +@pytest.mark.asyncio +async def test_viewability_under_two_standards_has_no_flat_total() -> None: + """AdCP 3.3: a metric reported under more than one qualifier omits its flat carrier.""" + store = _store() + await store.seed_media_buy( + media_buy_id="mixed-standards", fixture={"packages": [{"package_id": "p", "budget": 10}]} + ) + for standard in ("mrc", "groupm"): + await store.simulate_delivery( + "mixed-standards", + impressions=100, + reported_spend={"amount": 1, "currency": "USD"}, + viewability={ + "measurable_impressions": 100, + "viewable_impressions": 50, + "standard": standard, + }, + ) + report = await _seller().get_media_buy_delivery({"media_buy_ids": ["mixed-standards"]}) + totals = report["media_buy_deliveries"][0]["totals"] + assert totals["impressions"] == 200 + assert "viewability" not in totals + + +@pytest.mark.parametrize( + ("version", "expect_rows"), [("3.3-beta.1", True), ("3.2", False), (None, True)] +) +def test_impressionless_rows_are_served_only_from_33(version, expect_rows) -> None: + from types import SimpleNamespace + + context = None if version is None else SimpleNamespace(resolved_adcp_version=version) + assert _sa._serves_impressionless_rows(context) is expect_rows