Skip to content

Measurement: declare seller-initiated trackers and impression-ID joinability #6878

Open

Description

@lukasz-pubx

Summary

Seller agents can already inject their own measurement trackers at trafficking/assembly time — for agents without on-page presence or ad-server log access, it is the only way to build an independent impression count, and billing_measurement.vendor can already contract such a count as the invoicing authority. But the practice is protocol-invisible: nothing declares what a seller's reported numbers are counted from, that delivered payloads carry seller beacons absent from every buyer manifest, or whether {IMPRESSION_ID} makes the two sides' records joinable. This proposes small, additive declarations for all three, so counterparties select and contract on declared measurement posture instead of discovering it mid-dispute.

Current state (as of 7c155983 — re-verify at implementation time)

  • The tracker-execution contract work (PR feat(creative): declare tracker execution contracts #6786, closing RFC: Measurement capability declaration (tracking_capabilities, attribution methodologies, TMP exposure data) #6207) is one-directional by design: "Seller-authored production commitment for first-class buyer-supplied tracker occurrences" (static/schemas/source/core/tracker-execution-contract.json).
  • core/measurement-terms.json already provides the contractual hook: billing_measurement.vendor is "Vendor whose measurement of the billing metric is authoritative for invoicing" — a brand-ref that can name any measurement party, including a seller agent's own counting system. What's missing is the discovery-time declaration that would tell a buyer such a count exists and what it's based on.
  • core/provenance.json establishes the repo's precedent for honest-claim declarations: "Provenance is a claim by the declaring party — receiving parties should verify claims" — but it covers content/AI provenance only, not payload tracker injection.
  • docs/reference/known-limitations.mdx: "The underlying delivery platform — or a buyer-specified measurement vendor — is the system of record for counting… AdCP is the wire and the contract, not the ledger." This proposal moves no system of record; it makes the counting source declarable so the existing billing_measurement machinery has something to name.
  • core/reporting-capabilities.json (available_metrics, vendor_metrics, measurement_windows, …) is the established discovery-time declaration home.
  • enums/universal-macro.json defines IMPRESSION_ID ("…minted by the publisher, ad-decision layer, or buyer at {TMPX} decode") — the minting vocabulary exists only as prose, with no per-product declaration and no statement of whether the value survives into any impression-level data.

Problem

  1. Unattributable attestations (primary). A seller agent without ad-server log access attests delivery on get_media_buy_delivery, counted from its own trackers. The buyer assumes ad-server-sourced numbers. When variance exceeds billing_measurement.max_variance_percent, the parties dispute a discrepancy whose dominant cause — begin-to-render tracker counting vs post-IVT ad-server counting — was never on the wire. The measurement_window vocabulary (post_ivt, post_sivt) already acknowledges these stages matter for reconciliation; the counting source is the missing half.
  2. Unreconcilable counts (primary). Even a disclosed independent count is only auditable record-by-record if impression identifiers join across parties. Two conformant sellers both substitute {IMPRESSION_ID}; one preserves it in its impression-level data, one discards it after substitution. No surface distinguishes them before spend — this was RFC: Measurement capability declaration (tracking_capabilities, attribution methodologies, TMP exposure data) #6207's impression_id bullet, orphaned when PR feat(creative): declare tracker execution contracts #6786 narrowed that RFC's 3.2 scope.
  3. Undeclared payload beacons (secondary, severable). Delivered payloads already carry beacons no buyer manifest names (ad-server counting, publisher analytics), so today an extra seller pixel mostly passes unremarked. This changes when observation becomes machine-checked: spec(creative): implement preview event and macro observation sessions #6782 preview-observation sessions will intercept outbound actions and compare them against declarations, and buyers with strict data-governance terms increasingly enumerate permitted calls. A declaration makes the seller's beacon expected rather than anomalous. This field is the weakest-evidenced of the three and is explicitly severable if the WG prefers to wait for spec(creative): implement preview event and macro observation sessions #6782.

Proposed change

All additions optional; declarations normative when present; absence means undeclared, never unsupported (per #6207 and #6786 convention).

1. reporting_capabilities.impression_id — top-level, because joinability matters regardless of who counts (a buyer vendor reconciling against ad-server logs needs it as much as a seller-agent count does):

"impression_id": {
  "minted_by": "ad_decision",
  "available_in_delivery": true
}

minted_by: new enums/impression-id-source.json, values publisher | ad_decision | buyer_at_tmpx — verbatim from the merged macro's prose, no new vocabulary. available_in_delivery: whether the minted value is preserved as a join key in whatever impression-level surface the seller offers (log-level delivery per #2196, TMP exposure data, or out-of-band export). true commits to key-preservation; the pipe and record schema stay #2196's scope.

2. reporting_capabilities.counting_sources[] — what the seller's reported delivery numbers are counted from; an array, because real deployments are hybrid (own trackers for some inventory, ad-server aggregates elsewhere):

"counting_sources": ["seller_tracker", "ad_server"]

If #2041's measurement_sources vocabulary lands first, this MUST share it rather than fork — #2041 attributes numbers at report time; this declares posture at discovery time.

3. reporting_capabilities.seller_tracker_events[] (severable) — events for which the seller injects its own trackers into delivered payloads, values from enums/pixel-tracking-event.json for pixel-shaped injection, with VAST/DAAST-assembled <Impression> insertion declared via the impression value.

On conformance paths, directly: #6207's triage blocked substituted_macros_in_trackers[] for conflicting with DR-0005 — no wire-observable conformance path. These fields differ in kind: they are honest-claim declarations on the provenance.json model ("a claim by the declaring party — receiving parties should verify"), whose enforcement hook is contractual (billing_measurement.vendor, data-governance terms) rather than a compliance probe. available_in_delivery additionally becomes probe-able the moment #2196 lands (request the LLD surface, assert the key is present), and seller_tracker_events[] becomes probe-able under #6782 observation. counting_sources[] alone never gets a wire probe — the proposal accepts that openly and argues the declaration still pays for itself as the thing counterparties contract against; if the WG holds the DR-0005 line strictly, that field can be dropped without weakening the other two.

Privacy note: a seller-injected tracker is a data-collecting network call; declared trackers operate within the publisher's consent scope, and this declaration composes with (does not solve) consent-scope propagation, tracked in #2540.

Deliberately untouched: firing mechanics (no seller-side counterpart to tracker_execution_contract — the seller needs no buyer permission to measure its own delivery); get_media_buy_delivery aggregate shapes; billing_measurement (the hook already exists); attribution semantics; the LLD record schema (#2196); trusted_match.exposure_data (#6207's other orphaned bullet — flagged, not revived here).

Alternatives considered

  • Extend Measurement source attribution on delivery reports #2041 instead of adding counting_sources[]. Genuinely viable — its refactor discussion is already generalising the source enum. Acceptable resolution: this issue carries fields 1 and 3, and counting_sources[] lands as a comment on Measurement source attribution on delivery reports #2041. Either works; stated preference is one coherent declaration block, but the vocabulary must be shared regardless.
  • Per-representation disclosure instead of product-level seller_tracker_events[] (near Creative delivery evidence: manifest hashing, retention contracts, and evidence references #6349's evidence work): more precise, much heavier; product-level can ship first.
  • Do nothing. Seller-side counting keeps working as a hidden hack: undisclosed beacons, variance disputes whose cause is invisible, and joinability discovered post-spend. Since billing_measurement.vendor can already name a seller count as authoritative, doing nothing leaves the protocol able to contract on a measurement source it cannot describe.

Compatibility

Additive, optional, minor bump. Older clients ignore the new fields. No 3.x backport.

Files affected (as of 7c155983)

Related

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Aug 25, 2026
  2. bokelley commented on Aug 25, 2026

    @bokelley
    Contributor

    Triage

    Classification: Feature request (spec proposal)
    Bucket(s): spec/protocol
    Status: ready-for-human

    What the experts said:

    • ad-tech-protocol-expert: APPROVE on direction, field-3 severance, and impression_id placement. DR-0005 does not apply (honest-claim declarations, not conformance probes). CONCERN on counting_sources[] null-state semantics (is omission "unknown," and must sellers who have set billing_measurement.vendor to their own count declare this?). BLOCK on vocabulary sequencing — field 2 must share an enum with Measurement source attribution on delivery reports #2041 before landing or discovery-time and report-time nomenclature will fork permanently. Also flags: canonical-reporting-capabilities.json has additionalProperties: false; new fields must be declared there too (not mentioned in the issue).
    • adtech-product-expert: APPROVE on real-world gap (CTV/SSAI sellers are the primary case; pre-buy filtering is the dominant use, not post-dispute audit). CONCERN on placement — reporting_capabilities is output-focused; counting_sources[] and impression_id are counting-methodology inputs that may sit more naturally under billing_measurement where the invoicing authority already lives. Same vocabulary-coordination concern as above.

    My take: Both experts confirm the gap is real and the fields are non-breaking. They split on placement; vocab sequencing with #2041 is an agreed gate regardless. Two WG calls needed before implementation can start.


    Decision 1 — Placement (experts split) — @bokelley, your call:

    Option A — Fields in reporting_capabilities as proposed:

    "reporting_capabilities": {
      "impression_id": { "minted_by": "ad_decision", "available_in_delivery": true },
      "counting_sources": ["seller_tracker", "ad_server"]
    }

    Pro: follows existing pattern (measurement_windows, vendor_metrics); available_in_delivery is inherently a reporting-surface capability. Con: counting source is a methodology input, not a reporting output — may confuse implementers.

    Option B — Fields nest under billing_measurement (or a new measurement_posture object):

    "billing_measurement": {
      "vendor": { "brand_id": "...", "domain": "..." },
      "counting_sources": ["seller_tracker"],
      "impression_id": { "minted_by": "ad_decision", "available_in_delivery": true }
    }

    Pro: co-located with the contracting authority that already names the invoicing count. Con: impression_id joinability is useful regardless of billing_measurement config; forcing it here conflates declaration scope.

    Option C (hybrid): impression_id in reporting_capabilities; counting_sources in billing_measurement. Cleaner semantics at the cost of split placement.


    Decision 2 — Vocabulary sequencing with #2041 — WG call:

    counting_sources[] (discovery-time) and measurement_sources[] from #2041 (report-time) must share an enum. Either this issue takes ownership of enums/counting-source.json and #2041 imports it, or field 2 is formally blocked on #2041. The vocabulary must be shared regardless; the WG decides who leads.


    Field 3 (seller_tracker_events[]): Both experts APPROVE deferral pending #6782. Also note: enums/pixel-tracking-event.json (referenced in the issue) does not exist; vast-tracking-event.json / daast-tracking-event.json do — the severed issue will need to resolve this.


    Triaged by Claude Code. Session: https://claude.ai/code/session_011gsUyQRZDe4Eaz1govfaTu


    Generated by Claude Code

  3. added
    rfcProtocol change — auto-adds to roadmap board
    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.
    schemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygiene
    needs-wg-reviewBlocked on a working-group decision — surface in WG meeting agendas
    media-buyIssue concerns the media-buy protocol domain
    and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Aug 25, 2026
  4. lukasz-pubx commented on Aug 25, 2026

    @lukasz-pubx
    Author

    Thanks for the fast triage — responding on both decisions and the concerns, and folding in the 3.3 framing agreed with @bokelley on #6786 (his reply routes both surviving surfaces to 3.3, which this issue should be read as targeting; happy to see it labeled accordingly).

    Decision 1 (placement): as the issue author, Option C — accepted as an explicit re-scoping, not just a relocation. The product-expert's concern is right: product.measurement_terms already exists as seller defaults — "Declares who counts the billing metric" — so counting_sources[] next to billing_measurement.vendor stays pre-buy discoverable, and vendor/counting_sources become who counts / how it's counted for the same metric. But being precise about what moving it changes:

    • Scope narrows to the billing metric. As filed, the field covered all reported delivery numbers; under C it declares the billing-count methodology only. That's the honest scope — a seller can bill from ad-server counts while sourcing breakdowns from its own trackers, and one flat field can't describe both. General per-number provenance on reporting surfaces is then Measurement source attribution on delivery reports #2041's report-time measurement_sources[], and the two compose cleanly instead of overlapping. I'll re-frame Problem 1 in the body accordingly.
    • The field becomes negotiable, not just visible. Inside measurement_terms, a buyer can propose counting_sources and force accept / TERMS_REJECTED / adjust on the seller's methodology. Naming it as intentional: buyer-visibility is the requirement, buyer-negotiability is an acceptable consequence — the seller retains rejection. If the WG wants visibility without negotiability, that argues for Option A; flagging the fork so it's decided, not inherited.

    impression_id stays in reporting_capabilities per C: joinability is a property of the seller's reporting/log surfaces regardless of billing configuration — exactly the con Option B was flagged for. Also adopting the product-expert's framing gladly: pre-buy filtering is the dominant use, and both fields should read as selection inputs first, dispute forensics second.

    Once Decision 1 is called, I'll edit the issue body to match (placement, re-scoped Problem 1, corrected Files affected) so the body stays the source of truth rather than this thread.

    Decision 2 (vocabulary sequencing): mild preference that this issue owns enums/counting-source.json, with #2041 importing and extending. This issue is narrow and 3.3-targeted while #2041's refactor is still settling scope; a small enum landing here unblocks both. It should be drafted as the union from day one — seller_tracker | ad_server | publisher_logs | measurement_vendor reconciled with #2041's broadcast set (set_top_box, acr, panel, server_logs, sdk, census), with overlaps collapsed deliberately (server_logs vs publisher_logs/ad_server needs one answer, not near-synonyms). Happy to draft the merged enum for review. If the WG prefers #2041 to lead, formally blocking field 2 on it is equally acceptable — shared vocabulary is the requirement; ownership isn't.

    Null-state semantics (ad-tech-protocol-expert): omission means undeclared — never "unknown-but-probably-ad-server" — consistent with the issue's convention. On the self-vendor question, two options for the WG:

    • SHOULD: a seller naming itself as billing_measurement.vendor SHOULD declare counting_sources[] (docs-level prose; zero enforcement).
    • Conditional MUST: when vendor resolves to the seller's own domain, counting_sources[] is required — schema-expressible, testable, and notably it hands the field the wire-observable conformance path it otherwise lacks, closing the residual DR-0005 exposure.

    Author preference: the conditional MUST — it converts the field's weakest property into a probe.

    Corrections accepted, with one sequencing clarification:

    • canonical-reporting-capabilities.json (additionalProperties: false) — correct, missed in Files affected. impression_id must be declared there too (it's a selection input; the compact projection should carry it). Under C, counting_sources[] instead touches canonical-measurement-terms.json — same additionalProperties check applies there. Both go into the body edit.
    • enums/pixel-tracking-event.json — correct that it doesn't exist on main today: it's introduced by PR feat(creative): declare tracker execution contracts #6786 (extracted from the inline pixel-tracker-asset.json enum). Since feat(creative): declare tracker execution contracts #6786 is 3.2-beta and this issue is 3.3, the reference resolves before the severed field-3 issue lands — a sequencing dependency for that issue, not a defect here.

    Field 3: agreed all around — stays severable, and I'll file it as its own issue framed around #6782's observation surface once that work is far enough along to name the probe (per the #6786 discussion), carrying the enum-sequencing note above.

  5. added this to the 3.3.0 milestone on Aug 29, 2026
  6. bokelley commented on Aug 29, 2026

    @bokelley
    Contributor

    Agree this belongs in 3.3; the issue is now assigned to the 3.3.0 milestone. Before the schema shape is treated as implementation-ready, three constraints need to be reflected:

    1. Minting source is not necessarily singular at product scope. minted_by overstates what discovery can promise when the source may vary per impression or execution path. The discovery declaration should express possible_sources[]; if the actual source is needed for reconciliation, it belongs on the eventual impression-level record. This also matches the deferral recorded when RFC: Measurement capability declaration (tracking_capabilities, attribution methodologies, TMP exposure data) #6207 was narrowed.
    2. Joinability needs an explicit surface, not available_in_delivery. get_media_buy_delivery is aggregate reporting and cannot carry an impression-level key. The capability should name the concrete join/export surfaces on which the identifier is preserved (with Clean room and log-level delivery for reporting (3.1) #2196 owning transport and record shape), rather than a boolean that can be misread as presence in the aggregate delivery response.
    3. spec(creative): implement preview event and macro observation sessions #6782 is not yet the probe for seller-added trackers. Its current v1 observation model identifies tracker_source only as a manifest asset and explicitly excludes trackers discovered inside fetched/assembled VAST, DAAST, or JavaScript. Seller-added disclosure therefore needs either an explicit seller-added/assembled source branch in spec(creative): implement preview event and macro observation sessions #6782 or a separate representation/readback surface; it does not become observable merely because spec(creative): implement preview event and macro observation sessions #6782 lands as currently scoped.

    The counting-source vocabulary should still be shared with #2041. Placement of billing-count methodology under measurement_terms versus general reporting posture remains the WG decision described above; these constraints apply either way.

  7. bokelley commented on Oct 6, 2026

    @bokelley
    Contributor

    Updated 2026-10-06: decision-record number is assigned at merge; several briefs had claimed DR-0023, so it is now shown as DR-XXXX.

    Updated 2026-10-06: For the WG — one question: Should one closed six-value measurement-source-type vocabulary (no census) serve both billing_measurement.counting_sources[] and row-level measurement_provenance? Everything else below proceeds as recommended unless someone objects. #2041 is closed as a duplicate of this issue (same PR, #7940); its September memo is superseded.

    WG decision brief (3.3)

    Problem. A seller can bill from its own count (billing_measurement.vendor can already name it), but nothing on the wire says what that count is built from. Variance disputes then turn on a cause nobody could see before spend (server-side logs vs begin-to-render beacons). #2041 is the report-time twin: a vendor number can be set-top-box, ACR, panel, or a fusion, and buyers must follow methodology_url to learn which. Pulling for it: @lukasz-pubx (#6878, CTV/SSAI seller-side counting); broadcast measurement feedback from Greg (O-N-X) behind #2041, with VideoAmp/Nielsen-style fusion as the motivating case. #2041 is also tracked as WG-ready (draft DR in-thread).

    Proposed shape. Draft PR: #7940 (additive, minor, tests green). One shared closed vocabulary of collection mechanisms (set_top_box, acr, panel, server_logs, client_tracker, sdk) used twice. Parties stay in the BrandRef, so ad_server/publisher_logs collapse into server_logs.

    "billing_measurement": { "vendor": { "domain": "streamhaus.example" }, "counting_sources": ["server_logs"] }
    { "vendor": { "domain": "tunein-measurement.example" }, "metric_id": "tune_in_reach", "value": 1500000,
      "measurement_provenance": { "source_types": ["set_top_box", "acr"], "deduplicated": true } }

    Absence of either means undeclared. Both are honest-claim declarations enforced through the contract, not conformance probes (DR-0005 not engaged). DR-XXXX (proposed) records the ruling; the memo's DR-0020 number is taken.

    Already resolved with the protocol and product experts (no vote needed): source_types is a unique, order-insignificant set listing only sources that enter the value; counting_sources lists only sources that enter the billed count (cross-checks excluded); deduplicated absent = unspecified, and false must be serialized; party/ownership labels are not enum values; counting_sources also lands in canonical-measurement-terms.json; provenance is not a qualifier key.

    Open questions for the WG (each answerable yes/no or pick-one):

    1. Placement. counting_sources[] under billing_measurement (buyer-negotiable, seller can TERMS_REJECTED) or in reporting_capabilities (visible only)? Rec: billing_measurement. Keeps who-counts and how-counted on one term, and sellers keep rejection.
    2. Enum ownership. One enum named measurement-source-type.json, carried by feat(schema): experimental collection-mechanism vocabulary for counting_mechanisms and measurement_provenance #7940 and used by both issues, or this issue owns counting-source.json and Measurement source attribution on delivery reports #2041 imports it? Rec: the former. One PR lands both uses, so ownership is moot and the Measurement source attribution on delivery reports #2041 name stays.
    3. Closed vs open enum. Closed six values (additive minors later), or open with a custom/URI escape as in RFC: geo_cells / geo_cells_exclude targeting — hierarchical cell-grid systems as a declared geographic unit #5969 and feat(media-buy): add identifier-based place targeting #6093? Rec: closed. Adding a value is a minor, vendor nuance has methodology_url/breakdown, and an escape would add URI governance for six values.
    4. census. Drop it from the enum (draft as proposed in Measurement source attribution on delivery reports #2041 had it)? Rec: drop. It is a coverage claim, not a mechanism, so [server_logs, census] and [census] encode one count three ways, and set_top_box without panel already says "not projected". Values can never be removed once shipped; a dropped value can return later.
    5. Self-vendor rule. When billing_measurement.vendor is the seller (or its ad server), is counting_sources[] a conditional MUST or a SHOULD? Rec: SHOULD in 3.3, MUST via schema_required_when as a 4.0 candidate. A MUST is expressible (feat(schema): typed vendor_relationship and play-based coverage denominators for vendor metrics #7177 did it for vendor_relationship), but billing_measurement is optional, so a MUST invites omitting it instead of disclosing.
    6. Row-level provenance. measurement_provenance on each vendor_metric_values row (yes), or vendor-capability level only? Rec: row-level. A vendor's metrics draw on different sources (reach from STB+panel, tune-in from STB alone); costs one optional object. A vendor-level possible_source_types[] is a clean follow-up.
    7. deduplicated boolean. Keep it (true = across all listed feeds, false = simple union), or omit until a vendor needs household/person/device levels? Rec: keep; false is the one signal that overlap may double-count, and a deduplication_level can be added later because the object is open.
    8. Object openness. measurement_provenance open per DR-0009 (yes), or closed as drafted in-thread? Rec: open. It is a descriptor, not a join key, so DR-0019 Class A does not apply; unknown keys are ignorable.
    9. Field 3 and impression_id sequencing. Confirm yes/no: (a) seller_tracker_events[] is formally deferred to a separate issue after spec(creative): implement preview event and macro observation sessions #6782 grows a seller-added/assembled-payload branch; (b) impression_id ships with Clean room and log-level delivery for reporting (3.1) #2196 (milestone 3.4.0), using possible_sources[] plus an explicit join_surfaces[] list, not minted_by or available_in_delivery. Rec: yes to both. No impression-level surface exists to name yet, and possible_sources[] alone would lock a second closed enum with no consumer.

    Out of scope. 3.4: impression_id (reporting_capabilities plus canonical-reporting-capabilities.json), a get_products filter on counting_sources, a deduplication_level, vendor-level possible_source_types[], report-time provenance for standard scalars like impressions, ssai_logs, sensor values for DOOH. Blocked on #6782: seller_tracker_events[]. 4.0: MUST for self-counting sellers.

  8. bokelley commented on Oct 31, 2026

    @bokelley
    Contributor

    Part of this shipped: #7940 (3.3.0-beta.2, experimental media_buy.measurement_provenance) added the collection-mechanism vocabulary, billing_measurement.counting_mechanisms[] and row-level vendor_metric_provenance[].

    Not shipped, so per the WG rule ('anything not decided moves to 3.4') only the remainder moves to 3.4: impression_id joinability (with #2196) and seller_tracker_events[] (blocked on #6782), plus the get_products filter on counting mechanisms. Objections by Fri 2026-11-06. The milestone stays 3.3.0 for now; it will be moved after Nov 6.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.media-buyIssue concerns the media-buy protocol domainneeds-wg-reviewBlocked on a working-group decision — surface in WG meeting agendasrfcProtocol change — auto-adds to roadmap boardschemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygiene

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions