Add IMPRESSION_ID universal macro for impression dedup#5020
Conversation
Closes the gap where TMP context-only impressions have no impression_id
available for cross-identity dedup (no TMPX → no decode-time mint).
Positions {IMPRESSION_ID} as a general-purpose impression key — used
by buyers, measurement vendors, verification services, and TMP — with
a three-layer minting hierarchy: publisher first-party code, then the
ad-decision layer (Prebid TMP module, ad server, SSP), then the buyer
at TMPX decode time as a TMP-specific fallback. Format is implementation
choice (UUID, ULID, snowflake, or equivalent). Documents the Prebid TMP
module pattern using `tmp_impression_id` targeting KV with optional
`enableTIDs` reuse. No router changes; preserves the identity↔context
structural separation by keeping minting at the publisher/decision-layer
join, not in TMPY or any new context-match response field.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
LGTM. Follow-ups noted below. The three-layer minting hierarchy is the right shape — publisher → decision layer → buyer-at-decode preserves TMP's identity↔context separation while closing the context-only gap that motivated the rejected TMPY proposal. Schema change is purely additive; description-map parity preserved.
Things I checked
- Schema change at
static/schemas/source/enums/universal-macro.json:62,134is additive only — enum array andenumDescriptionsmap both updated; no reorder, no field changes elsewhere. resolveImpressionId()reference adapter atspecs/prebid-tmp-proposal.md:421-440correctly propagates the value through both code paths — Phase 1 setsunit.tmpImpressionIdat line ~454; identity-bearing join at line ~502 emits it;applyContextOnlyat line ~526 emits it. No path silently drops the id.- "MUST NOT reuse TMPX nonce" clause at
docs/trusted-match/impression-tracker-implementation.mdx:33closes the obvious foot-gun — the nonce is per-IM-evaluation and would silently merge distinct impressions if reused. enableTIDs/transactionIdreuse optimization is correctly gated on the Prebid config flag (specs/prebid-tmp-proposal.md:432) — won't break whenenableTIDsis the opt-out default in PBJS 8+.- GAM substitution pattern
%%PATTERN:tmp_impression_id%%atdocs/trusted-match/surfaces/web.mdx:218is a real GAM substitution, not invented.
Follow-ups (non-blocking — fix in this PR or as issues)
- Broken anchor links. Two link sites point at
#tmp-impression-identificationbut the heading is### Impression Identification— slug is#impression-identification. Sites:docs/trusted-match/impression-tracker-implementation.mdx:33anddocs/trusted-match/surfaces/web.mdx:187. Either fix both anchors or rename the heading to### TMP Impression Identification(the latter would also match the sibling### TMP Exposure Trackingpattern in the same file). - Changeset bump. Prior universal-macro additions in
CHANGELOG.mdareminor— TMPX at line ~4384 and APP_ITEM_ID at line ~4893 both shipped under Minor sections. A new public enum value is a feature add that downstream SDK regeneration picks up. Bump.changeset/impression-id-universal-macro.md:2frompatchtominorfor consistency, or note in the PR body why this one is patch. - Oversized enum description. The new
IMPRESSION_IDdescription instatic/schemas/source/enums/universal-macro.json:134is ~1200 chars; peer entries are 5–200 chars (TMPX is ~190). This map surfaces in generated SDK tooltips and OpenAPI clients where multi-paragraph blobs render poorly. Trim to peer length; the prose page already carries the full hierarchy. - Rule #2 density.
docs/trusted-match/impression-tracker-implementation.mdx:33now packs source priority, buyer consumption rule, context-only failure mode, degraded-mode behavior, and the TMPX-nonce caveat into one ~250-word bullet. Split — the SHOULD clause currently gets buried. - Required-vs-SHOULD drift. Schema description and
docs/creative/universal-macros.mdx:181both sayIMPRESSION_IDis "Required for TMP context-only impressions".impression-tracker-implementation.mdx:33says trackersSHOULD treat its absence as an integration errorwith a fallback path. Producer-side MUST and consumer-side SHOULD are reconcilable but should be stated symmetrically.
Minor nits (non-blocking)
applyContextOnlyimplicit dependency.specs/prebid-tmp-proposal.md:526readsunit.tmpImpressionIdthat's set in Phase 1 at line ~454. A one-line comment in the function would keep a reader from reordering and silently producing undefined.
Two follow-ups would have been one if the heading slug had matched the link target — worth a docs-build link-checker in CI as a separate item.
Approving.
There was a problem hiding this comment.
Approving. Additive enum value, the wire surface is clean, and the three-layer minting hierarchy (publisher → decision-layer → buyer-at-decode) is structurally cleaner than the rejected TMPY blob — minting happens upstream of the identity↔context join, so the value is opaque-by-construction by the time it reaches a pixel.
Things I checked
static/schemas/source/enums/universal-macro.json:62and:134—enumandenumDescriptionsstay in parity, no oneOf surface touched, no router/error-code schema needs to land alongside..changeset/impression-id-universal-macro.md:2— frontmatter isminor. That's the right call for an additive enum value; the PR body's "patch-level" phrasing is just sloppy prose, the shipped artifact is correct.- Three-layer hierarchy reads consistently across
docs/creative/universal-macros.mdx,docs/trusted-match/impression-tracker-implementation.mdx,docs/trusted-match/surfaces/web.mdx, andspecs/prebid-tmp-proposal.md. "Lower layers MUST defer" is stated the same way in each. resolveImpressionId()is functionally equivalent indocs/trusted-match/surfaces/web.mdx:201andspecs/prebid-tmp-proposal.md:434— one usespbjs.getConfig, the othergetGlobal().getConfig. Both idiomatic for their context.- Build/test plan: schemas + examples pass; the unchecked items in the PR test plan are CI and this review itself, not feature validation.
Follow-ups (non-blocking — file as issues)
- Rule #4 contradicts Rule #6.
docs/trusted-match/impression-tracker-implementation.mdx:35says the degraded fallback for missing{IMPRESSION_ID}on context-only is "skip the exposure write or fall back to a one-shot identifier per pixel fire." A one-shot-per-pixel-fire fallback directly violates rule #6 at:37— every retry mints a fresh id and double-counts. Tighten #4 to "skip the exposure write" only, or carve out an explicit exception for the degraded path. - Stale cross-reference in write-path rule #3.
docs/trusted-match/impression-tracker-implementation.mdx:113says "on context-only impressions, see rule #2 in the rules section above for the degraded-mode behavior." Rule #2 is the three-sources rule; the degraded-mode text is in rule #4. Re-point. - Pre-existing prose at
:79drifted under the rewrite. The bullet still reads "impression_id— generated at TMPX decode." Under the new hierarchy the value is more often minted upstream and consumed at decode. Worth a single edit so a reader following the table back to the rules doesn't get a contradiction. adcp_*convention claim doesn't match the KV name.docs/trusted-match/surfaces/web.mdx:223saystmp_impression_id"mirrors theadcp_*convention used elsewhere on this page" — but the page itself usesadcp_pkg/adcp_segat:153-154. Either rename toadcp_impression_idfor consistency, or fix the prose to say the KV deliberately uses atmp_*namespace.- MUST in a non-normative page. Rule #4 at
:35adds "Publishers and decision layers MUST include{IMPRESSION_ID}for TMP context-only impressions," but the page footnote at:37reaffirms the page is non-normative and points conformance atidentity-match-implementation.mdx. If the requirement is load-bearing, lift it onto the normative page; if not, downgrade to SHOULD here.
Minor nits (non-blocking)
- PR body vs. changeset type. The summary calls this a "patch-level changeset" but the file is
minor. The file is right; the PR description is wrong — worth fixing for log hygiene since the body is what gets read alongside the merged commit. The series of follow-up commits ("Address IMPRESSION_ID review follow-ups") apparently didn't reach the PR description itself.
Safe to merge once the contradiction in #1 is filed as a follow-up.
Summary
IMPRESSION_IDto the universal-macro enum as a general-purpose per-impression identifier — usable by buyers, measurement vendors, verification services for impression dedup, cross-vendor reconciliation, pixel-retry detection, and (in TMP) cross-identity exposure dedup.{TMPX}→ no buyer-side decode-time mint).{TMPX}decode (TMP-specific fallback). Format is implementation choice — UUID, ULID, snowflake, or equivalent.Why not TMPY
An earlier proposal added a
TMPYmacro carrying a buyer-minted opaque blob from the context-match response. Rejected because (a) it would have bridged TMP's structural identity↔context separation when rendered alongside{TMPX}on the same pixel, (b) it un-deprecates the AXEM pattern the protocol already moved away from, and (c) industry convention is for the ad-decision layer to mint and downstream trackers to consume via macro substitution. Publisher/decision-layer mint via{IMPRESSION_ID}solves the underlying need without any of those costs.Files changed
static/schemas/source/enums/universal-macro.json— addsIMPRESSION_IDto enum + description mapdocs/creative/universal-macros.mdx— new### Impression Identificationsection with use cases, format flexibility, three-source hierarchydocs/trusted-match/impression-tracker-implementation.mdx— rule Welcome to adcp Discussions! #1 (format-agnostic), rule Remove targeting_template and add pluggable product catalog system #2 (three valid sources), updated write-path to handle context-only pixelsdocs/trusted-match/surfaces/web.mdx— new### Impression ID Substitutionsubsection withtmp_impression_idGAM targeting KV,enableTIDsreuse, ULID code examplespecs/prebid-tmp-proposal.md— three-layerresolveImpressionId()in prose and reference adapter.changeset/impression-id-universal-macro.md— patch-level changesetTest plan
npm run build:schemassucceeds,IMPRESSION_IDlands indist/schemas/latest/enums/universal-macro.jsonnpm run test:schemas— 8/8 passnpm run test:examples— 36/36 pass🤖 Generated with Claude Code