Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/metric-leaf-identities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"adcontextprotocol": minor
---

Add leaf metric identities so nested delivery values are individually declarable, committable, aggregatable, and sortable: `quartile_25`–`quartile_100` (resolving to `quartile_data.q1_views`–`q4_views`) and `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds` (resolving to the same-named `viewability` fields) join `available-metric` and `sort-metric`. This closes an existing contradiction: `committed-metric` qualifier rules and `delivery-metric-aggregate` conditionals already referenced these metric_ids, and the shipped `committed_metrics` / `metric_aggregates` examples were invalid against their own schemas. Also adds the missing flat transactional scalars (`commissionable_value`, `plays`, `cost_per_completed_view`, `cpm`, `downloads`, `units_sold`, `new_to_brand_units`) to `sort-metric`, with survey/model-based lift scalars documented as intentionally sort-excluded. Leaf identities resolve to the nested canonical values — no duplicate flat response fields are introduced. A metric-identity coherence contract test now enforces enum/schema/example agreement.

**Seller conformance note:** the `viewability` description now requires (MUST) that sellers populate `standard` on reported viewability objects when `committed_metrics` carry a `viewability_standard` qualifier — upgraded from a SHOULD. The trigger is any `committed_metrics` entry with `qualifier.viewability_standard` set, including the container `metric_id: "viewability"` (already in the enum). Sellers with an existing `{metric_id: "viewability", qualifier: {viewability_standard: "mrc"}}` commitment who were sometimes omitting `standard` become non-conformant. Sellers who never use the `viewability_standard` qualifier are unaffected.
2 changes: 2 additions & 0 deletions docs/media-buy/media-buys/optimization-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ Buyers receive the intersection of both. `impressions` and `spend` are always re
- **`new_to_brand_units`**: Unit-volume parallel to `new_to_brand_rate` — count of units sold to first-time brand buyers
- **`plays`**: Raw play count for DOOH/broadcast inventory (mirrors `forecastable-metric.plays`); distinct from `dooh_metrics.loop_plays` (per-screen rotation) and `impressions` (multiplied audience figure)

The numeric values inside `viewability` and `quartile_data` are individually addressable through leaf metric identities — `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds`, and `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` (which resolve to `quartile_data.q1_views`–`q4_views`). Leaf identities appear in `available_metrics` declarations, `committed_metrics` contracts, `metric_aggregates` rows, and breakdown `sort_by`; the nested object remains the only carrier of the values in delivery reports. In every metric set operation (declaration, filtering, intersection, selection) a container token subsumes its leaf identities — declaring `viewability` satisfies `viewable_rate`. Note that `quartile_100` counts 100%-of-duration completions (VAST `complete`), which is distinct from `completed_views` when the buy defines a shorter billable view threshold.

Buyers can optionally request a subset via `requested_metrics` to reduce payload size and focus on relevant KPIs.

