-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Problem
GAM supports creative-level targeting via LineItemCreativeAssociation (LICA) with targetingName, but we currently don't use this feature. This means:
- All creatives in a line item get the same targeting - can't show different creatives to different audiences
- No language-specific creative delivery - can't show English creative to English users and Spanish creative to Spanish users in the same line item
- Limited flexibility - forces creation of multiple line items for simple creative variations
What GAM Supports (But We Don't Use)
GAM allows targeting at two levels:
Line Item Level (Currently Used)
line_item = {
"targeting": {
"geoTargeting": {...}, # Applies to ALL creatives
"browserLanguageTargeting": {...} # Applies to ALL creatives
}
}Creative Level (NOT Currently Used)
line_item = {
"creativeTargetings": [
{
"name": "english_targeting",
"targeting": {
"browserLanguageTargeting": {
"isTargeted": True,
"browserLanguages": [{"id": 1000}] # English
}
}
},
{
"name": "spanish_targeting",
"targeting": {
"browserLanguageTargeting": {
"isTargeted": True,
"browserLanguages": [{"id": 1003}] # Spanish
}
}
}
],
"creativePlaceholders": [
{"size": {"width": 728, "height": 90}, "targetingName": "english_targeting"},
{"size": {"width": 728, "height": 90}, "targetingName": "spanish_targeting"}
]
}
# Then associate creatives with targeting
lica_english = {
"lineItemId": line_item_id,
"creativeId": english_creative_id,
"targetingName": "english_targeting" # Links to creativeTargeting
}
lica_spanish = {
"lineItemId": line_item_id,
"creativeId": spanish_creative_id,
"targetingName": "spanish_creative"
}Use Cases
- Multi-language campaigns - Show language-appropriate creative based on browser language
- Geo-specific creatives - Show different creatives to US vs Canada within same campaign
- Device-specific creatives - Show mobile-optimized creative to mobile users
- A/B testing - Test different creatives against different audience segments
Blocked By: AdCP Spec Extension
Current AdCP spec (v2.4) only supports:
- Line item-level targeting via
TargetingOverlay - Package-level targeting (which becomes line item targeting)
- No creative-level targeting specification
Proposed AdCP Extension
Option 1: Add to CreativeAsset
```python
class CreativeAsset:
creative_id: str
# ... existing fields ...
targeting: TargetingOverlay | None = None # NEW: Creative-specific targeting
```
Option 2: Add to Package (map creative_id → targeting)
```python
class MediaPackage:
# ... existing fields ...
creative_targeting: dict[str, TargetingOverlay] | None = None # NEW: Map creative_id to targeting
```
Option 3: New field on TargetingOverlay for language
```python
class TargetingOverlay:
# ... existing fields ...
language_any_of: list[str] | None = None # NEW: ISO 639-1 codes (en, es, fr, etc.)
language_none_of: list[str] | None = None
```
Next Steps for AdCP
- Propose to AdCP Working Group - Present use case and proposed schema
- Get consensus - Agreement on schema design (Option 1, 2, or 3)
- Publish AdCP v2.5 - Include creative-level targeting
- Implement in this codebase - Once spec is finalized
Implementation Plan (After AdCP Spec Updated)
Phase 1: Language Targeting Infrastructure
1.1 Add Language Lookup Service
# src/adapters/gam/managers/language.py
class GAMLanguageManager:
"""Manages language lookup and mapping for GAM."""
def get_available_languages(self) -> list[dict]:
"""Query GAM API for available browser languages."""
def iso_to_gam_id(self, iso_code: str) -> int | None:
"""Map ISO 639-1 language code to GAM language ID.
Examples:
"en" -> 1000
"es" -> 1003
"fr" -> 1001
"""1.2 Add Language ID Configuration
# In tenant settings or product config
"language_id_map": {
"en": 1000,
"es": 1003,
"fr": 1001,
"de": 1002,
# ... pre-configured for common languages
}1.3 Extend Targeting Builder for Language
# In src/adapters/gam/managers/targeting.py::build_targeting()
if targeting_overlay.language_any_of:
browser_languages = []
for iso_code in targeting_overlay.language_any_of:
gam_id = language_manager.iso_to_gam_id(iso_code)
if gam_id:
browser_languages.append({"id": gam_id})
else:
raise ValueError(f"Language '{iso_code}' not supported in GAM network")
gam_targeting["browserLanguageTargeting"] = {
"isTargeted": True,
"browserLanguages": browser_languages
}Phase 2: Creative-Level Targeting Core
2.1 Extend LineItem Creation
# In src/adapters/gam/managers/orders.py::create_line_items()
def create_line_items(
self,
...,
creative_level_targeting: dict[str, dict] = None # NEW parameter
):
"""Create line items with optional creative-level targeting.
Args:
creative_level_targeting: Map targeting names to targeting configs
Example:
{
"english_targeting": {
"browserLanguageTargeting": {
"isTargeted": True,
"browserLanguages": [{"id": 1000}]
}
},
"spanish_targeting": {...}
}
"""
line_item = {...}
# Add creative targetings if provided
if creative_level_targeting:
line_item["creativeTargetings"] = []
for targeting_name, targeting_config in creative_level_targeting.items():
line_item["creativeTargetings"].append({
"name": targeting_name,
"targeting": targeting_config
})
# Update creative placeholders to reference targeting names
for placeholder in line_item["creativePlaceholders"]:
# Assign targetingName based on format/size/assignment logic
placeholder["targetingName"] = get_targeting_name_for_placeholder(placeholder)2.2 Extend LICA Creation
# In src/adapters/gam/managers/creatives.py::_associate_creative_with_line_items()
def _associate_creative_with_line_items(
self,
gam_creative_id: str,
asset: dict[str, Any],
line_item_map: dict[str, str],
lica_service,
targeting_name: str = None # NEW parameter
):
"""Associate creative with line items, optionally with creative-level targeting."""
association = {
"creativeId": gam_creative_id,
"lineItemId": line_item_id,
}
# Add targeting name if creative-level targeting is used
if targeting_name:
association["targetingName"] = targeting_name
lica_service.createLineItemCreativeAssociations([association])2.3 Add Targeting Name Generation
# In src/adapters/gam/utils/targeting.py (new file)
def generate_targeting_name(creative_id: str, targeting: TargetingOverlay) -> str:
"""Generate unique targeting name for creative-level targeting.
Examples:
Creative with English language targeting:
-> "creative_abc123_lang_en"
Creative with geo + language targeting:
-> "creative_abc123_geo_us_lang_en"
"""
parts = [f"creative_{creative_id}"]
if targeting.language_any_of:
lang_codes = "_".join(sorted(targeting.language_any_of))
parts.append(f"lang_{lang_codes}")
if targeting.geo_country_any_of:
countries = "_".join(sorted(targeting.geo_country_any_of))
parts.append(f"geo_{countries}")
return "_".join(parts)Phase 3: AdCP Integration
3.1 Parse Creative-Level Targeting from AdCP Request
# In src/core/main.py::_create_media_buy_impl()
# After AdCP spec updated to support creative targeting
for package in packages:
creative_level_targeting = {}
# Option 1: From CreativeAsset.targeting
for asset in assets:
if asset.targeting:
targeting_name = generate_targeting_name(asset.creative_id, asset.targeting)
gam_targeting = targeting_manager.build_targeting(asset.targeting)
creative_level_targeting[targeting_name] = gam_targeting
# Option 2: From Package.creative_targeting
if package.creative_targeting:
for creative_id, targeting in package.creative_targeting.items():
targeting_name = generate_targeting_name(creative_id, targeting)
gam_targeting = targeting_manager.build_targeting(targeting)
creative_level_targeting[targeting_name] = gam_targeting
# Pass to line item creation
line_item_ids = orders_manager.create_line_items(
...,
creative_level_targeting=creative_level_targeting
)3.2 Map Creative Assets to Targeting Names
# When associating creatives, determine targeting name
for asset in assets:
gam_creative_id = create_creative(asset)
# Determine targeting name from asset
if asset.targeting: # Option 1
targeting_name = generate_targeting_name(asset.creative_id, asset.targeting)
elif package.creative_targeting and asset.creative_id in package.creative_targeting: # Option 2
targeting = package.creative_targeting[asset.creative_id]
targeting_name = generate_targeting_name(asset.creative_id, targeting)
else:
targeting_name = None # No creative-level targeting
# Associate with targeting name
associate_creative_with_line_items(
gam_creative_id,
asset,
line_item_map,
lica_service,
targeting_name=targeting_name
)Phase 4: Validation & Error Handling
4.1 Validate Creative Targeting Compatibility
def validate_creative_targeting(
line_item_targeting: dict,
creative_targeting: dict
) -> list[str]:
"""Validate creative targeting is compatible with line item targeting.
GAM Rule: Creative targeting must be a subset of or consistent with line item targeting.
Example Invalid:
Line item targets: US only
Creative targets: Canada <- ERROR: Creative targeting outside line item scope
Example Valid:
Line item targets: US + Canada
Creative 1 targets: US <- OK: Subset of line item
Creative 2 targets: Canada <- OK: Subset of line item
"""
errors = []
# Validate geo targeting
# Validate language targeting
# Validate other targeting dimensions
return errors4.2 Handle Networks Without Creative Targeting
# Check if feature is enabled in GAM network
try:
line_item = create_line_item_with_creative_targeting(...)
except Exception as e:
if "creative targeting" in str(e).lower():
raise ValueError(
"Creative-level targeting is not enabled in your GAM network. "
"Contact Google support to enable this feature or create separate line items "
"for each targeting variant."
)
raisePhase 5: Testing
5.1 Unit Tests
def test_language_targeting_name_generation():
"""Test targeting name generation for language targeting."""
targeting = TargetingOverlay(language_any_of=["en", "es"])
name = generate_targeting_name("creative_123", targeting)
assert name == "creative_123_lang_en_es"
def test_creative_targeting_builds_correctly():
"""Test GAM creative targeting structure."""
creative_targeting = {
"english_targeting": {
"browserLanguageTargeting": {
"isTargeted": True,
"browserLanguages": [{"id": 1000}]
}
}
}
line_item = build_line_item_with_creative_targeting(
...,
creative_level_targeting=creative_targeting
)
assert "creativeTargetings" in line_item
assert len(line_item["creativeTargetings"]) == 1
assert line_item["creativeTargetings"][0]["name"] == "english_targeting"5.2 Integration Tests
@pytest.mark.integration
def test_multi_language_creative_delivery(gam_client):
"""Test creating line item with language-specific creatives."""
# Create line item with creative-level language targeting
line_item_id = create_media_buy(
packages=[{
"name": "Multi-language Package",
"creative_targeting": {
"creative_en": TargetingOverlay(language_any_of=["en"]),
"creative_es": TargetingOverlay(language_any_of=["es"])
}
}],
assets=[
{"creative_id": "creative_en", "name": "English Ad"},
{"creative_id": "creative_es", "name": "Spanish Ad"}
]
)
# Verify LICAs have correct targeting names
licas = gam_client.get_line_item_creative_associations(line_item_id)
assert len(licas) == 2
english_lica = next(l for l in licas if l["creativeId"] == "creative_en_gam_id")
assert english_lica["targetingName"] == "creative_creative_en_lang_en"
spanish_lica = next(l for l in licas if l["creativeId"] == "creative_es_gam_id")
assert spanish_lica["targetingName"] == "creative_creative_es_lang_es"5.3 E2E Tests
@pytest.mark.e2e
def test_language_targeting_end_to_end():
"""Test complete flow from AdCP request to GAM delivery."""
# Send AdCP create_media_buy request with language targeting
response = create_media_buy(
packages=[{
"package_id": "multi_lang_pkg",
"creative_targeting": {
"creative_123": {
"language_any_of": ["en"]
},
"creative_456": {
"language_any_of": ["es"]
}
}
}],
creative_assets=[...]
)
# Verify media buy created successfully
assert response.status == "success"
# Verify GAM line item has creative targetings
# Verify LICAs have correct targeting names
# Verify creatives serve to correct language usersDocumentation Updates
- Update
docs/gam-creative-level-targeting.mdwith final implementation - Update
docs/gam-targeting-capabilities.mdwith language targeting examples - Add API documentation for new parameters
- Add user guide for multi-language campaigns
- Update AdCP compliance documentation
Success Criteria
- Can create line item with multiple
creativeTargetings - Can associate creatives with specific targeting names via LICA
- Language targeting works (English creative to English users, Spanish to Spanish)
- Other targeting types work at creative level (geo, device, etc.)
- Validation prevents incompatible creative targeting
- Clear error messages when feature not enabled in GAM network
- All tests passing (unit, integration, e2e)
- Documentation complete and accurate
Timeline
- Blocked: Waiting for AdCP spec update
- Phase 1-2: 2 weeks (language infrastructure + core creative targeting)
- Phase 3: 1 week (AdCP integration)
- Phase 4: 1 week (validation & error handling)
- Phase 5: 1 week (testing)
- Total: ~5 weeks after AdCP spec finalized
References
- GAM Creative-Level Targeting Documentation
- GAM Targeting Capabilities Reference
- GAM API: LineItemCreativeAssociation
- GAM API: CreativeTargeting
- AdCP Specification v2.4
Related Issues
- #TBD - Add device type targeting to GAM adapter
- #TBD - Add browser/OS targeting to GAM adapter
- #TBD - Propose AdCP v2.5 spec extension for creative-level targeting