Android 版 Protected Audience API 開發人員指南

詳閱 Android 版 Privacy Sandbox 說明文件時,請利用「開發人員預覽版」或「Beta 版」按鈕選取您要使用的計畫版本,因為兩者的操作說明可能不盡相同。


提供意見

Android 版 Protected Audience API (舊稱 FLEDGE) 包含 Custom Audience API 和 Ad Selection API。廣告技術平台和廣告主可利用這些 API,根據先前的應用程式參與情形放送自訂廣告,藉此限制不同應用程式間的 ID 分享,並限制與第三方分享使用者的應用程式互動資訊。

Custom Audience API 以「自訂目標對象」抽象層為中心,代表有共通意圖的使用者群組。廣告主可以向自訂目標對象登錄使用者,並為相關廣告建立關聯。此資訊會儲存在本機中,並且可用以提供廣告主出價、廣告篩選和廣告顯示的相關資訊。

Ad Selection API 提供的架構可讓多名開發人員針對自訂目標對象在本機上進行競價。因此,系統會考慮與自訂目標對象相關的廣告,並針對廣告技術平台傳回裝置的廣告進行額外的處理。

廣告技術平台可以整合這些 API 以實作再行銷,藉此保護使用者隱私。我們計劃在未來的版本中提供更多用途的支援 (包括應用程式安裝廣告)。如要進一步瞭解 Android 版 Protected Audience API,請參閱設計提案

本指南將說明如何使用 Android 版 Protected Audience API 執行下列操作:

  1. 管理自訂目標對象
  2. 在裝置上設定及執行廣告選擇
  3. 回報廣告曝光

事前準備

在開始之前,請先完成下列操作:

  1. 在 Android 裝置上為 Privacy Sandbox 設定開發環境
  2. 將系統映像檔安裝到支援的裝置上,或者設定支援 Android 版 Privacy Sandbox 的模擬器
  3. 在終端機中,使用以下 ADB 指令啟用 Protected Audience API 的存取權 (預設為停用)。

      adb shell device_config put adservices ppapi_app_allow_list \"*\"
    
  4. 在應用程式資訊清單中加入 ACCESS_ADSERVICES_CUSTOM_AUDIENCE 權限:

      <uses-permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE" />
    
  5. 在資訊清單的 <application> 元素中參照廣告服務設定:

      <property android:name="android.adservices.AD_SERVICES_CONFIG"
                android:resource="@xml/ad_services_config" />
    
  6. 指定資訊清單所參照的廣告服務 XML 資源,例如 res/xml/ad_services_config.xml進一步瞭解廣告服務權限和 SDK 存取權控管

      <ad-services-config>
        <custom-audiences allowAllToAccess="true" />
      </ad-services-config>
    
  7. 根據預設,Ad Selection API 會限制競價或曝光報表指令碼可分配的記憶體最大容量。記憶體限制功能需要搭配使用 WebView 105.0.5195.58 以上版本。平台會強制執行版本檢查,如不符合上述版本條件,就無法呼叫 selectAdsreportImpression API。您可以透過以下兩種做法進行設定:

    • 做法 1:執行下列 ADB 指令以停用這項檢查:

      adb device_config put fledge_js_isolate_enforce_max_heap_size false
      
    • 做法 2:從 Google Play 商店安裝 WebView Beta 版。這個版本必須等於或高於上述版本。

加入自訂目標對象

「自訂目標對象」代表由廣告主應用程式決定,且具有共同意圖或興趣的使用者。應用程式或 SDK 可以使用自訂目標對象來表示特定目標對象,例如曾將商品放入購物車的使用者。如要以非同步的方式建立或加入自訂目標對象,請按照下列步驟操作:

  1. CustomAudienceManager 物件初始化。
  2. 指定買家套件和相關名稱等重要參數以建立 CustomAudience 物件。然後,使用 CustomAudience 物件初始化 JoinCustomAudienceRequest 物件。
  3. 使用 JoinCustomAudienceRequest 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 joinCustomAudience()

Kotlin

val customAudienceManager: CustomAudienceManager =
    context.getSystemService(CustomAudienceManager::class.java)

// Initialize a custom audience.
val audience = CustomAudience.Builder()
    .setBuyer(buyer)
    .setName(name)
    ...
    .build()

// Initialize a custom audience request.
val joinCustomAudienceRequest: JoinCustomAudienceRequest =
    JoinCustomAudienceRequest.Builder().setCustomAudience(audience).build()

// Request to join a custom audience.
customAudienceManager.joinCustomAudience(joinCustomAudienceRequest,
    executor,
    outcomeReceiver)

Java

CustomAudienceManager customAudienceManager =
    context.getSystemService(CustomAudienceManager.class);

// Initialize a custom audience.
CustomAudience audience = new CustomAudience.Builder()
    .setBuyer(buyer)
    .setName(name)
    ...
    .build();

// Initialize a custom audience request.
JoinCustomAudienceRequest joinCustomAudienceRequest =
    new JoinCustomAudienceRequest.Builder().setCustomAudience(audience).build();

// Request to join a custom audience.
customAudienceManager.joinCustomAudience(joinCustomAudienceRequest,
    executor,
    outcomeReceiver);

下列參數的組合可明確識別裝置上的每個 CustomAudience 物件:

  • owner:擁有者應用程式的套件名稱。以隱含形式設定為呼叫端應用程式的套件名稱。
  • buyer:管理此自訂目標對象廣告的買家廣告聯播網 ID。
  • name:自訂目標對象的任意名稱或 ID。

使用 CustomAudience 的不同執行個體重複呼叫 joinCustomAudience(),將以比對符合的 owner, buyer,以及 name 參數更新任何現有的 CustomAudience。為保護隱私,API 的結果不會區分「建立」和「更新」。

此外,建立 CustomAudience 時必須使用以下必要參數:

  • 每日更新網址每天在背景中查詢的 HTTPS 網址,可更新自訂目標對象的使用者出價信號、受信任的出價資料,以及廣告的顯示網址和中繼資料。
  • 出價邏輯網址:在廣告選擇期間查詢的 HTTPS 網址,以擷取買家的 JavaScript 出價邏輯。請參閱此 JavaScript 中所需的函式簽章
  • 廣告顯示 ID:買方廣告技術設定的任意 ID。這是針對 B&A 產生酬載的最佳化作業

CustomAudience 物件的選用參數可能包括:

  • 啟用時間:自訂目標對象只能於啟用時間過後參與廣告選擇和每日更新。舉例來說,您可以利用此方式吸引很久沒有使用應用程式的使用者進行互動。
  • 到期時間:自訂目標對象從裝置中移除的未來時間。
  • 使用者出價信號:包含使用者信號 (例如使用者偏好語言) 的 JSON 字串,廣告選擇程序期間,買家的出價邏輯 JavaScript 將會用以產生出價。此格式可協助廣告技術平台在多個平台之間重複使用程式碼,並減少 JavaScript 函式的使用量。
  • 受信任的出價資料:廣告選擇程序期間使用的 HTTPS 網址和字串清單,可從受信任的鍵/值服務擷取出價信號。
  • 廣告AdData 物件清單,對應至參與廣告選擇的廣告。每個 AdData 物件都由以下項目組成:
    • 顯示網址:要查詢以顯示最終廣告的 HTTPS 網址。
    • 中繼資料:已序列化為字串的 JSON 物件,包含買方出價邏輯在廣告選擇程序中需使用的資訊。
    • 廣告篩選條件:此類別包含在廣告選擇期間套用應用程式安裝廣告篩選功能與展示頻率上限的所有必要資訊。

以下是 CustomAudience 物件例項化的範例:

Kotlin

// Minimal initialization of a CustomAudience object
val customAudience: CustomAudience = CustomAudience.Builder()
    .setBuyer(AdTechIdentifier.fromString("my.buyer.domain.name"))
    .setName("example-custom-audience-name")
    .setDailyUpdateUrl(Uri.parse("https://DAILY_UPDATE_URL"))
    .setBiddingLogicUrl(Uri.parse("https://BIDDING_LOGIC_URL"))
    .build()

Java

// Minimal initialization of a CustomAudience object
CustomAudience customAudience = CustomAudience.Builder()
    .setBuyer(AdTechIdentifier.fromString("my.buyer.domain.name"))
    .setName("example-custom-audience-name")
    .setDailyUpdateUrl(Uri.parse("https://DAILY_UPDATE_URL"))
    .setBiddingLogicUrl(Uri.parse("https://BIDDING_LOGIC_URL"))
    .build();

處理 joinCustomAudience() 結果

非同步 joinCustomAudience() 方法會使用 OutcomeReceiver 物件以指出 API 呼叫的結果。

  • onResult() 回呼代表自訂目標對象已成功建立或更新。
  • onError() 回呼代表兩種可能的條件。

以下是處理 joinCustomAudience() 結果的範例:

Kotlin

var callback: OutcomeReceiver<Void, AdServicesException> =
    object : OutcomeReceiver<Void, AdServicesException> {
    override fun onResult(result: Void) {
        Log.i("CustomAudience", "Completed joinCustomAudience")
    }

    override fun onError(error: AdServicesException) {
        // Handle error
        Log.e("CustomAudience", "Error executing joinCustomAudience", error)
    }
};

Java

OutcomeReceiver callback = new OutcomeReceiver<Void, AdServicesException>() {
    @Override
    public void onResult(@NonNull Void result) {
        Log.i("CustomAudience", "Completed joinCustomAudience");
    }

    @Override
    public void onError(@NonNull AdServicesException error) {
        // Handle error
        Log.e("CustomAudience", "Error executing joinCustomAudience", error);
    }
};

退出自訂目標對象

如果使用者不再滿足特定自訂目標對象的業務條件,應用程式或 SDK 可呼叫 leaveCustomAudience(),從裝置中移除自訂目標對象。如要根據專用參數移除特定的 CustomAudience,請執行下列步驟:

  1. CustomAudienceManager 物件初始化。
  2. 使用自訂目標對象的 buyername 初始化 LeaveCustomAudienceRequest。如要進一步瞭解這些輸入欄位,請參閱「加入自訂目標對象」。
  3. 使用 LeaveCustomAudienceRequest 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 leaveCustomAudience() 方法。

Kotlin

val customAudienceManager: CustomAudienceManager =
    context.getSystemService(CustomAudienceManager::class.java)

// Initialize a LeaveCustomAudienceRequest
val leaveCustomAudienceRequest: LeaveCustomAudienceRequest =
    LeaveCustomAudienceRequest.Builder()
        .setBuyer(buyer)
        .setName(name)
        .build()

// Request to leave a custom audience
customAudienceManager.leaveCustomAudience(
    leaveCustomAudienceRequest,
    executor,
    outcomeReceiver)

Java

CustomAudienceManager customAudienceManager =
    context.getSystemService(CustomAudienceManager.class);

// Initialize a LeaveCustomAudienceRequest
LeaveCustomAudienceRequest leaveCustomAudienceRequest =
    new LeaveCustomAudienceRequest.Builder()
        .setBuyer(buyer)
        .setName(name)
        .build();

// Request to leave a custom audience
customAudienceManager.leaveCustomAudience(
    leaveCustomAudienceRequest,
    executor,
    outcomeReceiver);

與呼叫 joinCustomAudience() 類似,OutcomeReceiver 會發出 API 呼叫已結束的通知。為保護隱私,錯誤結果不會區分內部錯誤和無效引數。API 呼叫完成時,不論是否成功移除相符的自訂目標對象,系統都會呼叫 onResult() 回呼。

執行廣告選擇

如要使用 Protected Audience API 選擇廣告,請呼叫 selectAds() 方法:

  1. 初始化 AdSelectionManager 物件。
  2. 建構 AdSelectionConfig 物件。
  3. 使用 AdSelectionConfig 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 selectAds()

Kotlin

val adSelectionManager: AdSelectionManager =
  context.getSystemService(AdSelectionManager::class.java)

// Initialize AdSelectionConfig
val adSelectionConfig: AdSelectionConfig =
  AdSelectionConfig.Builder().setSeller(seller)
    .setDecisionLogicUrl(decisionLogicUrl)
    .setCustomAudienceBuyers(customAudienceBuyers)
    .setAdSelectionSignals(adSelectionSignals)
    .setSellerSignals(sellerSignals)
    .setPerBuyerSignals(perBuyerSignals)
    .setBuyerContextualAds(
      Collections.singletonMap(
        contextualAds.getBuyer(), contextualAds
      )
    ).build()

// Run ad selection with AdSelectionConfig
adSelectionManager.selectAds(
  adSelectionConfig, executor, outcomeReceiver
)

Java

AdSelectionManager adSelectionManager =
    context.getSystemService(AdSelectionManager.class);

// Initialize AdSelectionConfig
AdSelectionConfig adSelectionConfig =
  new AdSelectionConfig.Builder()
    .setSeller(seller)
    .setDecisionLogicUrl(decisionLogicUrl)
    .setCustomAudienceBuyers(customAudienceBuyers)
    .setAdSelectionSignals(adSelectionSignals)
    .setSellerSignals(sellerSignals)
    .setPerBuyerSignals(perBuyerSignals)
    .setBuyerContextualAds(
      Collections.singletonMap(contextualAds.getBuyer(), contextualAds)
    )
    .build();

// Run ad selection with AdSelectionConfig
adSelectionManager.selectAds(adSelectionConfig, executor, outcomeReceiver);

selectAds() 方法需要 AdSelectionConfig 輸入內容,您必須在其中指定以下必要參數:

  • 賣方:賣方廣告聯播網 ID,這用於指出啟動廣告選擇程序的賣方廣告聯播網。
  • 決策邏輯網址:為取得賣方廣告聯播網的 JavaScript 邏輯而查詢的 HTTPS 網址。
    • HTTPS 網址:為取得賣方廣告聯播網的 JavaScript 邏輯而查詢的網址。請參閱必要的函式簽章
    • 預先建立的 URI:依循 FLEDGE 廣告選擇格式。如果傳遞不受支援或格式錯誤的預先建構 URI,會擲回 IllegalArgumentException
  • 自訂目標對象買方:買方廣告聯播網的完整 ID 清單,皆獲得賣方允許,可參與廣告選擇程序。這些買方 ID 會對應至參與廣告選擇的自訂目標對象的 CustomAudience.getBuyer()

您可以選擇指定下列參數,進一步自訂廣告選擇程序:

  • 廣告選擇信號:已序列化為字串的 JSON 物件,包含 CustomAudience.getBiddingLogicUrl() 中買家出價邏輯 JavaScript 所使用的信號。
  • 賣家信號:序列化為字串的 JSON 物件,包含 AdSelectionConfig.getDecisionLogicUrl() 中賣家擷取 JavaScript 決策邏輯所使用的信號。
  • 個別買方信號:已序列化為字串的 JSON 物件對應,包含從 CustomAudience.getBiddingLogicUrl() 擷取的特定買方出價邏輯 JavaScript 所使用的信號,且辨識信號的方法為使用參與廣告選擇的自訂目標對象的買方欄位。
  • 內容相關廣告:在 Protected Audience 競價以外的競價期間,直接向買方收集的候選廣告集合。

選擇廣告之後,系統會在內部保留結果、出價和信號,用於製作報表。OutcomeReceiver.onResult() 回呼會傳回包含以下內容的 AdSelectionOutcome

  • AdData.getRenderUrl() 取得的得標廣告顯示網址。
  • 裝置使用者專屬的廣告選擇 ID。這個 ID 的作用是回報廣告曝光。

如果廣告選擇程序因為引數無效、逾時或資源消耗過多等因素而未能順利完成,OutcomeReceiver.onError() 回呼就會提供 AdServicesException 以指出下列行為:

  • 如果用於啟動廣告選擇程序的引數無效,AdServicesException 會指出 IllegalArgumentException 是錯誤原因。
  • 發生其他錯誤時,收到的 AdServicesException 則會指出 IllegalStateException 是錯誤原因。

內容相關廣告

Protected Audience 可將內容相關廣告整合至 Protected 競價。系統需在廣告技術伺服器上選取及簽署內容相關廣告,然後傳回至 Protected Audience API 外的裝置,接著就能透過 AdSelectionConfig 將內容相關廣告納入競價;假如這些廣告的簽章經過驗證,廣告在競價中的運作方式就會與裝置端廣告相同,包括是否符合依廣告篩選條件排除的資格也是一樣。而當完成 Protected Audience 競價後,您需要叫用 reportImpression()。這會在得標的內容相關廣告中呼叫 reportWin(),以便在裝置上接收得標廣告。呼叫模式會與曝光報表的模式相同。每個內容相關廣告都需要買方、出價、導向回報邏輯的連結、顯示網址和廣告中繼資料。

如要在應用程式中部署內容相關廣告,目標應用程式需建立 SignedContextualAds 物件:

Kotlin

val signedContextualAds: SignedContextualAds =
  Builder().setBuyer(AdTechIdentifier.fromString(mBiddingLogicUri.getHost()))
    //Pass in your valid app install ads
    .setDecisionLogicUri(mContextualLogicUri)
    .setAdsWithBid(appInstallAd)
    .setSignature(signature)
    .build()

Java

SignedContextualAds signedContextualAds = new SignedContextualAds.Builder()
  .setBuyer(AdTechIdentifier.fromString(mBiddingLogicUri.getHost()))
  .setDecisionLogicUri(mContextualLogicUri)
  //Pass in your valid app install ads
  .setAdsWithBid(appInstallAd)
  .setSignature(signature)
  .build();

您可以在建立 AdSelectionConfig 時一併傳遞產生的 SignedContextualAds 物件:

Kotlin

val adSelectionConfig = AdSelectionConfig.Builder()
  .setPerBuyerSignedContextualAds(signedContextualAds)
  // Other fields for your config
  .build()

Java

AdSelectionConfig adSelectionConfig = new AdSelectionConfig.Builder()
    .setPerBuyerSignedContextualAds(signedContextualAds)
    //Other fields for your config
    .build();

應用程式安裝廣告篩選功能

應用程式安裝廣告篩選功能可針對裝置上已安裝的應用程式篩選安裝廣告。

此程序的第一步是定義哪些廣告主可篩選已安裝的套件。這項作業需要在利用廣告指定的應用程式中進行。

Kotlin

//Create a request for setting the app install advertisers
val adtech = AdTechIdentifier.fromString("your.enrolled.uri")
val adtechSet = setOf(adtech)
val request = SetAppInstallAdvertisersRequest(adtechSet)

//Set the app install advertisers in the ad selection manager
mAdSelectionManager.setAppInstallAdvertisers(
  request,
  mExecutor,
  object : OutcomeReceiver<Any?, Exception?>() {
    fun onResult(@NonNull ignoredResult: Any?) {
      Log.v("[your tag]", "Updated app install advertisers")
    }

    fun onError(@NonNull error: Exception?) {
      Log.e("[your tag]", "Failed to update app install advertisers", error)
    }
  })

Java

//Create a request for setting the app install advertisers
AdTechIdentifier adtech = AdTechIdentifier.fromString("your.enrolled.uri");
Set<AdTechIdentifier> adtechSet = Collections.singleton(adtech);
SetAppInstallAdvertisersRequest request = new SetAppInstallAdvertisersRequest(adtechSet);

//Set the app install advertisers in the ad selection manager
mAdSelectionManager.setAppInstallAdvertisers(
  request,
  mExecutor,
  new OutcomeReceiver<Object, Exception>() {
    @Override
    public void onResult(@NonNull Object ignoredResult) {
      Log.v("[your tag]", "Updated app install advertisers");
    }

    @Override
    public void onError(@NonNull Exception error) {
      Log.e("[your tag]", "Failed to update app install advertisers", error);
    }
  });

執行上述程式碼時,傳入的廣告主可篩除您在出價產生期間指定的已安裝應用程式。如果您需要移除廣告主對此應用程式安裝狀態的存取權,請移除該廣告主的資訊,然後再次執行這段程式碼。

下一步是在發布商應用程式內設定廣告篩選功能。在發布商應用程式中放送廣告的一方 (最有可能是供應端 SDK) 必須初始化 AdFilters 物件,並提供資訊,指出要篩除哪些應用程式相關廣告:

Kotlin

// Instantiate AdFilters object with package names.
val filters: AdFilters = Builder().setAppInstallFilters(
    Builder().setPackageNames(setOf("example.target.app")).build()
  ).build()

Java

// Instantiate AdFilters object with package names.
AdFilters filters = new AdFilters.Builder()
.setAppInstallFilters(
  new AppInstallFilters.Builder()
  .setPackageNames(Collections.singleton("example.target.app"))
  .build())
.build();

需求端發布商也可以為自訂目標對象內的廣告設定 AdFilter

AdFilters 也可在新 AdData 物件例項化時傳入:

Kotlin

// Instantiate an AdData object with the AdFilters created in the
// previous example.
val appInstallAd: AdData =
  Builder().setMetadata("{ ... }") // Valid JSON string
    .setRenderUri(Uri.parse("www.example-dsp1.com/.../campaign123.html"))
    .setAdFilters(filters).build()

Java

// Instantiate an AdData object with the AdFilters created in the
// previous example.
AdData appInstallAd = new AdData.Builder()
.setMetadata("{ ... }") // Valid JSON string
.setRenderUri(Uri.parse("www.example-dsp1.com/.../campaign123.html"))
    .setAdFilters(filters)
    .build();

展示頻率上限篩選功能

展示頻率上限篩選功能可讓廣告技術人員限制廣告的顯示次數。此功能可以降低廣告過度曝光率,並針對特定廣告活動選擇最佳的備用廣告。

展示頻率上限篩選器內含兩個主要元件:廣告事件類型和廣告計數器鍵。可用的廣告事件類型如下:

  • 勝出:勝出事件表示廣告贏得競價。勝出事件會自動由 Protected Audience API 更新,且無法讓開發人員直接呼叫。勝出資料只會向特定自訂目標對象中的廣告顯示。
  • 曝光:與 reportImpression 不同,裝置端的呼叫端 (賣方平台或 MMP) 會使用 updateAdCounterHistogram() 在所選程式碼的特定位置叫用曝光事件。曝光事件會向隸屬於特定 DSP 的所有廣告顯示,不僅限於同一個自訂目標對象的廣告。
  • 觀看:裝置端的呼叫端 (賣方平台或 MMP) 會使用對 updateAdCounterHistogram() 的呼叫,在所選程式碼的某個位置叫用事件。觀看事件會向隸屬於特定 DSP 的所有廣告顯示,不僅限於同一個自訂目標對象的廣告。
  • 點擊:裝置端的呼叫端 (賣方平台或 MMP) 會使用對 updateAdCounterHistogram() 的呼叫,在所選程式碼的某個位置叫用事件。點擊事件會向隸屬於特定 DSP 的所有廣告顯示,不僅限於同一個自訂目標對象的廣告。

在發布商應用程式中,位於裝置上的賣方平台或 MMP 會叫用廣告事件。當系統呼叫 updateAdCounterHistogram() 時,展示頻率上限篩選器的計數器會逐步遞增,好讓日後競價獲得使用者對特定廣告的最新曝光率資訊。廣告事件類型不一定要與對應的使用者動作有關,這只是一套協助呼叫端自行建構事件系統的規範。為了在事件發生時增加廣告計數器,裝置端操作者會提供勝出廣告競價的廣告選擇 ID。

廣告計數器鍵是由買家廣告技術指派的任意 32 位元帶正負號整數,對應於 DSP 定義的一組特定廣告。由於廣告計數器鍵僅對應隸屬於特定 DSP 的廣告,因此您可選取這些鍵,而不必擔心與其他廣告技術的直方圖重疊。廣告計數器鍵可用於在 DSP 的廣告或特定自訂目標對象中增加 DSP 專屬 ID,以便從未來的競價中篩除廣告。

計數器鍵還可用於在指定買家廣告技術提供其他廣告後,根據特定使用者與這些廣告的互動情形,優先放送他們更可能感興趣的廣告。舉例來說,如果廣告透過勝出的廣告競價、觀看和點擊事件而提高參與度,即可做為推斷的資料點。為了進一步說明這個概念,我們舉左手高爾夫球桿的廣告為例。這類廣告可能暗示使用者不會對右手球桿感興趣。此時若為指派給左手桿廣告的計數器鍵設定展示頻率上限篩選器,可能就會篩除右手球桿的廣告。

如要在競價中使用展示頻率上限,您必須先建立 KeyedFrequencyCap 物件,如下所示:

Kotlin

// Value used when incrementing frequency counter
val adCounterKey = 123

// Frequency cap exceeded after 2 counts
val keyedFrequencyCapForImpression: KeyedFrequencyCap = Builder(
  adCounterKey, 2, Duration.ofSeconds(10)
).build()

// Frequency cap exceeded after 1 counts
val keyedFrequencyCapForImpression: KeyedFrequencyCap = Builder(
  adCounterKey, 1, Duration.ofSeconds(10)
).build()

Java

// Value used when incrementing frequency counter
int adCounterKey = 123;

// Frequency cap exceeded after 2 counts
KeyedFrequencyCap keyedFrequencyCapForImpression =
  new KeyedFrequencyCap.Builder(
    adCounterKey, 2, Duration.ofSeconds(10)
  ).build();

// Frequency Cap exceeded after 1 counts
KeyedFrequencyCap keyedFrequencyCapForClick =
  new KeyedFrequencyCap.Builder(
    adCounterKey, 1, Duration.ofSeconds(10)
  ).build();

建立 KeyedFrequencyCap 物件後,您可以將這些物件傳遞至 AdFilters 物件中。

Kotlin

val filters: AdFilters = Builder()
  .setFrequencyCapFilters(
    Builder()
      .setKeyedFrequencyCapsForImpressionEvents(
        ImmutableObject.of(keyedFrequencyCapForImpression)
      )
      .setKeyedFrequencyCapsForClickEvents(
        ImmutableObject.of(keyedFrequencyCapForClick)
      )
  ).build()

Java

AdFilters filters = new AdFilters.Builder()
    .setFrequencyCapFilters(new FrequencyCapFilters.Builder()
        .setKeyedFrequencyCapsForImpressionEvents(
            ImmutableObject.of(keyedFrequencyCapForImpression)
        )
        .setKeyedFrequencyCapsForClickEvents(
            ImmutableObject.of(keyedFrequencyCapForClick)
        )
    ).build();

AdFilters 物件填入展示頻率上限篩選器後,系統會在建立自訂目標對象時一併傳遞該物件:

Kotlin

// Initialize a custom audience.
val audience: CustomAudience = Builder()
  .setBuyer(buyer)
  .setName(name)
  .setAds(
    listOf(
      Builder()
        .setRenderUri(renderUri)
        .setMetadata(JSONObject().toString())
        .setAdFilters(filters)
        .setAdCounterKeys(adCounterKeys)
        .build()
    )
  ).build()

Java

// Initialize a custom audience.
CustomAudience audience = new CustomAudience.Builder()
    .setBuyer(buyer)
    .setName(name)
    .setAds(Collections.singletonList(new AdData.Builder()
        .setRenderUri(renderUri)
        .setMetadata(new JSONObject().toString())
        .setAdFilters(filters)
        .setAdCounterKeys(adCounterKeys)
        .build()))
    .build();

為自訂目標對象實作展示頻率上限篩選器後,賣方平台就可以叫用必要的點擊、觀看或曝光事件。

Kotlin

val callerAdTech: AdTechIdentifier = mAdSelectionConfig.getSeller()

val request: UpdateAdCounterHistogramRequest = Builder(
  adSelectionId,
  FrequencyCapFilters.AD_EVENT_TYPE_CLICK,  //CLICK, VIEW, or IMPRESSION
  callerAdTech
).build()

Java

AdTechIdentifier callerAdTech = mAdSelectionConfig.getSeller();

UpdateAdCounterHistogramRequest request =
  new UpdateAdCounterHistogramRequest.Builder(
      adSelectionId,
      FrequencyCapFilters.AD_EVENT_TYPE_CLICK, //CLICK, VIEW, or IMPRESSION
      callerAdTech
).build();

如果廣告達到預設的展示頻率上限篩選器限制,就會從競價中篩除。篩選作業發生的時間點,是在裝置端競價執行出價邏輯之前,以及在為出價和競價服務競價產生酬載時。藉由這個工具包,廣告技術可靈活運用使用者與自訂目標對象中廣告的互動情形,在盡量降低廣告過度曝光的同時,也可將重點鎖定在指定廣告目標。

如何建立簽章

簽章演算法和金鑰產生功能

請使用 ECDSA 演算法簽署廣告。金鑰類型應為 P-256 ECDSA (又稱「secp256r1」或「prime256v1」)。以下舉例說明不同語言的簽署方式:

Java

import java.security.*;

public class ECDSASignatureManager {
    private static KeyPair generateKeyPair() throws Exception {
        // Specify the curve name P-256
        ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");

        // Initialize the KeyPairGenerator
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
        keyPairGenerator.initialize(ecSpec);

        // Generate the KeyPair
        return keyPairGenerator.generateKeyPair();
    }

    public byte[] sign(byte[] data, byte[] privateKey) throws Exception {
        Signature ecdsa = Signature.getInstance("SHA256withECDSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        PrivateKey aPrivateKey = keyFactory.generatePrivate(keySpec);

        ecdsa.initSign(aPrivateKey);
        ecdsa.update(data);

        return ecdsa.sign();
    }
}

JavaScript

const crypto = require('crypto');

function generateKeyPair() {
  const keyPair = crypto.generateKeyPairSync('ec', {
    // Specifies ECDSA algorithm
    namedCurve: 'prime256v1',
    publicKeyEncoding: { type: 'spki', format: 'der' },
    privateKeyEncoding: { type: 'pkcs8', format: 'der'}
  });

  return {
    publicKey: keyPair.publicKey,
    privateKey: keyPair.privateKey
  };
}

function signContextualAds(data, privateKey) {
  // Convert the private key from PKCS8 to PEM format
  const base64PrivateKey = Buffer.from(privateKey).toString('base64');
  const pemPrivateKey = `-----BEGIN PRIVATE KEY-----\n${base64PrivateKey}\n-----END PRIVATE KEY-----`;

  // Create a Sign object using SHA256
  const sign = crypto.createSign('SHA256');
  sign.update(Buffer.from(data));

  // Sign the data with ECDSA as specified in the key specs
  return sign.sign(pemPrivateKey);
}

Python

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec

def sign_contextual_ads(data, private_key_der):
    private_key = serialization.load_der_private_key(
        private_key_der,
        password=None,
        backend=default_backend()
    )

    signature = private_key.sign(
        data,
        ec.ECDSA(hashes.SHA256())
    )

    return signature

def generate_key_pair():
    private_key = ec.generate_private_key(ec.SECP256R1())

    # Serialize the keys into DER format
    public_key_der = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )

    private_key_der = private_key.private_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )

    return public_key_der, private_key_der

序列化

由於簽章建立於買家的伺服器中,因此我們需要一組標準規則,才能確保特定 SignedContextualAds 的序列化程序在不同系統中保持一致。

標準規則

這組規則可確保廣告技術和 Protected Audience 之間的序列化程序保持一致。在開發指南中,這些規則會與不同語言的程式碼片段一併提供,方便您採用。

  • 物件:在每個欄位後方,以「|」(直立線符號) 將欄位的序列化值串連起來。為了方便起見,最後一個欄位也會有「|」,例如:「field_name_1=value1|field_name_2=value2|」
  • 物件欄位的序列化方式,是將欄位名稱 (採用蛇形命名法)、等號以及序列化的欄位值串連起來,例如「my_field=|」
  • 所有欄位都是按照物件中欄位名稱的字母順序排序
  • 如果可為空值 (選填) 的欄位為空值或未設定,系統會略過這些欄位
  • 倍數會轉換為精確值取到小數點後 2 位的字串
  • 整數會轉換為字串值
  • 組合會按照字母順序排序
  • 清單保留相同順序,以半形逗號分隔
  • 字串會以 UTF-8 編碼為 byte[]

Java

public class ContextualAdsSerializationUtil {
    private static final String FIELD_SEPARATOR = "|";
    private static final String ENUMERATOR_SEPARATOR = ",";

    private final ByteArrayOutputStream mByteArrayStream;

    public ContextualAdsSerializationUtil() {
        this.mByteArrayStream = new ByteArrayOutputStream();
    }

    byte[] getBytes() {
        return this.mByteArrayStream.toByteArray();
    }

    public byte[] serialize(ContextualAds ads) {
        writeContextualAds(ads);
        return getBytes();
    }

    private void writeContextualAds(ContextualAds ads) {
        writeString("buyer=");
        writeField(ads.getBuyer().getIdentifier(), this::writeString);

        writeString("decision_logic_uri=");
        writeField(ads.getDecisionLogicUri().toString(), this::writeString);

        writeString("ads_with_bid=");
        writeList(ads.getAdsWithBid(), this::writeAdWithBid);
    }

    private void writeAdWithBid(AdWithBid adWithBid) {
        writeString("ad_data=");
        writeField(adWithBid.getAdData(), this::writeAdData);

        writeString("bid=");
        writeField(adWithBid.getBid(), this::writeDouble);
    }

    private void writeAdData(AdData adData) {
        writeString("ad_counter_keys=");
        writeSet(adData.getAdCounterKeys(), this::writeInt);

        if (!Objects.isNull(adData.getAdFilters())) {
            writeString("ad_filters=");
            writeField(adData.getAdFilters(), this::writeAdFilters);
        }

        if (!Objects.isNull(adData.getAdRenderId())) {
            writeString("ad_render_id=");
            writeField(adData.getAdRenderId(), this::writeString);
        }

        writeString("metadata=");
        writeField(adData.getMetadata(), this::writeString);

        writeString("render_uri=");
        writeField(adData.getRenderUri().toString(), this::writeString);
    }

    private void writeAdFilters(AdFilters adFilters) {
        if (!Objects.isNull(adFilters.getAppInstallFilters())) {
            writeString("app_install_filters=");
            writeField(adFilters.getAppInstallFilters(), this::writeAppInstallFilter);
        }

        if (!Objects.isNull(adFilters.getFrequencyCapFilters())) {
            writeString("frequency_cap_filters=");
            writeField(adFilters.getFrequencyCapFilters(), this::writeFrequencyCapFilters);
        }
    }

    private void writeAppInstallFilter(AppInstallFilters appInstallFilters) {
        writeString("package_names=");
        writeSet(appInstallFilters.getPackageNames(), this::writeString);
    }

    private void writeFrequencyCapFilters(FrequencyCapFilters frequencyCapFilters) {
        writeString("keyed_frequency_caps_for_click_events=");
        writeList(
            frequencyCapFilters.getKeyedFrequencyCapsForClickEvents(),
            this::writeKeyedFrequencyCap);

        writeString("keyed_frequency_caps_for_impression_events=");
        writeList(
            frequencyCapFilters.getKeyedFrequencyCapsForImpressionEvents(),
            this::writeKeyedFrequencyCap);

        writeString("keyed_frequency_caps_for_view_events=");
        writeList(
            frequencyCapFilters.getKeyedFrequencyCapsForViewEvents(),
            this::writeKeyedFrequencyCap);

        writeString("keyed_frequency_caps_for_win_events=");
        writeList(
            frequencyCapFilters.getKeyedFrequencyCapsForWinEvents(),
            this::writeKeyedFrequencyCap);
    }

    private void writeKeyedFrequencyCap(KeyedFrequencyCap keyedFrequencyCap) {
        writeString("ad_counter_key=");
        writeField(keyedFrequencyCap.getAdCounterKey(), this::writeInt);

        writeString("interval=");
        writeField(keyedFrequencyCap.getInterval().toMillis(), this::writeLong);

        writeString("max_count=");
        writeField(keyedFrequencyCap.getMaxCount(), this::writeInt);
    }

    private <T> void writeField(T field, Consumer<T> writerConsumer) {
        Objects.requireNonNull(field);

        writerConsumer.accept(field);
        writeString(FIELD_SEPARATOR);
    }

    private <T> void writeList(List<T> list, Consumer<T> writerConsumer) {
        FirstElementStateHolder state = new FirstElementStateHolder();
        for (T t : list) {
            if (!state.isFirstThenSetFalse()) {
                writeString(ENUMERATOR_SEPARATOR);
            }
            writerConsumer.accept(t);
        }
        writeString(FIELD_SEPARATOR);
    }

    private <T> void writeSet(Set<T> set, Consumer<T> writerConsumer) {
        FirstElementStateHolder state = new FirstElementStateHolder();
        set.stream()
            .sorted()
            .forEach(
                (value) -> {
                    if (!state.isFirstThenSetFalse()) {
                        writeString(ENUMERATOR_SEPARATOR);
                    }
                    writerConsumer.accept(value);
                });
        writeString(FIELD_SEPARATOR);
    }

    void writeString(String value) {
        mByteArrayStream.writeBytes(value.getBytes(StandardCharsets.UTF_8));
    }

    void writeLong(long value) {
        writeString(Long.toString(value));
    }

    void writeDouble(double value) {
        writeString(String.format("%.2f", value));
    }

    void writeInt(int value) {
        writeString(Integer.toString(value));
    }

    private static class FirstElementStateHolder {

        private boolean mIsFirst = true;

        public boolean isFirstThenSetFalse() {
            boolean initialValue = mIsFirst;
            mIsFirst = false;
            return initialValue;
        }
    }
}

JavaScript

class SignedContextualAdsHashUtil {
  constructor() {
    this.serializedString = '';
  }

  serialize(ads) {
    this.writeContextualAds(ads);
    return new TextEncoder().encode(this.serializedString);
  }

  writeContextualAds(ads) {
    this.writeString('buyer=');
    this.writeField(ads.buyer.identifier, value => this.writeString(value));

    this.writeString('decision_logic_uri=');
    this.writeField(ads.decisionLogicUri, value => this.writeString(value));

    this.writeString('ads_with_bid=');
    this.writeList(ads.adsWithBid, adWithBid => this.writeAdWithBid(adWithBid));
  }

  writeAdWithBid(adWithBid) {
    this.writeString('ad_data=');
    this.writeField(adWithBid.adData, adData => this.writeAdData(adData));

    this.writeString('bid=');
    this.writeField(adWithBid.bid, value => this.writeDouble(value));
  }

  writeAdData(adData) {
    this.writeString('ad_counter_keys=');
    this.writeSet([...adData.adCounterKeys], value => this.writeInt(value));

    if (adData.adFilters) {
      this.writeString('ad_filters=');
      this.writeField(adData.adFilters, adFilters => this.writeAdFilters(adFilters));
    }

    if (adData.adRenderId) {
      this.writeString('ad_render_id=');
      this.writeField(adData.adRenderId, value => this.writeString(value));
    }

    this.writeString('metadata=');
    this.writeField(adData.metadata, value => this.writeString(value));

    this.writeString('render_uri=');
    this.writeField(adData.renderUri, value => this.writeString(value));
  }

  writeAdFilters(adFilters) {
    if (adFilters.appInstallFilters) {
      this.writeString('app_install_filters=');
      this.writeField(adFilters.appInstallFilters, appInstallFilters => this.writeAppInstallFilter(appInstallFilters));
    }

    if (adFilters.frequencyCapFilters) {
      this.writeString('frequency_cap_filters=');
      this.writeField(adFilters.frequencyCapFilters, frequencyCapFilters => this.writeFrequencyCapFilters(frequencyCapFilters));
    }
  }

  writeAppInstallFilter(appInstallFilters) {
    this.writeString('package_names=');
    this.writeSet([...appInstallFilters.packageNames], value => this.writeString(value));
  }

  writeFrequencyCapFilters(frequencyCapFilters) {
    this.writeString('keyed_frequency_caps_for_click_events=');
    this.writeList(frequencyCapFilters.keyedFrequencyCapsForClickEvents, cap => this.writeKeyedFrequencyCap(cap));

    this.writeString('keyed_frequency_caps_for_impression_events=');
    this.writeList(frequencyCapFilters.keyedFrequencyCapsForImpressionEvents, cap => this.writeKeyedFrequencyCap(cap));

    this.writeString('keyed_frequency_caps_for_view_events=');
    this.writeList(frequencyCapFilters.keyedFrequencyCapsForViewEvents, cap => this.writeKeyedFrequencyCap(cap));

    this.writeString('keyed_frequency_caps_for_win_events=');
    this.writeList(frequencyCapFilters.keyedFrequencyCapsForWinEvents, cap => this.writeKeyedFrequencyCap(cap));
  }

  writeKeyedFrequencyCap(cap) {
    this.writeString('ad_counter_key=');
    this.writeField(cap.adCounterKey, value => this.writeInt(value));

    this.writeString('interval=');
    this.writeField(cap.interval, value => this.writeLong(value));

    this.writeString('max_count=');
    this.writeField(cap.maxCount, value => this.writeInt(value));
  }

  writeField(field, writer) {
    writer(field);
    this.writeString('|');
  }

  writeList(list, writer) {
    list.forEach((item, index) => {
      if (index > 0) this.writeString(',');
      writer(item);
    });
    this.writeString('|');
  }

  writeSet(set, writer) {
    [...set].sort().forEach((item, index) => {
      if (index > 0) this.writeString(',');
      writer(item);
    });
    this.writeString('|');
  }

  writeString(value) {
    this.serializedString += value;
  }

  writeLong(value) {
    this.writeString(value.toString());
  }

  writeDouble(value) {
    this.writeString(value.toFixed(2));
  }

  writeInt(value) {
    this.writeString(value.toString());
  }
}

module.exports = {
  SignedContextualAdsHashUtil,
};

Python

class SignedContextualAdsHashUtil:
    def __init__(self):
        self.serialized_string = ''

    def serialize(self, ads):
        self.write_contextual_ads(ads)
        return self.serialized_string.encode('utf-8')

    def write_contextual_ads(self, ads):
        self.write_string('buyer=')
        self.write_field(ads.buyer.identifier, lambda v: self.write_string(v))

        self.write_string('decision_logic_uri=')
        self.write_field(ads.decision_logic_uri, lambda v: self.write_string(v))

        self.write_string('ads_with_bid=')
        self.write_list(ads.ads_with_bid, self.write_ad_with_bid)

    def write_ad_with_bid(self, ad_with_bid):
        self.write_string('ad_data=')
        self.write_field(ad_with_bid.ad_data, self.write_ad_data)

        self.write_string('bid=')
        self.write_field(ad_with_bid.bid, lambda v: self.write_double(v))

    def write_ad_data(self, ad_data):
        self.write_string('ad_counter_keys=')
        self.write_set(ad_data.ad_counter_keys, lambda v: self.write_int(v))

        if ad_data.ad_filters:
            self.write_string('ad_filters=')
            self.write_field(ad_data.ad_filters, self.write_ad_filters)

        if ad_data.ad_render_id:
            self.write_string('ad_render_id=')
            self.write_field(ad_data.ad_render_id, lambda v: self.write_string(v))

        self.write_string('metadata=')
        self.write_field(ad_data.metadata, lambda v: self.write_string(v))

        self.write_string('render_uri=')
        self.write_field(ad_data.render_uri, lambda v: self.write_string(v))

    def write_ad_filters(self, ad_filters):
        if ad_filters.app_install_filters:
            self.write_string('app_install_filters=')
            self.write_field(ad_filters.app_install_filters,
                            self.write_app_install_filter)

        if ad_filters.frequency_cap_filters:
            self.write_string('frequency_cap_filters=')
            self.write_field(ad_filters.frequency_cap_filters,
                            self.write_frequency_cap_filters)

    def write_app_install_filter(self, app_install_filters):
        self.write_string('package_names=')
        self.write_set(app_install_filters.package_names,
                      lambda v: self.write_string(v))

    def write_frequency_cap_filters(self, frequency_cap_filters):
        for attr in ['keyed_frequency_caps_for_click_events',
                    'keyed_frequency_caps_for_impression_events',
                    'keyed_frequency_caps_for_view_events',
                    'keyed_frequency_caps_for_win_events']:
            self.write_string(f'{attr}=')
            self.write_list(getattr(frequency_cap_filters, attr),
                            self.write_keyed_frequency_cap)

    def write_keyed_frequency_cap(self, cap):
        self.write_string('ad_counter_key=')
        self.write_field(cap.ad_counter_key, lambda v: self.write_int(v))

        self.write_string('interval=')
        self.write_field(cap.interval, lambda v: self.write_long(v))

        self.write_string('max_count=')
        self.write_field(cap.max_count, lambda v: self.write_int(v))

    def write_field(self, field, writer):
        writer(field)
        self.write_string('|')

    def write_list(self, list_items, writer):
        for index, item in enumerate(list_items):
            if index > 0:
                self.write_string(',')
            writer(item)
        self.write_string('|')

    def write_set(self, set_items, writer):
        for index, item in enumerate(sorted(set_items)):
            if index > 0:
                self.write_string(',')
            writer(item)
        self.write_string('|')

    def write_string(self, value):
        self.serialized_string += value

    def write_long(self, value):
        self.write_string(str(value))

    def write_double(self, value):
        self.write_string(f'{value:.2f}')

    def write_int(self, value):
        self.write_string(str(value))

示例

示例 1:含有全部欄位的內容相關廣告物件 假設您有下列內容相關廣告物件:

  ContextualAds contextualAds = new ContextualAds.Builder()
      .setBuyer(new AdTechIdentifier.Builder().setIdentifier("https://example.com").build()) // Replaced 'buyer' variable
      .setDecisionLogicUri(URI.create("example.com/decisionLogic")) // Replaced 'decisionLogicUri'
      .setAdsWithBid(
          List.of(
              new AdWithBid.Builder()
                  .setAdData(new AdData.Builder()
                      .setMetadata("metadata") // Replaced 'metadata'
                      .setAdFilters(buildAdFilters()) // Created a helper for filters
                      .setAdRenderId("987")  // Replaced 'adRenderId'
                      .setAdCounterKeys(buildAdCounterKeys()) // Created a helper
                      .setRenderUri(URI.create("example.com/render")) // Replaced 'adRenderUri'
                      .build())
                  .setBid(5.10) // Replaced 'bid' (as double)
                  .build()))
      .build();

  // Helper function to build FrequencyCapFilters
  private static FrequencyCapFilters buildAdFilters() {
      KeyedFrequencyCap keyedFrequencyCap = new KeyedFrequencyCap.Builder()
              .setAdCounterKey(42) // Replaced 'adCounterKey'
              .setMaxCount(43) // Replaced 'maxCount'
              .setInterval(Duration.ofDays(1)) // Replaced 'oneDay'
              .build();

    return new FrequencyCapFilters.Builder()
            // ... set all KeyedFrequencyCaps using keyedFrequencyCap ...
            .build();
  }

  // Helper function to convert adCounterKeys string
  private static Set<Integer> buildAdCounterKeys() {
      return Arrays.stream("1,2,3".split(",")) // Replaced 'adCounterKeys'
              .map(Integer::valueOf)
              .collect(Collectors.toSet());
  }

序列化 byte[] 的字串應如下所示:

    "buyer=https://example.com|decision_logic_uri=example.com/decisionLogic|ads_with_bid=ad_data=ad_counter_keys=1,2,3|ad_filters=app_install_filters=package_names=com.awesome.app1,com.awesome.app2||frequency_cap_filters=keyed_frequency_caps_for_click_events=ad_counter_key=42|interval=86400000|max_count=43||keyed_frequency_caps_for_impression_events=ad_counter_key=42|interval=86400000|max_count=43||keyed_frequency_caps_for_view_events=ad_counter_key=42|interval=86400000|max_count=43||keyed_frequency_caps_for_win_events=ad_counter_key=42|interval=86400000|max_count=43||||ad_render_id=987|metadata=metadata|render_uri=example.com/render||bid=5.10||"

示例 2:含有空白欄位的內容廣告物件 在下列物件中,「adFilters」和「adCounterKeys」為空白,分別是「nullable」和「nonnull」。

  ContextualAds contextualAds = new ContextualAds.Builder()
            .setBuyer(new AdTechIdentifier.Builder().setIdentifier("https://example.com").build())
            .setDecisionLogicUri(URI.create("example.com/decisionLogic"))
            .setAdsWithBid(
                List.of(
                    new AdWithBid.Builder()
                        .setAdData(new AdData.Builder()
                            .setMetadata("metadata")
                            .setAdRenderId("987")
                            .setRenderUri(URI.create("example.com/render"))
                            .build())
                        .setBid(5.10)
                        .build()))
            .build();

如以下序列化字串所示,如果可為空值的欄位為空值 (或完全不設定),該欄位會省略序列化程序。但如果沒有設定的是 NonNull 欄位,則該欄位會納入序列化程序中,並以空字串做為值。

  "buyer=https://example.com|decision_logic_uri=example.com/decisionLogic|ads_with_bid=ad_data=ad_counter_keys=|ad_render_id=987|metadata=metadata|render_uri=example.com/render||bid=5.10||"

沒有網路呼叫的內容相關廣告篩選功能

如果沒有在裝置上進行再行銷的需求,您可以對內容相關廣告執行廣告選擇功能,無需使用網路呼叫。透過預先建構的 URI 和含有出價的內容相關廣告清單,平台可以略過擷取出價邏輯、出價信號和評分信號的程序。平台會利用預先建構的 URI 選取出價最高的內容相關廣告。

為了縮短延遲時間,廣告技術可以使用廣告篩選功能,執行僅含內容相關廣告的廣告選擇流程,無需使用網路呼叫,方法是使用預先建構的 URI 取得評分信號。如需 scoreAds 實作項目的清單,請參閱「支援的預建 URI 用途和名稱」一節。

如何執行廣告選擇功能,但不使用網路呼叫:

  1. 設定廣告篩選功能
  2. 建立內容相關廣告
  3. 請使用以下項目建立 AdSelectionConfig 物件:

    1. 空白買家清單
    2. 用於選取最高出價的預先建構 URI
    3. 內容相關廣告
    4. 評分信號的空白 URI。空白 URI 可指出您不想透過擷取受信任的信號來進行評分:
    Uri prebuiltURIScoringUri = Uri.parse("ad-selection-prebuilt://ad-selection/highest-bid-wins/?reportingUrl=your.registered.uri/reporting");
    // Initialize AdSelectionConfig
    AdSelectionConfig adSelectionConfig =
      new AdSelectionConfig.Builder()
        .setSeller(seller)
        .setDecisionLogicUri(prebuiltURIScoringUri)
        .setCustomAudienceBuyers(Collections.emptyList())
        .setAdSelectionSignals(adSelectionSignals)
        .setSellerSignals(sellerSignals)
        .setPerBuyerSignals(perBuyerSignals)
        .setBuyerContextualAds(buyerContextualAds)
        .setTrustedScoringSignalsUri(Uri.EMPTY)
        .build();
    
  4. 執行廣告選擇:

    adSelectionManager.selectAds(
        adSelectionConfig,
        executor,
        outcomeReceiver);
    

使用預先建構的 URI 自行執行報表 JavaScript

目前,Privacy Sandbox 平台只有基本報表的 JavaScript 實作項目,供預先建構的 URI 使用。如果您想執行自己的報表 JavaScript,但仍要使用預先建構的 URI 來縮短廣告選擇時的延遲時間,可以覆寫廣告選擇和報表執行間的 DecisionLogicUri

  1. 執行相關步驟,使用預先建構的 URI 針對內容相關廣告執行廣告選擇
  2. 執行報表之前,請建立 AdSelectionConfig 的副本

    adSelectionConfigWithYourReportingJS = adSelectionConfig.cloneToBuilder()
      // Replace <urlToFetchYourReportingJS> with your own URL:
      .setDecisionLogicUri(Uri.parse(<urlToFetchYourReportingJS>))
      .build();
    
  3. 執行曝光報表

    // adSelectionId is from the result of the previous selectAds run
    ReportImpressionRequest request = new ReportImpressionRequest(
      adSelectionId,
      adSelectionConfigWithYourReportingJS);
    adSelectionManager.reportImpression(
      request,
      executor,
      outcomeReceiver);
    

執行刊登序列中介服務

刊登序列中介服務需要讓多個第三方 SDK (第三方聯播網) 接受第一方 SDK 中介服務聯播網的調度。無論競價是在裝置上進行,還是在出價與競價服務 (B&A) 上執行,刊登序列中介服務的做法都相同'。

第三方聯播網

第三方聯播網必須提供轉接程式,讓中介服務聯播網能夠叫用執行競價需要的方法:

  • 執行廣告選擇
  • 回報曝光

中介服務聯播網轉接程式示例如下:

Kotlin

class NetworkAdaptor {
    private val adSelectionManager : AdSelectionManager

    init {
        adSelectionManager = context.getSystemService(AdSelectionManager::class.java)
    }

    fun selectAds() {...}

    fun reportImpressions() {...}
}

Java

class NetworkAdaptor {
    AdSelectionManager adSelectionManager;

    public NetworkAdaptor() {
        AdSelectionManager adSelectionManager =
            context.getSystemService(AdSelectionManager.class);
    }

    public void selectAds() {...}

    public void reportImpressions() {...}
}

每個 SDK 都有專屬的廣告選擇服務管理工具和用戶端,以及自己的 selectAdsreportImpressions 實作項目。SDK 供應商可以參閱裝置端競價的「執行廣告選擇」章節,如想瞭解 B&A 競價,可以參閱「B&A 說明」。請按照「單一賣方平台曝光報表」的說明回報廣告曝光

中介服務聯播網

中介服務聯播網和第三方聯播網一樣需要導入 selectAdsreportImpression。如需瞭解詳情,請參閱介紹如何「執行廣告選擇」和「回報廣告曝光」的章節。

中介服務聯播網負責執行中介服務鏈,也會將自身置於中介服務鏈中。下一節將說明此程序的設定與執行做法。

擷取中介服務鏈和出價下限

中介服務聯播網會負責擷取第一方的內容相關廣告、中介服務鏈和第三方聯播網的出價下限。每當有人要求擷取中介服務聯播網提供的內容相關廣告,中介服務聯播網就可能採取上述行動。中介服務鏈會決定逐一查看第三方聯播網的方式,出價下限則能以 adSelectionSignals 的形式傳遞至競價程序。

聯播網在中介服務鏈中的排名

中介服務 SDK 可以根據第一方廣告出價的即時有效千次曝光出價,決定自身在中介服務鏈中的排名。Protected Audience API 中的廣告出價是不透明的。中介服務 SDK 應使用 AdSelectionFromOutcomesConfig,才能將指定第一方廣告的出價與服務鏈中下一個第三方聯播網的出價下限進行比較。如果第一方出價高於第三方出價下限,中介服務 SDK 的排名就會在第三方聯播網前面。

執行廣告選擇

中介服務聯播網在擷取第一方候選廣告時,可以按照「執行廣告選擇」一節的步驟進行裝置端競價,從而產生第一方候選廣告、出價和中介服務程序所用的 AdSelectionId

建立 AdSelectionFromResultsConfig

AdSelectionFromOutcomesConfig 可讓中介服務聯播網傳遞 AdSelectionIds 清單 (先前競價的結果)、廣告選擇信號及 URI,以便根據 URI 擷取 JavaScript,再利用 JavaScript 從多個候選廣告中選出得標廣告。這份列有 AdSelectionId 和相應出價的清單會跟信號一起傳遞至 JavaScript,如果當中有高於出價下限的出價,JavaScript 就會傳回其中一個 AdSelectionIds;如果中介服務鏈應繼續執行,則不會傳回內容。

中介服務聯播網在建立 AdSelectionFromOutcomesConfig 時,會使用上一節提到的第一方 AdSelectionId,並參考第三方聯播網的出價下限。您應針對中介服務鏈中的每個步驟建立新的 AdSelectionFromOutcomesConfig

Kotlin

fun  runSelectOutcome(
    adSelectionClient : AdSelectionClient,
    outcome1p : AdSelectionOutcome,
    network3p : NetworkAdapter) : ListenableFuture<AdSelectionOutcome?> {
    val config = AdSelectionFromOutcomesConfig.Builder()
        .setSeller(seller)
        .setAdSelectionIds(listOf(outcome1p))
        .setSelectionSignals({"bid_floor": bid_floor})
        .setSelectionLogicUri(selectionLogicUri)
        .build()
    return adSelectionClient.selectAds(config)
}

Java

public ListenableFuture<AdSelectionOutcome> runSelectOutcome(AdSelectionOutcome outcome1p,
                                              NetworkAdapter network3p) {
    AdSelectionFromOutcomesConfig config = new AdSelectionFromOutcomesConfig.Builder()
            .setSeller(seller)
            .setAdSelectionIds(Collection.singletonList(outcome1p))
            .setSelectionSignals({"bid_floor": bid_floor})
            .setSelectionLogicUri(selectionLogicUri)
            .build();

    return adSelectionClient.selectAds(config){}
}

刊登序列中介服務的 selectAds() 方法覆寫需要 AdSelectionFromOutcomesConfig 輸入內容,當中須指定下列必要參數:

  • 賣方:賣方廣告聯播網 ID,這用於指出啟動廣告選擇程序的賣方廣告聯播網。
  • AdSelectionId:之前針對第一方廣告執行的 selectAds() 的單例模式清單。
  • 廣告選擇信號:序列化為字串的 JSON 物件,含有買方出價邏輯所用的信號。上例中也加入了針對指定第三方聯播網擷取的出價下限。
  • 選擇邏輯 URI:在廣告選擇期間查詢的 HTTPS 網址。中介服務聯播網會從這個網址擷取用來選出得標廣告的 JavaScript。請留意這個 JavaScript 必須包含的函式簽章。JavaScript 應於出價高於出價下限時傳回第三方廣告,並在其他情況下傳回 null。如此一來,中介服務 SDK 就能在找到得標廣告後截斷中介服務鏈。

建立 AdSelectionOutcomesConfig 後,即可呼叫服務鏈中首個第三方聯播網的 selectAds() 方法。

Kotlin

val adSelectionManager = context.getSystemService(AdSelectionManager::class.java)

// Initialize AdSelectionFromOutcomesConfig
AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig =
  AdSelectionFromOutcomesConfig.Builder()
    .setSeller(seller)
    .setAdSelectionIds(listof(outcome1p))
    .setSelectionSignals({"bid_floor": bid_floor})
    .setSelectionLogicUri(selectionLogicUri)
    .setAdSelectionIds(outcomeIds)
    .build()

// Run ad selection with AdSelectionConfig
adSelectionManager.selectAds(
    adSelectionFromOutcomesConfig,
    executor,
    outcomeReceiver)

Java

AdSelectionManager adSelectionManager =
    context.getSystemService(AdSelectionManager.class);

// Initialize AdSelectionFromOutcomesConfig
AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig =
        new AdSelectionFromOutcomesConfig.Builder()
            .setSeller(seller)
            .setAdSelectionIds(Collection.singletonList(outcome1p))
            .setSelectionSignals({"bid_floor": bid_floor})
            .setSelectionLogicUri(selectionLogicUri)
            .setAdSelectionIds(outcomeIds)
            .build();

// Run ad selection with AdSelectionConfig
adSelectionManager.selectAds(
    adSelectionFromOutcomesConfig,
    executor,
    outcomeReceiver);

調度刊登序列中介服務

以下是執行中介服務的作業順序。

  1. 執行第一方廣告選擇。
  2. 逐一查看中介服務鏈中的每個項目。針對每個第三方聯播網執行下列操作:
    1. 建立 AdSelectionFromOutcomeConfig 並納入第一方 outcomeId 和第三方 SDK 出價下限。
    2. 使用上一個步驟的設定呼叫 selectAds()
    3. 若結果不為空,就會傳回廣告。
    4. 呼叫當前 SDK 廣告聯播網轉接程式的 selectAds() 方法。若結果不為空,就會傳回廣告。
  3. 如果無法從服務鏈中找到得標廣告,就會傳回第一方廣告。

Kotlin

fun runWaterfallMediation(mediationChain : List<NetworkAdapter>)
  : Pair<AdSelectionOutcome, NetworkAdapter> {
    val outcome1p = runAdSelection()

    var outcome : AdSelectionOutcome
    for(network3p in mediationChain) {
      outcome = runSelectOutcome(outcome1p, network3p)
      if (outcome1p.hasOutcome() && outcome.hasOutcome()) {
          return Pair(outcome, this)
      }

      outcome = network3p.runAdSelection()
      if(outcome.hasOutcome()) {
          return Pair(outcome, network3p)
      }
    }
  return Pair(outcome1p, this)
}

Java

class MediationNetwork {
    AdSelectionManager adSelectionManager;

    public MediationNetwork() {
        AdSelectionManager adSelectionManager =
            context.getSystemService(AdSelectionManager.class);
    }

    public void runAdSelection() {...}

    public void reportImpressions() {...}

    public Pair<AdSelectionOutcome, NetworkAdapter> runWaterfallMediation(
            List<NetworkAdapter> mediationChain) {
        AdSelectionOutcome outcome1p = runAdSelection();

        AdSelectionOutcome outcome;
        for(NetworkAdapter network3p: mediationChain) {
            if (outcome1p.hasOutcome() &&
              (outcome = runSelectOutcome(outcome1p, network3p)).hasOutcome()) {
                return new Pair<>(outcome, this);
            }

            if((outcome = network3p.runAdSelection()).hasOutcome()) {
                return new Pair<>(outcome, network3p);
            }
        }
        return new Pair<>(outcome1p, this);
    }

    /* Runs comparison by creating an AdSelectionFromOutcomesConfig */
    public AdSelectionOutcome runSelectOutcome(AdSelectionOutcome outcome1p,
                                              NetworkAdapter network3p) { ... }
}

回報廣告曝光

廣告曝光的回報流程依競價方式而異,共有兩套流程。如果您是進行競價的賣方平台,請參閱本節資訊。若您要實作刊登序列中介服務,請按照「刊登序列中介服務回報曝光」一節的步驟執行。

單一賣方平台回報曝光

透過廣告選擇工作流程選出得標的廣告後,您可以使用 AdSelectionManager.reportImpression() 方法向參與的買方和賣方平台回報曝光。如何回報廣告曝光:

  1. 初始化 AdSelectionManager 物件。
  2. 使用廣告選擇 ID 建立 ReportImpressionRequest 物件。
  3. 使用 ReportImpressionRequest 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 reportImpression()

Java

AdSelectionManager adSelectionManager =
    context.getSystemService(AdSelectionManager.class);

// Initialize a ReportImpressionRequest
ReportImpressionRequest reportImpressionRequest =
        new ReportImpressionRequest.Builder()
                .setAdSelectionId(adSelectionId)
                .setAdSelectionConfig(adSelectionConfig)
                .build();

// Request to report the impression with the ReportImpressionRequest
adSelectionManager.reportImpression(
    reportImpressionRequest,
    executor,
    outcomeReceiver);

Kotlin

val adSelectionManager = context.getSystemService(AdSelectionManager::class.java)

// Initialize a ReportImpressionRequest
val adSelectionConfig: ReportImpressionRequest =
    ReportImpressionRequest.Builder()
        .setAdSelectionId(adSelectionId)
        .setAdSelectionConfig(adSelectionConfig)
        .build()

// Request to report the impression with the ReportImpressionRequest
adSelectionManager.reportImpression(
    reportImpressionRequest,
    executor,
    outcomeReceiver)

使用下列必要參數初始化 ReportImpressionRequest

  • 廣告選擇 ID:裝置使用者專屬 ID,用於表示成功選出廣告。
  • 廣告選擇設定:與提供的廣告選擇 ID 辨識的 selectAds() 呼叫中使用設定相同的設定。

非同步 reportImpression() 方法會使用 OutcomeReceiver 物件,指出 API 呼叫的結果。

  • onResult() 回呼會指出是否已建立曝光報表網址,以及是否已排定要求執行時間。
  • onError() 回呼代表可能發生下列情況:
    • 如果呼叫是因為輸入無效引數才初始化,AdServicesException 會指出 IllegalArgumentException 是錯誤原因。
    • 發生其他錯誤時,收到的 AdServicesException 則會指出 IllegalStateException 是錯誤原因。

刊登序列中介服務回報曝光

中介服務 SDK 必須追蹤勝出的 SDK 來觸發回報流程。參與中介服務鏈的 SDK 應讓中介服務可藉由叫用特定方法,觸發自身的回報流程。參與中介競價的 SDK 可以按照上述步驟,自行實作回報機制。

賣方平台可以將下方的第三方 SDK 程式碼範例當成原型,瞭解如何加入中介服務流程:

Pair<AdSelectionOutcome, NetworkAdapter> winnerOutcomeAndNetwork =
         mediationSdk.orchestrateMediation(mediationChain);

if (winner.first.hasOutcome()) {
      winner.second.reportImpressions(winner.first.getAdSelectionId());

曝光回報端點

曝光回報 API 會向賣方平台和得標買方平台提供的端點發出 HTTPS GET 要求:

買方平台端點:

  • API 使用自訂目標對象中指定的出價邏輯網址擷取買方提供的 JavaScript,該 JavaScript 包含傳回曝光報表網址所需的邏輯。
  • 叫用 reportWin() JavaScript 函式,該函式應會傳回買方的曝光報表網址。

賣家平台端點:

  • 使用 AdSelectionConfig 物件中指定的 決策邏輯網址擷取賣家的決策邏輯 JavaScript。
  • 叫用 reportResult() JavaScript 函式,該函式應傳回買方的曝光回報網址。

出價與競價服務報表

在出價與競價服務執行的競價,會在伺服器端競價的加密回應中提供所有必要的報表資訊,包括針對廣告互動報表產生的網址。將回應解密後,系統會向平台註冊對應的網址,因此廣告和曝光報表都會遵循上述的步驟。

最佳效果曝光報表

reportImpression() 方法的用意是盡可能提供最佳報表。

回報廣告互動

Protected Audience 可提供支援,針對顯示的廣告回報更精細的互動資料。這類資料包括瀏覽時間、點擊、懸停等互動,或任何其他可收集的實用指標。只需兩個步驟,即可設定接收報表的程序。首先,買方和賣方必須完成註冊,才能在報表 JavaScript 中查看這些報表。接著,用戶端需要回報這些事件。

註冊以接收互動事件

系統註冊互動事件時,會在買方的 reportWin() 和賣方的 reportResult() JavaScript 函式中,使用平台提供的 JavaScript 函式:registerAdBeacon。如要註冊以接收事件報表,只要從報表 JavaScript 呼叫平台 JavaScript 函式即可。下列程式碼片段使用買家的 reportWin(),但同樣做法也適用於 reportResult()

reportWin(
  adSelectionSignals,
  perBuyerSignals,
  signalsForBuyer,
  contextualSignals,
  customAudienceSignals) {
    ...
    // Calculate reportingUri, clickUri, viewUri, and hoverUri
    registerAdBeacon("click", clickUri)
    registerAdBeacon("view", viewUri)
    registerAdBeacon("hover", hoverUri)

    return reportingUrl;
}

回報互動事件

回報曝光後,用戶端可以使用 AdSelectionManager.reportInteraction() 方法,將互動回報給先前已註冊的得標買方和賣方平台。如何回報廣告事件:

  1. 初始化 AdSelectionManager 物件。
  2. 使用廣告選擇 ID、互動鍵、互動資料和報表目的地,建構 ReportInteractionRequest 物件。
  3. 使用 request 物件和相關的 ExecutorOutcomeReceiver 物件,呼叫非同步 reportInteraction() 方法。
AdSelectionManager adSelectionManager =
    context.getSystemService(AdSelectionManager.class);

// Initialize a ReportInteractionRequest
ReportInteractionRequest request =
  new ReportInteractionRequest.Builder()
    .setAdSelectionId(adSelectionId)
    .setInteractionKey("view")
    .setInteractionData("{ viewTimeInSeconds : 1 }") // Can be any string
    .setReportingDestinations(
      FLAG_REPORTING_DESTINATION_BUYER | FLAG_REPORTING_DESTINATION_SELLER
    )
    .build();

// Request to report the impression with the ReportImpressionRequest
adSelectionManager.reportInteraction(
  reportImpressionRequest,
  executor,
  outcomeReceiver);

使用下列必要參數初始化 ReportInteractionRequest

  • 廣告選擇 ID:從先前傳回的 AdSelectionOutcome 擷取的廣告選擇 ID。
  • 互動鍵:由用戶端定義的字串鍵,用於說明回報的動作。這必須符合賣方或買方在報表 JavaScript 函式中註冊的鍵。
  • 互動資料:包含要納入事件報表的字串,會以 POST 方式傳回報表伺服器。
  • 報表目的地:指定是否應向買方和/或賣方回報事件的位元遮罩。這些標記是由平台提供,而系統可使用位元運算建立最終目的地遮罩。如要回報給單一目的地,可以直接使用平台提供的標記。如要回報給多個目的地,可以使用按位元或 (|) 合併標記值。

非同步 reportInteraction() 方法會使用 OutcomeReceiver 物件,指出 API 呼叫的結果。

  • onResult() 回呼代表回報互動呼叫有效。
  • onError() 回呼代表可能發生下列情況:
    • 若是在應用程式於背景執行時發出呼叫,會傳回 IllegalStateException 和失敗說明。
    • 如果用戶端因呼叫 reportInteraction() 而受到節流限制,會傳回 LimitExceededException
    • 如果未註冊套件就呼叫隱私權保護 API,會傳回 SecurityException()
    • 如果回報互動的應用程式不同於呼叫 selectAds() 的應用程式,會傳回 IllegalStateException
  • 如果使用者不同意啟用 Privacy Sandbox API,則呼叫會失敗,而且不會顯示相關通知。

互動報表端點

報表互動 API 會向賣方平台和得標買方平台提供的端點發出 HTTPS POST 要求。Protected Audience 會比對互動鍵和報表 JavaScript 中宣告的 URI,並針對每個回報的互動向每個端點發出 POST 要求。要求的內容類型為純文字,主體為互動資料。

最佳效果互動報表

reportInteraction() 的用意是透過 HTTP POST 盡可能提供最佳報表。

每日背景更新

建立自訂目標對象時,應用程式或 SDK 可以初始化自訂目標對象的中繼資料。此外,平台也可以透過每日背景更新程序,更新自訂目標對象的下列中繼資料內容。

  • 使用者出價信號
  • 受信任的出價資料
  • AdData 清單

這項程序會根據自訂目標對象中定義的每日更新網址進行查詢,而網址可能會傳回 JSON 回應。

  • JSON 回應可能包含需要更新的任何受支援中繼資料欄位。
  • 每個 JSON 欄位都會經過獨立驗證。用戶端會忽略任何格式錯誤的欄位,因此不會在回應中更新該特定欄位。
  • 空白的 HTTP 回應或空白的 JSON 物件「{}」會導致無法更新中繼資料。
  • 回應訊息的大小上限為 10 KB。
  • 所有 URI 都必須使用 HTTPS。
  • trusted_bidding_uri 必須和買方使用相同的 ETLD+1。

範例:每日背景更新的 JSON 回應

{
    "user_bidding_signals" : { ... },  // Valid JSON object
    "trusted_bidding_data" : {
        "trusted_bidding_uri" : 'example-dsp1-key-value-service.com',
        "trusted_bidding_keys" : [ 'campaign123', 'campaign456', ... ]
    },
    'ads' : [
        {
            "render_uri" : 'www.example-dsp1.com/.../campaign123.html',
            'metadata' : { ... }  // Valid JSON object
        },
        {
            "render_uri" : 'www.example-dsp1.com/.../campaign456.html',
            'metadata' : { ... }  // Valid JSON object
        },
        ...
    ]
}

廣告選擇專用的 JavaScript

廣告選擇工作流程可自動化調度管理買家提供和賣家提供的 JavaScript。

買家提供的 JavaScript 是擷取自自訂目標對象指定的出價邏輯網址。傳回的 JavaScript 應包含下列函式:

賣家提供的 JavaScript 是擷取自廣告選擇 API 的 AdSelectionConfig 參數中指定的決策邏輯網址。傳回的 JavaScript 應包含下列函式:

generateBid()

function generateBid(
  ad,
  auction_signals,
  per_buyer_signals,
  trusted_bidding_signals,
  contextual_signals,
  user_signals,
  custom_audience_bidding_signals) {
  return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };
}

輸入參數:

  • ad:JSON 物件,格式為 var ad = { 'render_url': url, 'metadata': json_metadata };
  • auction_signals, per_buyer_signals:競價設定物件中指定的 JSON 物件
  • custom_audience_bidding_signals:平台產生的 JSON 物件。格式如下:

    var custom_audience_signals = {
      "owner":"ca_owner",
      "buyer":"ca_buyer",
      "name":"ca_name",
      "activation_time":"ca_activation_time_epoch_ms",
      "expiration_time":"ca_expiration_time_epoch_ms",
      "user_bidding_signals":"ca_user_bidding_signals"
    }
    

    其中:

    • ownerbuyername 是取自屬性的字串,名稱與參與廣告選擇的自訂目標對象相同
    • activation_timeexpiration_time 是自訂目標對象啟用和到期的時間,以 Unix Epoch 時間起算的秒數表示。
    • ca_user_bidding_signalsCustomAudience 建立時在 userBiddingSignals 欄位中指定的 JSON 字串
    • trusted_bidding_signals, contextual_signalsuser_signals 是 JSON 物件。這些物件是以空白物件的形式傳遞,後續版本將填入資料。這種格式並非由平台強制執行,而是由廣告技術管理。

成果:

  • ad:出價參考的廣告。允許使用指令碼以回傳有不同中繼資料的廣告複本。廣告的 render_url 屬性應該不會改變。
  • bid:代表這則廣告出價的浮點值
  • status:整數值,可能如下:
    • 0:成功執行
    • 1:(或任何非零值) 有無效的輸入信號。如果產生的出價傳回非零值,則所有 自訂目標對象廣告的出價程序都會失效

scoreAd()

function scoreAd(
  ad,
  bid,
  ad_selection_config,
  seller_signals,
  trusted_scoring_signals,
  contextual_signal,
  user_signal,
  custom_audience_signal) {
    return {'status': 0, 'score': score };
}

輸入參數:

  • ad:請參閱 generateBid 說明文件
  • bid:廣告出價的值
  • ad_selection_config:JSON 物件,代表 selectAds API 的 AdSelectionConfig 參數。格式為:

    var ad_selection_config = {
      'seller': 'seller',
      'decision_logic_url': 'url_of_decision_logic',
      'custom_audience_buyers': ['buyer1', 'buyer2'],
      'auction_signals': auction_signals,
      'per_buyer_signals': per_buyer_signals,
      'contextual_ads': [ad1, ad2]
    }
    
  • seller_signals:讀取自 sellerSignals AdSelectionConfig API 參數的 JSON 物件

  • trusted_scoring_signal:讀取自 AdSelectionConfig API 參數中的 adSelectionSignals 欄位

  • contextual_signals, user_signals:JSON 物件。目前這些物件是以空白物件的形式傳遞,後續版本將填入資料。這種格式並非由平台強制執行,而是由廣告技術管理

  • per_buyer_signals:讀取自 AdSelectionConfig API 參數中 perBuyerSignal 對應讀取的 JSON 物件,做為目前自訂目標對象買家的索引鍵。如果對應內容未包含特定買家的任何項目,則為空白。

輸出內容:

  • score:代表這則廣告評分值的浮點值
  • status:整數值,可能如下:
    • 0:成功執行
    • 1:如果 customAudienceSignals 無效
    • 2:如果 AdSelectionConfig 無效
    • 3:如果上述信號以外的信號無效
    • 零以外的值都會導致程序失敗,此值決定了擲回例外狀況的類型

selectOutcome()

function selectOutcome(
  outcomes,
  selection_signals) {
    return {'status': 0, 'result': null};
}

輸入參數:

  • outcomes:JSON 物件 {"id": id_string, "bid": bid_double}
  • selection_signals:競價設定物件中指定的 JSON 物件

輸出內容:

  • status0 為成功,其他為失敗
  • result:傳遞的結果之一或空值

reportResult()

function reportResult(ad_selection_config, render_url, bid, contextual_signals) {
   return {
      'status': status,
      'results': {'signals_for_buyer': signals_for_buyer, 'reporting_url': reporting_url }
   };
}

輸入參數:

  • ad_selection_config:請參閱 scoreAds 的說明文件
  • render_url:得標廣告的顯示網址
  • bid:得標廣告的出價
  • contextual_signals:請參閱 generateBid 的說明文件

輸出內容:

  • status: 0 為成功,非零為失敗
  • results:JSON 物件,包含以下項目
    • signals_for_buyer:傳遞至 reportWin 函式的 JSON 物件
    • reporting_url:平台向買方回報曝光所用的網址

reportWin()

function reportWin(
   ad_selection_signals,
   per_buyer_signals,
   signals_for_buyer,
   contextual_signals,
   custom_audience_signals) {
   return {'status': 0, 'results': {'reporting_url': reporting_url } };
}

輸入參數:

  • ad_selection_signals, per_buyer_signals:請參閱 scoreAd 的說明文件
  • signals_for_buyerreportResult 傳回的 JSON 物件
  • contextual_signals, custom_audience_signals:請參閱 generateBid 的說明文件

輸出內容:

  • status: 0 為成功,非零為失敗
  • results:包含以下項目的 JSON 物件:
    • reporting_url:平台向賣方回報曝光所用的網址

registerAdBeacon()

function registerAdBeacon(
  interaction_key,
  reporting_uri
)

輸入參數

  • interaction_key:代表事件的字串。平台稍後會使用這個參數回報事件互動,查詢應收到通知的 reporting_uri。這個鍵需符合買方或賣方註冊的項目,以及賣方回報的內容。
  • reporting_uri:用於接收事件報表的 URI。這應視回報的事件類型而定。這個參數必須接受 POST 要求,才能處理與事件一併回報的資料。

廣告選擇程序的預建 URI

預先建構的 URI 可讓廣告技術人員指定 JavaScript 函式,以用於 AdSelectionConfigAdSelectionFromOutcomesConfig 類別中的廣告選擇決策邏輯。預先建構的 URI 無需網路呼叫即可下載對應的 JavaScript。廣告技術人員可以使用預先建構的 URI,不必設定註冊網域來代管 JavaScript。

預先建構的 URI 是以下列格式構建而成:

ad-selection-prebuilt:<use-case>/<name>?<required-script-generation-parameters>

Privacy Sandbox 平台會在執行階段使用這個 URI 的資訊提供 JavaScript。

如果發生下列情況,系統會擲回 IllegalArgumentException

  • URI 中沒有任何必要參數
  • URI 中有無法辨識的參數

支援的預建 URI 用途和名稱

用途 1:ad-selection

selectAds(AdSelectionConfig) 流程支援 ad-selection 用途下的預先建構 URI。

預先建構的 URI 名稱:highest-bid-wins

這個預先建構的 URI 會提供 JavaScript,可在出價後挑選出價最高的廣告。此外,此 URI 還提供基本報表功能,可回報勝出廣告的 render_uribid

必要參數

reportingUrl:透過勝出廣告的 render_uribid 參數化的基本報表網址:

<reportingUrl>?render_uri=<renderUriOfWinnigAd>&bid=<bidOfWinningAd>

用法

如果基本報表網址為 https://www.ssp.com/reporting,預先建構的 URI 會是:

`ad-selection-prebuilt://ad-selection/highest-bid-wins/?reportingUrl=https://www.ssp.com/reporting`

用途 2:ad-selection-from-outcomes

ad-selection-from-outcomes 用途下的預先建構 URI 可支援 selectAds(AdSelectionFromOutcomesConfig) 工作流程。

預先建構的 URI 名稱:waterfall-mediation-truncation

waterfall-mediation-truncation 的預先建構 URI 提供可實作刊登序列中介服務截斷邏輯的 JavaScript,如果 bid 大於或等於 bid floor,JavaScript 就會傳回第一方廣告,在其他情況下則傳回 null

必要參數

bidFloorgetSelectionSignals() 中傳遞的出價下限值鍵,用於與中介服務 SDK 的廣告比較。

用法

如果廣告選擇信號類似於 {"bid_floor": 10},那麼產生的預先建構 URI 會是:

`ad-selection-prebuilt://ad-selection-from-outcomes/waterfall-mediation-truncation/?bidFloor=bid_floor`

測試

為協助您開始使用 Protected Audience API,我們在 GitHub 上提供了 Kotlin 和 Java 的範例應用程式。

必要條件

在選擇廣告以及製作曝光報表時,Protected Audience API 需要一些 JavaScript。在測試環境中提供這個 JavaScript 的方法有兩種:

  • 執行內含傳回 JavaScript 的必要 HTTP 端點的伺服器
  • 提供來自本機來源的必要程式碼,以覆寫遠端擷取

無論採用哪一種方法,都需要設定 HTTPS 端點來處理曝光報表。

HTTPS 端點

如要測試廣告選擇和曝光回報程序,您必須設定 7 個可讓測試裝置或模擬器存取的 HTTPS 端點:

  1. 提供出價邏輯 JavaScript 的買方端點。
  2. 提供出價信號的端點。
  3. 提供決策邏輯 JavaScript 的賣家端點。
  4. 提供評分信號的端點。
  5. 得標買家曝光報表端點。
  6. 賣家曝光報表端點。
  7. 為自訂目標對象提供每日更新內容的端點。

為方便起見,GitHub 存放區提供基本的 JavaScript 程式碼供您進行測試。其中也包含 OpenAPI 服務定義,可部署至支援的模擬或微服務平台。如需瞭解詳情,請參考 README 專案。

覆寫遠端擷取 JavaScript

這項功能用於端對端測試。如要覆寫遠端擷取功能,應用程式必須在偵錯模式中執行,並啟用開發人員選項。

如要為您的應用程式啟用偵錯模式,請將以下程式碼新增至 AndroidManifest.xml 的應用程式屬性:

<application
  android:debuggable="true">

如需覆寫機制的應用範例,請參考 GitHub 上的 Protected Audience API 範例應用程式

您必須自行新增 JavaScript 來處理廣告選擇日常安排,例如出價、評分決策和報表。您可以在 GitHub 存放區中找到處理所有必要要求的基本 JavaScript 程式碼範例。Protected Audience API 範例應用程式展示了如何讀取該檔案的程式碼,以及如何準備用來覆寫的資料。

您可以單獨覆寫賣方或買方指定擷取的 JavaScript,但對於未覆寫的 JavaScript,您需要透過 HTTPS 端點來處理相關作業。如要瞭解如何設定伺服器來處理這些情況,請參閱 README

您只能覆寫針對套件擁有的自訂目標對象進行擷取的 JavaScript。

覆寫賣方 JavaScript

如要設定賣方 JavaScript 的覆寫值,請按照下方程式碼範例操作:

  1. 初始化 AdSelectionManager 物件。
  2. AdSelectionManager 物件取得 TestAdSelectionManager 的參照。
  3. 建構 AdSelectionConfig 物件。
  4. 使用 AdSelectionConfig 物件和 String 建構 AddAdSelectionOverrideRequest 代表您打算當做覆寫使用的 JavaScript。
  5. 使用 AddAdSelectionOverrideRequest 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 overrideAdSelectionConfigRemoteInfo()

Kotlin

val testAdSelectionManager: TestAdSelectionManager =
  context.getSystemService(AdSelectionManager::class.java).getTestAdSelectionManager()

// Initialize AdSelectionConfig =
val adSelectionConfig = new AdSelectionConfig.Builder()
    .setSeller(seller)
    .setDecisionLogicUrl(decisionLogicUrl)
    .setCustomAudienceBuyers(customAudienceBuyers)
    .setAdSelectionSignals(adSelectionSignals)
    .setSellerSignals(sellerSignals)
    .setPerBuyerSignals(perBuyerSignals)
    .build()

// Initialize AddAddSelectionOverrideRequest
val request = AddAdSelectionOverrideRequest.Builder()
    .setAdSelectionConfig(adSelectionConfig)
    .setDecisionLogicJs(decisionLogicJS)
    .build()

// Run the call to override the JavaScript for the given AdSelectionConfig
// Note that this only takes effect in apps marked as debuggable
testAdSelectionManager.overrideAdSelectionConfigRemoteInfo(
    request,
    executor,
    outComeReceiver)

Java

TestAdSelectionManager testAdSelectionManager =
  context.getSystemService(AdSelectionManager.class).getTestAdSelectionManager();

// Initialize AdSelectionConfig =
AdSelectionConfig adSelectionConfig = new AdSelectionConfig.Builder()
    .setSeller(seller)
    .setDecisionLogicUrl(decisionLogicUrl)
    .setCustomAudienceBuyers(customAudienceBuyers)
    .setAdSelectionSignals(adSelectionSignals)
    .setSellerSignals(sellerSignals)
    .setPerBuyerSignals(perBuyerSignals)
    .build();

// Initialize AddAddSelectionOverrideRequest
AddAdSelectionOverrideRequest request = AddAdSelectionOverrideRequest.Builder()
    .setAdSelectionConfig(adSelectionConfig)
    .setDecisionLogicJs(decisionLogicJS)
    .build();

// Run the call to override the JavaScript for the given AdSelectionConfig
// Note that this only takes effect in apps marked as debuggable
testAdSelectionManager.overrideAdSelectionConfigRemoteInfo(
    request,
    executor,
    outComeReceiver);

如要進一步瞭解 AdSelectionConfig 中每個欄位代表的意義,請參閱「執行廣告選擇」一節。主要差異在於「decisionLogicUrl」可以設為預留位置值,系統會忽略該值。

如要覆寫廣告選擇期間使用的 JavaScript,decisionLogicJs 必須包含適當的賣方端函式簽章。如需如何以字串形式讀取 JavaScript 檔案的範例,請參閱 GitHub 上的「Protected Audience API 範例應用程式」。

非同步 overrideAdSelectionConfigRemoteInfo() 方法會使用 OutcomeReceiver 物件指出 API 呼叫的結果。

onResult() 回呼表示已成功套用覆寫值。日後對 selectAds() 的呼叫將使用您傳遞用於覆寫的判斷和回報邏輯。

onError() 回呼代表兩種可能情況:

  • 如果嘗試使用無效引數進行覆寫,AdServiceException 會指出 IllegalArgumentException 是錯誤原因。
  • 如果嘗試覆寫時,應用程式未執行偵錯模式,但已啟用開發人員選項,AdServiceException 會指出 IllegalStateException 是錯誤原因。

重設賣方覆寫值

本節假設您已覆寫賣方 JavaScript,並已取得上一節所用 TestAdSelectionManagerAdSelectionConfig 的參照。

如何重設所有 AdSelectionConfigs 的覆寫值:

  1. 使用相關的 OutcomeReceiver 物件呼叫非同步 resetAllAdSelectionConfigRemoteOverrides() 方法。

Kotlin

// Resets overrides for all AdSelectionConfigs
testAadSelectionManager.resetAllAdSelectionConfigRemoteOverrides(
  outComeReceiver)

Java

// Resets overrides for all AdSelectionConfigs
testAdSelectionManager.resetAllAdSelectionConfigRemoteOverrides(
    outComeReceiver);

重設賣方覆寫值後,對 selectAds() 的呼叫就會使用 AdSelectionConfig 中儲存的 decisionLogicUrl 來嘗試擷取需要的 JavaScript。

如果呼叫 resetAllAdSelectionConfigRemoteOverrides() 失敗,OutComeReceiver.onError() 回呼會提供 AdServiceException。若應用程式未執行偵錯模式,但已啟用開發人員選項,當系統嘗試移除覆寫資料時,AdServiceException 就會指出 IllegalStateException 是錯誤原因。

覆寫買方 JavaScript

  1. 請按照步驟加入自訂目標對象
  2. 根據要覆寫的自訂目標對象,使用相應的「買方」和「名稱」值建構 AddCustomAudienceOverrideRequest,並建構用於覆寫的出價邏輯和資料
  3. 使用 AddCustomAudienceOverrideRequest 物件和相關的 ExecutorOutcomeReceiver 物件呼叫非同步 overrideCustomAudienceRemoteInfo()

Kotlin

val testCustomAudienceManager: TestCustomAudienceManager =
  context.getSystemService(CustomAudienceManager::class.java).getTestCustomAudienceManager()

// Join custom audience

// Build the AddCustomAudienceOverrideRequest
val request = AddCustomAudienceOverrideRequest.Builder()
    .setBuyer(buyer)
    .setName(name)
    .setBiddingLogicJs(biddingLogicJS)
    .setTrustedBiddingSignals(trustedBiddingSignals)
    .build()

// Run the call to override JavaScript for the given custom audience
testCustomAudienceManager.overrideCustomAudienceRemoteInfo(
    request,
    executor,
    outComeReceiver)

Java

TestCustomAudienceManager testCustomAudienceManager =
  context.getSystemService(CustomAudienceManager.class).getTestCustomAudienceManager();

// Join custom audience

// Build the AddCustomAudienceOverrideRequest
AddCustomAudienceOverrideRequest request =
    AddCustomAudienceOverrideRequest.Builder()
        .setBuyer(buyer)
        .setName(name)
        .setBiddingLogicJs(biddingLogicJS)
        .setTrustedBiddingSignals(trustedBiddingSignals)
        .build();

// Run the call to override JavaScript for the given custom audience
testCustomAudienceManager.overrideCustomAudienceRemoteInfo(
    request,
    executor,
    outComeReceiver);

「買家」和「名字」的值與用來建立自訂目標對象的值相同。進一步瞭解這些欄位

此外,您還可以指定兩個額外參數:

  • biddingLogicJs:這個 JavaScript 包含在選擇廣告時使用的買方邏輯。請留意這個 JavaScript 必須包含的函式簽章
  • trustedBiddingSignals:選擇廣告時使用的出價信號。如果是測試用途,這個值可以是空白字串。

非同步 overrideCustomAudienceRemoteInfo() 方法會使用 OutcomeReceiver 物件來指出 API 呼叫的結果。

onResult() 回呼表示已成功套用覆寫值。後續對 selectAds() 的呼叫將使用您傳遞用於覆寫的判斷和回報邏輯。

onError() 回呼代表兩種可能情況。

  • 如果嘗試使用無效引數進行覆寫,AdServiceException 會指出 IllegalArgumentException 是錯誤原因。
  • 如果嘗試覆寫時,應用程式未執行偵錯模式,但已啟用開發人員選項,AdServiceException 會指出 IllegalStateException 是錯誤原因。

重設買方覆寫值

本節假設您已覆寫買方 JavaScript,且已取得上一節中所用 TestCustomAudienceManager 的參照。

如要重設所有自訂目標對象的覆寫值,請按照下列步驟操作:

  1. 使用相關的 ExecutorOutcomeReceiver 物件呼叫非同步的 resetAllCustomAudienceOverrides() 方法。

Kotlin

// Resets overrides for all custom audiences
testCustomAudienceManager.resetCustomAudienceRemoteInfoOverride(
    executor,
    outComeReceiver)

Java

// Resets overrides for all custom audiences
testCustomAudienceManager.resetCustomAudienceRemoteInfoOverride(
    executor,
    outComeReceiver)

重設買方覆寫值後,後續對 selectAds() 的呼叫就會使用 CustomAudience 中儲存的 biddingLogicUrltrustedBiddingData 來嘗試擷取需要的 JavaScript。

如果呼叫 resetCustomAudienceRemoteInfoOverride() 失敗,OutComeReceiver.onError() 回呼會提供 AdServiceException。若應用程式未執行偵錯模式,但已啟用開發人員選項,當系統嘗試移除覆寫資料時,AdServiceException 就會指出 IllegalStateException 是錯誤原因。

設定回報伺服器

使用遠端擷取覆寫功能時,您仍須設定伺服器,讓裝置或模擬器能存取該伺服器來回應回報事件。傳回 200 的簡單端點就足以進行測試。GitHub 存放區包含 OpenAPI 服務定義,您可以將這些定義部署至支援的模擬或微服務平台。如需瞭解詳情,請參考 README 專案。

尋找 OpenAPI 定義時,請搜尋 reporting-server.json。這個檔案含有會傳回 200 的簡易端點,代表 HTTP 回應代碼。這個端點會在 selectAds() 期間使用,並向 Protected Audience API 發出信號,表示曝光報表已順利完成。

要測試的功能

  • 根據先前的使用者動作,加入/退出及設定自訂目標對象。
  • 透過遠端代管的 JavaScript 在裝置上啟動廣告選擇程序。
  • 觀察應用程式與自訂目標對象設定之間的關聯如何影響廣告選擇結果。
  • 在廣告選擇完成後回報曝光。

限制

下表列出了 Protected Audience API 處理程序的限制。這些限制可能根據意見回饋有所調整。如要查看開發中的功能,請參閱版本資訊

元件 限制說明 限制值
自訂目標對象 (CA) 每個自訂目標對象的廣告數量上限 100
每個應用程式的自訂目標對象數量上限 1000
可建立自訂目標對象的應用程式數量上限 1000
CA 的啟用時間與其建立時間的最長延遲 60 天
自訂目標對象啟用期間上限 60 天
裝置的自訂目標對象數量上限 4000
自訂目標對象名稱大小上限 200 個位元組
每日擷取 URI 的大小上限 400 個位元組
出價邏輯 URI 的大小上限 400 個位元組
受信任出價資料大小上限 10 KB
使用者出價信號大小上限 10 KB
每個買家的 leaveCustomAudience 呼叫頻率上限 每秒 1 次
每個買家的 joinCustomAudience 呼叫頻率上限 每秒 1 次
自訂目標對象背景擷取 連線逾時 5 秒
HTTP 讀取逾時 30 秒
總下載大小上限 10 KB
擷取疊代時長上限 5 分鐘
每項工作更新的 CA 數量上限 1000
廣告選擇 買家人數上限 未定
每個買家的自訂目標對象數量上限 未定
單次競價廣告數量上限 未定
初始連線逾時 5 秒
連線讀取逾時 5 秒
整體 AdSelection 的執行時間上限 10 秒
AdSelection 中每個自訂目標對象的出價執行時間上限 5 秒
AdSelection 中的評分執行時間上限 5 秒
AdSelection 中每個買家的執行時間上限 未定
廣告選擇/賣方/每個買方信號的大小上限 未定
賣方/買方指令碼的大小上限 未定
selectAds 的呼叫頻率上限 1 QPS
曝光報表 廣告選擇至少保留多久才會移除 24 小時
儲存空間廣告選擇數量上限 未定
報表輸出網址大小上限 未定
曝光報表時間上限 未定
通知呼叫的重試次數上限 未定
連線逾時 5 秒
reportImpression 的總執行時間上限 2 秒
reportImpressions 的呼叫頻率上限 1 QPS
事件回報功能 每次競價的每個買家信標數量上限 10

每次競價的每個賣方信標數量上限

10

事件索引鍵大小上限

40 位元組

事件資料大小上限

64 KB

廣告 廣告清單的大小上限 關聯單一 CA 中所有 AdData 共用 10 KB
網址 接受輸入的任何網址字串長度上限 未定
JavaScript 執行時間上限 出價及曝光報表的評分皆為 1 秒
耗用記憶體上限 10 MB

回報錯誤和問題

您的意見回饋對 Android 版 Privacy Sandbox 至關重要!如果您發現了任何問題,或希望針對 Android 版 Privacy Sandbox 提出改進意見,請告訴我們