For `completion_rate` and `quartile_data`, sellers MAY return `null` to signal the metric does not apply (e.g. on a non-video buy), and clients MUST accept `null` as a valid value for these two fields. Every other metric signals "not applicable" by omission — sellers omit it rather than sending `null`.
Expand Down
2 changes: 1 addition & 1 deletion docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ When you include `reporting_dimensions` in the request, the response includes di
}
```

Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported.
Geo, device, audience, demographic, and placement dimensions accept optional `limit` and `sort_by` (any value from the `sort-metric` enum; defaults to `spend`). The sort vocabulary includes leaf metric identities that order rows by a value nested inside an object-shaped metric — `sort_by: "quartile_100"` orders by `quartile_data.q4_views`, `sort_by: "viewable_rate"` orders by `viewability.viewable_rate` — so queries like "top placements by in-view rate" need no flat duplicate fields. Survey/model-based lift scalars are excluded from sorting; see the `sort-metric` enum description. Geo requires `geo_level` (`country`, `region`, `metro`, `postal_area`). Demographic accepts optional canonical `age_ranges`. Each requested range must be supported by the product's `supports_demographic_breakdown.age`: `exact_predicates` accepts ranges within its declared bounds, while `enumerated_intervals` accepts only exact declared intervals. Unsupported ranges return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature); the seller never substitutes a wider or narrower bucket. Omitting `age_ranges` requests the product's native declared breakdown. The spot log accepts only `limit`; its canonical order is `aired_at` ascending. Breakdowns are per-dimension only — cross-dimensional intersections are not supported.

### Available dimensions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"deploy:cdn-artifacts-cutover:dry-run": "wrangler deploy --config workers/artifact-cdn/wrangler.cutover.toml --dry-run",
"verify:cdn-artifacts-cutover": "node scripts/verify-cdn-artifacts-cutover.mjs",
"typecheck": "tsc --project server/tsconfig.json --noEmit",
"test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting",
"test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/metric-identity-coherence.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting",
"test:performance-feedback": "node --test --test-force-exit --test-timeout=30000 tests/performance-feedback-contract.test.cjs",
"test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs",
"test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge",
Expand Down
6 changes: 5 additions & 1 deletion scripts/precommit-server-unit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ function pathsFromNameStatus(output) {
}

function stagedFiles() {
const output = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRD', '-z']);
// Merge commits with main can stage tens of thousands of dist/ files;
// Node's default 1 MB maxBuffer overflows (ENOBUFS) on the -z listing.
const output = execFileSync('git', ['diff', '--cached', '--name-status', '--diff-filter=ACMRD', '-z'], {
maxBuffer: 64 * 1024 * 1024,
});
return pathsFromNameStatus(output);
}

Expand Down
4 changes: 2 additions & 2 deletions static/schemas/source/core/delivery-metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
},
"quartile_data": {
"type": ["object", "null"],
"description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy).",
"description": "Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy). Individual quartiles are addressable via the leaf metric identities `quartile_25` (q1_views), `quartile_50` (q2_views), `quartile_75` (q3_views), and `quartile_100` (q4_views) for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. Quartiles are player-fired events (VAST firstQuartile/midpoint/thirdQuartile/complete). `quartile_100` counts 100%-of-duration completions and is distinct from `completed_views`, which counts completions at the seller's billable view threshold (`view_duration_seconds`) when one is set.",
"properties": {
"q1_views": {
"type": "number",
Expand Down Expand Up @@ -288,7 +288,7 @@
},
"viewability": {
"type": "object",
"description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable.",
"description": "Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. The numeric leaves are addressable via the leaf metric identities `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. When a buy reports under more than one standard, contract a specific standard via the `viewability_standard` qualifier on `committed_metrics`; when the package's `committed_metrics` carry a `viewability_standard` qualifier, sellers MUST populate `standard` on reported viewability objects so reconciliation can match the qualifier.",
"properties": {
"vendor": {
"$ref": "/schemas/core/brand-ref.json",
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/format.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@
},
"reported_metrics": {
"type": "array",
"description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. If omitted, the format defers entirely to product-level metric declarations.",
"description": "Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. The intersection is evaluated under the container-subsumption rule in `enums/available-metric.json` — a container token (e.g. `viewability`) matches its leaf identities (e.g. `viewable_rate`), so mixed container/leaf declarations do not produce an empty intersection. If omitted, the format defers entirely to product-level metric declarations.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/product-filters.json
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@
},
"required_metrics": {
"type": "array",
"description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.",
"description": "Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). Superset evaluation follows the container-subsumption rule in `enums/available-metric.json` — a product declaring the `viewability` container satisfies a requirement for the `viewable_rate` leaf. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
2 changes: 1 addition & 1 deletion static/schemas/source/core/reporting-webhook.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
},
"requested_metrics": {
"type": "array",
"description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics.",
"description": "Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics. Subset evaluation and leaf resolution follow the container-subsumption rule in `enums/available-metric.json`: requesting a leaf identity selects its canonical carrier object in the payload.",
"items": {
"$ref": "/schemas/enums/available-metric.json"
},
Expand Down
10 changes: 9 additions & 1 deletion static/schemas/source/enums/available-metric.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/enums/available-metric.json",
"title": "Available Metric",
"description": "Standard delivery and performance metrics available for reporting",
"description": "Standard delivery and performance metrics available for reporting. Most values name flat numeric fields of `core/delivery-metrics.json`. Leaf identities address a single numeric value nested inside an object-shaped metric so it can be declared, committed, aggregated, and sorted individually: `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` resolve to `quartile_data.q1_views`–`q4_views`, and `viewable_rate`/`viewable_impressions`/`measurable_impressions`/`viewed_seconds` resolve to the same-named fields of `viewability`. The nested object remains the canonical carrier of the reported value — leaf identities never introduce duplicate flat response fields. The object-shaped tokens (`viewability`, `quartile_data`, `dooh_metrics`) declare the whole nested surface at once. **Container subsumption**: in every set operation over this enum — capability declaration, `required_metrics` filtering, format `reported_metrics` intersection, and `requested_metrics` selection — a container token subsumes its leaf identities: declaring `viewability` satisfies a requirement or request for `viewable_rate`, and requesting a leaf identity selects its canonical carrier object in the response (never a flat duplicate). A leaf declaration does not imply sibling leaves or the carrier's non-numeric fields.",
"type": "string",
"enum": [
"impressions",
Expand All @@ -28,7 +28,15 @@
"saves",
"profile_visits",
"viewability",
"viewable_rate",
"viewable_impressions",
"measurable_impressions",
"viewed_seconds",
"quartile_data",
"quartile_25",
"quartile_50",
"quartile_75",
"quartile_100",
"dooh_metrics",
"cost_per_click",
"cost_per_completed_view",
Expand Down
19 changes: 17 additions & 2 deletions static/schemas/source/enums/sort-metric.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/enums/sort-metric.json",
"title": "Sort Metric",
"description": "Numeric delivery metrics available for sorting breakdown rows. Subset of delivery-metrics fields that are flat numeric values (excludes nested objects like quartile_data, dooh_metrics, viewability, by_event_type, by_action_source).",
"description": "Numeric delivery metrics available for sorting breakdown rows. Contains the flat numeric fields of `core/delivery-metrics.json` plus leaf identities that resolve to a single numeric value nested inside an object-shaped metric: `quartile_25`–`quartile_100` order rows by `quartile_data.q1_views`–`q4_views`; `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` order rows by the same-named fields of `viewability`. Sorting by a leaf identity orders on the nested value — the nested object remains the only carrier of the value in responses; no duplicate flat fields exist. Excludes object/array containers (`quartile_data`, `dooh_metrics`, `viewability`, `by_event_type`, `by_action_source`, `vendor_metric_values`) and the survey/model-based lift scalars (`incremental_sales_lift`, `brand_lift`, `foot_traffic`, `conversion_lift`, `brand_search_lift`), which are package-grain estimates rather than row-grain orderings.",
"type": "string",
"enum": [
"impressions",
Expand All @@ -26,6 +26,21 @@
"saves",
"profile_visits",
"engagement_rate",
"cost_per_click"
"cost_per_click",
"commissionable_value",
"plays",
"cost_per_completed_view",
"cpm",
"downloads",
"units_sold",
"new_to_brand_units",
"viewable_rate",
"viewable_impressions",
"measurable_impressions",
"viewed_seconds",
"quartile_25",
"quartile_50",
"quartile_75",
"quartile_100"
]
}
Loading
Loading