Skip to content

Generalize frequency rules from scalar package_id+campaign_id to arbitrary fcap_keys[] #104

@bokelley

Description

@bokelley

Background

The AdCP TMP spec (adcp#3359) defines frequency caps using an fcap_keys[] label model — tenant:dimension:value strings (e.g. buyer-acme:campaign:42, buyer-acme:advertiser:13, buyer-acme:creative:8) — with arbitrary buyer-chosen dimensions.

The current adcp-go/targeting/ impl predates this design and uses scalar fields:

  • ExposureEntry { ImpressionID, PackageID, CampaignID, SourceID, Timestamp }
  • PackageIdentityConfig.FrequencyRules (per package)
  • CampaignFreqConfig.FrequencyRules (per campaign)
  • CheckFrequencyRulesMultiLog(logs, filterHash, isCampaign, ...) — boolean isCampaign flag picks between two scalar paths

This means today the impl supports exactly package-level and campaign-level caps; advertiser, creative, line-item, group, or any other dimension a buyer might want to cap on isn't expressible.

What needs to change

Generalize the data model so the dimension is arbitrary:

  1. ExposureEntry: carry fcap_keys []string (or pre-hashed fcap_keys [][8]byte for binary log compactness) instead of separate PackageID / CampaignID fields. Or keep PackageID/CampaignID for backward compat and add fcap_keys as an additional field.
  2. FrequencyRule: attach to a fcap_key string rather than implicitly to a package or campaign.
  3. Engine: filter exposure entries by fcap_key membership in the entry's fcap_keys[], not by scalar match.
  4. Binary log format: exposure_binary.go currently has fixed slots for ImpressionHash / PackageHash / CampaignHash / SourceHash. Either widen entries to variable-size (carry a count + N fcap_key hashes) or version-bump to a different layout. Backward compat plan: v1 = current, v2 = arbitrary fcap_keys, decoder handles both.
  5. CheckFrequencyRulesMultiLog / CheckFrequencyRulesAggregated: drop the isCampaign bool parameter, take a single filterHash that matches against any fcap_key on the entry.

Backward compatibility

Existing callers using package_id and campaign_id should continue to work. Suggested approach:

  • Keep PackageID / CampaignID fields on ExposureEntry (deprecate but don't remove)
  • Add FcapKeys []string
  • During the transition, treat PackageID and CampaignID as implicit fcap_keys (pkg:<id> and campaign:<id>) so existing rules keep working
  • New rules attached via fcap_key directly

Why this matters

Without the generalization, buyers can only cap at package and campaign level. The whole point of the label model (per the spec) is that buyers choose dimensions — most DSPs need advertiser-level, creative-level, and flight-level caps. Today buyers running adcp-go's targeting/ package can't express those.

Out of scope for this issue

  • The pre-aggregation optimization in #103 — that's orthogonal and should land first; the generalization extends the same data structures it indexes.
  • Wire-spec changes — TMP itself doesn't carry fcap_keys on the wire; they're buyer-internal. No protocol coordination needed.
  • Multi-tenant isolation rules (tenant prefix requirement) — those are buyer-side enforcement that lives in the SDK, not in targeting/.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions