feat(compliance): per_creative_attribution capability bit + scenario (closes #4725)#4731
Merged
Merged
Conversation
…loses #4725) Lands the deferred per-creative conversion attribution work from #4642 as a single PR with two coupled changes: - New boolean `media_buy.conversion_tracking.per_creative_attribution` capability bit in get-adcp-capabilities-response.json. Optional; defaults to false. Declares that the seller can attribute conversions per creative within a package and surface them via media_buy_deliveries[].by_package[].by_creative[].conversions. - New scenario media_buy_seller/per_creative_conversion_attribution, gated on the new bit, added to sales-non-guaranteed.requires_scenarios. Registers two creatives via sync_creatives, creates a media buy assigning both to one package with a CPA goal, logs two conversion events, simulates delivery, and asserts the by_creative[] breakdown is populated for both creatives (creative_id + conversions on rows 0 and 1) — the second-row assertion is the asymmetry check that separates honest per-creative attribution from a single-row façade. Closes the gap deliberately left by performance_buy_flow (#4642), whose narrative defers per-creative attribution because honest adopters report at differing granularities: social per-ad, retail-media per-line, MMP-mediated per-campaign, broadcast/CTV per-placement. Requiring per-creative in the base CPA scenario would have failed those honest implementations. The bit gates the scenario; sellers that don't advertise it grade not_applicable. log_event's payload (core/event.json) does not carry creative_id — attributing each event to a specific creative is the seller's internal click / view-through correlation responsibility, not the buyer's. The scenario logs two events with distinct event_ids and relies on the seller's correlation to spread simulate_delivery's conversions across the two assigned creatives in the by_creative[] breakdown. No training-agent changes — the reference implementation does not declare per_creative_attribution, so the scenario grades not_applicable and CI passes. Same anti-façade pattern as event_dedup_flow (#4664) and frequency_cap_enforcement (#4640). Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta), #4642 (performance_buy_flow that deferred this), #4639 (supported_targets bit for the sibling ROAS gate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands the deferred per-creative conversion attribution work from #4642 as one PR with two coupled changes:
media_buy.conversion_tracking.per_creative_attributioninstatic/schemas/source/protocol/get-adcp-capabilities-response.json. Optional; defaults tofalse. Declares that the seller can attribute conversions per creative within a package and surface them viamedia_buy_deliveries[].by_package[].by_creative[].conversions.media_buy_seller/per_creative_conversion_attribution, gated onrequires_capability: { path: media_buy.conversion_tracking.per_creative_attribution, equals: true }, added tosales-non-guaranteed.requires_scenarios(alphabetic position betweenpending_creatives_to_startandperformance_buy_flow).What was deferred from #4642 and why
performance_buy_flow(#4642) deliberately omits per-creative assertions because honest adopters report attribution at differing granularities — social platforms per-ad, retail-media networks (Criteo, Amazon Ads) per-line, MMP-mediated mobile (post-iOS-14) per-campaign / per-ad-set, broadcast and CTV performance products per-placement. Requiring per-creative in the base CPA scenario would have failed those honest implementations. The scenario's narrative explicitly tees this follow-up up. This PR closes that gap behind a sub-capability bit so the population is correctly partitioned into has the breakdown (asserted) vs does not (gradednot_applicable).What the new scenario asserts
Five phases:
sync_accounts+get_productswithrequired_capabilities: ["conversion_tracking"].sync_event_sourcesregistering a website source, withupstream_trafficanti-façade.sync_creativespushingcreative_acme_1andcreative_acme_2(display_300x250), thencreate_media_buywith one package whosecreative_assignmentsreferences both and whoseoptimization_goals[0]is event-kind withtarget.kind: cost_per.log_eventcalls with distinctevent_ids.core/event.jsondoes not carrycreative_id, so attributing each event back to a specific creative is the seller's internal click / view-through correlation, not the buyer's. The seller spreadssimulate_delivery'sconversionsacross the two assigned creatives per its own correlation.simulate_deliverywith impressions + conversions + spend, thenget_media_buy_delivery, asserting:field_presentonmedia_buy_deliveries[0].totals.conversionsfield_presentonmedia_buy_deliveries[0].by_package[0].by_creative[0].creative_idfield_presentonmedia_buy_deliveries[0].by_package[0].by_creative[0].conversions— the discriminating assertionfield_presentonmedia_buy_deliveries[0].by_package[0].by_creative[1].conversions— the asymmetry check that separates honest per-creative attribution from a single-row façadeTraining-agent status
No training-agent changes. The reference implementation does not declare
per_creative_attribution, so the scenario gradesnot_applicableand CI passes. Same anti-façade pattern asevent_dedup_flow(#4664) andfrequency_cap_enforcement(#4640): the bit gates the scenario, the assertion targets the runtime behavior the bit commits to.Test plan
npm run build:schemas— cleannpm run build:compliance— all 12 lints greennpm run typecheck— no new errors (pre-existing failures inserver/src/training-agent/*andserver/src/types.tsfor unrelated@adcp/sdkmodule resolution, confirmed againstorigin/main)not_applicableagainst the training agent (noper_creative_attributionadvertised)Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta), #4642 (performance_buy_flow that deferred this), #4639 (supported_targets bit for the sibling ROAS gate), #4664 (event_dedup_flow precedent), #4640 (frequency_cap_enforcement precedent).
🤖 Generated with Claude Code