はじめに
AI事業本部でソフトウェアエンジニアをしている太田と申します。Dynalystという広告配信プロダクトの開発をしております。
広告事業に関連した最近のtopicとしてPrivacySandboxAPIがあります。PrivacySandboxAPIの広告関連のAPIは2023年7月からGeneral Availability(GA)のフェーズに入り、すべてのChromeユーザー(iOS版を除く)に提供されます(https://privacysandbox.com/intl/ja_jp/news/the-next-stages-of-privacy-sandbox-general-availability)。その内の一つにProtectedAudienceAPI(以下PA API)というユーザーのwebブラウザ上でオンデバイスオークションを行うための仕様があります。PA APIは実装するにあたって落とし穴がいくつかあるため、この記事ではProtectedAudienceAPIを用いたプロダクト開発を開始するまでのガイドをご紹介します。
本題に入る前に少し話が逸れますが、最近弊社で開催されたCyberAgentDeveloperConferenceにてチームメンバーがPrivacySandboxAPIについての発表を行いましたので併せてご覧ください。
また、最近Privacy Sandbox Demosが公開されました。Privacy Sandbox Demosでは簡単にPrivacySandboxAPIを試したり実験するための環境が用意されています。まずはAPIを動かしてみて理解を深めたい場合はPrivacy Sandbox Demosを試してみることをおすすめします。
前提
- アドテクやRTB入札に関する基本的な知識はあるものとします
- この記事は広告事業者の中でもbuyer(特にDSP)の立場で記載しますが、sellerの立場で開発する際も一部流用できます
- PA APIの流れや大まかな概要は改めて説明しません。開発者ガイドや、Explainerを読んで理解しているものとします。
- 特にPA APIで利用可能な関数の引数や返り値の仕様は開発者ガイドやExplainerを参照してください
- 場合によってはSpec が参考になる場合もあります
- 本記事は2023/06/18時点の仕様に基づいて記載していますが、開発中の仕様であるため変更される可能性があります。
事前準備
PA APIはhttps通信を前提としているので、httpsを用いて開発できる開発環境を用意する必要があります。この記事ではmkcertを利用します。また、複数のロール(広告主、広告掲載メディア、buyer、seller)のオリジンが必要になるので、ワイルドカード証明を生成しておくと便利です。
# リポジトリ作成
mkdir paa-tips
cd paa-tips
# 以降の操作は全てpaa-tipsディレクトリで行います
# 証明書作成
brew install mkcert
mkcert -install
mkcert "*.paa-tips.com" paa-tips.com
mkdir certfiles
mv _wildcard.paa-tips.com* certfiles
次に生成した証明書を利用してhttps通信を行うためにCaddyを利用する準備をします。Caddyはリバースプロキシとして利用することができ、先ほど生成した証明書ファイルを利用します。まずはCaddyの実行ファイルをダウンロードしてpathの通っている場所に caddy
と言うファイル名にして置いてください(pluginは必要ありません)。社内のメンバーが試したところ、Arm CPUのmacではダウンロードリンクから取得した実行ファイルがうまく動かないようでした。その場合、Githubのreleaseページから直接バイナリをダウンロードしてください。
次にオリジンの数だけ設定ファイルを作成します。
CaddyfileAdvertiser
advertiser.paa-tips.com:44301
tls ./certfiles/_wildcard.paa-tips.com+1.pem ./certfiles/_wildcard.paa-tips.com+1-key.pem
reverse_proxy :8080
CaddyfilePublisher
publisher.paa-tips.com:44302
tls ./certfiles/_wildcard.paa-tips.com+1.pem ./certfiles/_wildcard.paa-tips.com+1-key.pem
reverse_proxy :8080
CaddyfileDsp
dsp.paa-tips.com:44303
tls ./certfiles/_wildcard.paa-tips.com+1.pem ./certfiles/_wildcard.paa-tips.com+1-key.pem
reverse_proxy :8080
CaddyfileSsp
ssp.paa-tips.com:44304
tls ./certfiles/_wildcard.paa-tips.com+1.pem ./certfiles/_wildcard.paa-tips.com+1-key.pem
reverse_proxy :8080
オリジンの名前とport番号をすべて違う値にしています。
また、オリジン毎にcaddyのプロセスを起動する必要があり、一つ一つ起動させるのは面倒なので起動用のスクリプトを用意しておきます。
caddy-run.sh
#!/usr/bin/env bash
caddy run --config CaddyfileAdvertiser &
advertiserProcessId=$!
exec caddy run --config CaddyfilePublisher &
publisherProcessId=$!
exec caddy run --config CaddyfileDsp &
dspProcessId=$!
exec caddy run --config CaddyfileSsp &
sspProcessId=$!
trap "kill ${advertiserProcessId} ${publisherProcessId} ${dspProcessId} ${sspProcessId}" EXIT
while true
do
sleep 1;
done
最後にこれらのオリジンの名前解決をするために/etc/hostsに設定を追加します
/etc/hosts
127.0.0.1 advertiser.paa-tips.com publisher.paa-tips.com dsp.paa-tips.com ssp.paa-tips.com
最小構成のfledge auctionを実行する
jsの準備
今回の例ではnpm, webpackを利用します。必要に応じて違うツールを使えますが、webpack以外のツールを使う場合、後述するPA API特有の課題へのワークアラウンドが機能しない可能性があります。
package.json
{
"name": "paa-tips",
"version": "0.0.1",
"description": "",
"scripts": {
"start": "webpack-dev-server"
},
"engines": {
"node": ">=18.12.0",
"npm": ">=8.19.2"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
npm install
InterestGroupを設定する
まずはユーザーが広告主のサイトにアクセスしたときにInterestGroup(IG)に登録します。
Advertiser
広告主のwebサイトとして以下を実装します。
- htmlを作成する
- DSPのオリジンをsrc属性に指定したiframeを読み込む
- この要件は開発者ガイドの以下から来ています
joinAdInterestGroup()
の呼び出し元コンテキストのオリジンは、インタレストグループのオーナーのオリジンと一致する必要があるため、joinAdInterestGroup()
は、インタレストグループのオーナーのオリジンと現在のドキュメントのオリジンが一致しない限り(たとえば、独自のインタレストグループを持つウェブサイト)、iframe から呼び出す必要があります。- 現実世界では直接DSPのiframeを読み込むことはなく、DSPのtagを読み込んだ後に、DSPのjsがiframeを作成することがほとんどでしょう
- iframeの属性に設定しているallow属性はpermission policyと呼ばれており、現時点では設定していないくても問題ないですが将来的には必要になります
src/advertiser/index.html
<html>
<head>
</head>
<body>
<h1>This is advertiser's web site.</h1>
<div><a href="https://publisher.paa-tips.com:44302/publisher/index.html" rel="noreferrer noopener" target="_blank">click to visit a publisher's web site</a></div>
<iframe src="https://dsp.paa-tips.com:44303/dsp/join-ad-interest-group/index.html" allow="join-ad-interest-group"></iframe>
</body>
</html>
DSP
DSPが実装することは以下の通りです。
- 前述の広告主のiframeに埋め込むためのhtmlを作成する
- iframeの中で
joinAdInterestGroup()
を実行する。joinAdInterestGroup()
の引数には以下を設定します- owner: IGのownerのオリジン
- iframeのsrc属性と一致させておく必要があります
- name: IGの名前
- biddingLogicUrl: auctionの際に入札額を計算するlogicが実装されたURL
- このURLのオリジンは
owner
と一致している必要があります - 今の段階ではまだ実装する必要はなく、オンデバイスオークションを実行する段階で実装します
- このURLのオリジンは
- ads: 入札に勝利した際に表示される広告のURL
- このURLはownerと一致している必要はありませんが、実装の簡便化のために同じオリジンを利用しています
- 今の段階ではまだ実装する必要はありません。広告を描画する段階で実装します。
- owner: IGのownerのオリジン
src/dsp/join-ad-interest-group/index.html
<html>
<head>
<script src="index.js"></script>
</head>
<body>
</body>
</html>
src/dsp/join-ad-interest-group/index.js
const interestGroup = {
owner: 'https://dsp.paa-tips.com:44303',
name: 'paa-tips',
biddingLogicUrl: 'https://dsp.paa-tips.com:44303/dsp/bidding-logic-url/index.js',
ads: [{ renderUrl: 'https://dsp.paa-tips.com:44303/dsp/creative/index.html' }]
}
navigator.joinAdInterestGroup(interestGroup, 60 * 60 * 24 * 7)
ここまでで、一旦動作確認をしてみましょう。開発環境ではwebpack-dev-serverを使うために以下の設定をしてください。
webpack.config.js
const path = require('path');
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'src')
},
allowedHosts: [ 'all' ],
}
}
src/index.js
console.log('hello');
この設定を見るとwebpackの機能を使っておらず、http-serverなどで十分ではないかと思われるかもしれません。現段階ではその通りで、最終的にwebpackの機能を利用した開発を行なっていきます。
リバースプロキシとwebpack-dev-serverを起動
sh caddy-run.sh
# `ERROR unable to autosave config`のような出力があるはずですが起動はできています。
# この出力が気になる場合はsudoで実行するか、caddyのautosave configの設定を変更するなどの方法があります
# 別タブで実行
npm start
https://advertiser.paa-tips.com:44301/advertiser/index.html をchromeで開く(執筆時はdev channelのバージョン: 116.0.5845.4で検証しています)。(事前に chrome://flags/#privacy-sandbox-ads-apis とchrome://settings/privacySandbox でflagがONになっていることを確認してください)
developer toolを開いて、Applicationタブ > Storage > Interest Groupsを確認して以下のような表示が出たら成功です。何も表示されない場合はdeveloper toolを開いたままreloadしてください。それでも表示していない場合はdeveloper toolのconsoleに何かエラーメッセージが表示されている可能性がありますので確認してください。
オンデバイスオークションを実行する
IGの設定が完了したらPA APIの肝であるオンデバイスのオークションを実行します。
publisher
広告を表示するwebメディアはSSPのjsコードを読み込みます
src/publisher/index.html
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>publisher web site</title>
<script src="https://ssp.paa-tips.com:44304/ssp/run-ad-auction/index.js"></script>
</head>
<body>
<h1>This is publisher's site.</h1>
</body>
</html>
ssp
SSPはpublisherのwebページで実行されるjsコードとauctionのjsコードを用意します。
puublisherのwebページで実行されるコードは以下の処理を行います。
- jsコードの中でPA APIの関数
runAdAuction()
を実行しますrunAdAuciton()
の引数にはSSPのオリジン、auctionの勝者を決めるためのSSPのlogicを実装したURL、SSPがauctionの参加を許可するDSPのオリジンを引数に含めますSSPのlogicを実装したURL
はseller
引数のオリジンと同一である必要があります。
src/ssp/run-ad-auction/index.js
window.addEventListener('DOMContentLoaded', async (event) => {
const auctionConfig = {
'seller': 'https://ssp.paa-tips.com:44304',
'decisionLogicUrl': 'https://ssp.paa-tips.com:44304/ssp/decision-logic-url/index.js',
'interestGroupBuyers': ['https://dsp.paa-tips.com:44303'],
}
const result = await navigator.runAdAuction(auctionConfig)
console.log(result)
})
- 先の
SSPのlogicを実装したURL
はjsコードを返すようにします。- このjsコードには以下の2つの関数を定義している必要があります。
scoreAd()
- DSPの入札情報などを引数としてauctionの勝者を決定するscoreを返り値とする
reportResult()
- 入札結果などの通知を行う
- このjsコードには以下の2つの関数を定義している必要があります。
src/ssp/decision-logic-url/index.js
function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
console.log('scoreAd', JSON.stringify({
adMetadata,
bid,
auctionConfig,
trustedScoringSignals,
browserSignals,
}))
return bid;
}
function reportResult(auctionConfig, browserSignals) {
console.log('reportResult', JSON.stringify({
auctionConfig,
browserSignals
}))
}
dsp
DSPはauctionで入札額を決めるための処理をjsで実装する必要があります。前述の joinAdInterestGroup()
にてbiddingLogicUrlとして指定されたURLがこのjsファイルを返します。
- このjsファイルは以下の2つの関数を定義している必要があります
generateBid()
- 入札時に参照が許されているいくつかの情報を引数に入札額と、入札に勝利した場合に表示する広告のURLを返り値として返します。広告のURLに設定できるのはInterestGroupを設定する章で設定した
renderUrl
のいずれかひとつであることに注意してください。
- 入札時に参照が許されているいくつかの情報を引数に入札額と、入札に勝利した場合に表示する広告のURLを返り値として返します。広告のURLに設定できるのはInterestGroupを設定する章で設定した
reportWin()
- 入札に処理したときに実行されるreport用の関数
src/dsp/bidding-logic-url/index.js
function generateBid(group, auctionSignals, perBuyerSignals, trustedBiddingSignals, browserSignals) {
console.log(
JSON.stringify({
group,
auctionSignals,
perBuyerSignals,
trustedBiddingSignals,
browserSignals,
})
)
return {
bid: 1,
ad: {
adName: "adName"
},
render: group.ads[0].renderUrl,
}
}
function reportWin(auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
console.log("reportWin", JSON.stringify({
auctionSignals,
perBuyerSignals,
sellerSignals,
browserSignals
}))
}
ここで再び一旦動作確認をしてみましょう。SSPの SSPのlogicを実装したURL
とDSPの 入札額を計算するlogicが実装されたURL
からjsファイルを取得するときはそれぞれresponse headerに Ad-Auction-Allowed: true
もしくは X-Allow-FLEDGE: true
(こちらはdeprecatedです)をセットする必要があります。これはwebpackに設定を追加すると以下のようになります。
webpack.config.js
const path = require('path');
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'src')
},
allowedHosts: [ 'all' ],
headers: {
'Ad-Auction-Allowed': true,
}
}
}
npm start
を再実行
- developert toolsを開いたまま https://advertiser.paa-tips.com:44301/advertiser/index.html を開く
- https://publisher.paa-tips.com:44302/publisher/index.html を開くと、developer toolのconsoleに
reportWin
やreportResult
といった文字列が表示されているはずです
広告を描画する
さて、ここまででオンデバイスオークションの実行までが完了しました。オークションに勝利した場合、広告を表示させることができますが、ここにもいくつか注意するポイントがあります。まず、seller側は広告をiframeに描画させるか、fenced frameに描画させるかを選ぶ必要があります。fenced frameとは広告などのクロスオリジンなコンテンツを埋め込む際に利用されることを想定した、よりプライバシーに配慮されたframeです。fenced frameでの広告の描画は現時点では必須ではありせんが、iframeとfenced frameでの描画の仕方に差異があるので両方記載します。
iframeで描画する
publisher
publisherはオークションに勝利した広告を入札するためのiframeを用意します(実際はSSPのjsが生成すると思いますが、簡便のために用意しておきます。)
src/publisher/index.html
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>publisher web site</title>
<script src="https://ssp.paa-tips.com:44304/ssp/run-ad-auction/index.js"></script>
</head>
<body>
<h1>This is publisher's site.</h1>
<iframe id="paa-ad-container"></iframe>
</body>
</html>
SSP
SSPは runAdAuction()
の返り値をiframeのsrc属性に設定します
src/ssp/run-ad-auction/index.js
window.addEventListener('DOMContentLoaded', async (event) => {
const auctionConfig = {
'seller': 'https://ssp.paa-tips.com:44304',
'decisionLogicUrl': 'https://ssp.paa-tips.com:44304/ssp/decision-logic-url/index.js',
'interestGroupBuyers': ['https://dsp.paa-tips.com:44303'],
}
const result = await navigator.runAdAuction(auctionConfig)
const container = document.getElementById('paa-ad-container')
container.src = result
})
DSP
DSPは描画する広告を実装します。このとき、オンデバイスオークションを実行するの章で設定した generateBid
の返り値のURLから配信されることに注意してください。実際にはDSPのオリジンから配信されるとは限りませんが、簡便化のためにDSPの処理として実装しています。
src/dsp/creative/index.html
<html lang="ja">
<head>
<title>creative</title>
<meta charset="UTF-8">
</head>
<body>
<a href="">
<img src="banner.png" width="320" height="50">
</a>
</body>
</html>
src/dsp/creative/banner.png
適当な画像ファイルを作っておく
https://publisher.paa-tips.com:44302/publisher/index.html にアクセスして、以下のような表示がされたら成功です。
fenced frameで描画する
publisher
fenced frameを利用する場合はiframeの代わりにfencedframeを設定します。
src/publisher/index.html
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>publisher web site</title>
<script src="https://ssp.paa-tips.com:44304/ssp/run-ad-auction/index.js"></script>
</head>
<body>
<h1>This is publisher's site.</h1>
<fencedframe id="paa-ad-container"></fencedframe>
</body>
</html>
SSP
SSPは以下の変更が必要です
runAdAuction()
の引数に設定するauctionConfig
に'resolveToConfig': true
を設定するrunAdAuctionの()
の返り値をfencedframeのconfig属性に設定する
src/ssp/run-ad-auction/index.js
window.addEventListener('DOMContentLoaded', async (event) => {
const auctionConfig = {
'seller': 'https://ssp.paa-tips.com:44304',
'decisionLogicUrl': 'https://ssp.paa-tips.com:44304/ssp/decision-logic-url/index.js',
'interestGroupBuyers': ['https://dsp.paa-tips.com:44303'],
'resolveToConfig': true,
}
const result = await navigator.runAdAuction(auctionConfig)
const container = document.getElementById('paa-ad-container')
container.config = result
})
DSP
DSPは変更点はありません。
最後にwebpack.config.jsに 'SUPPORTS-LOADING-MODE': 'fenced-frame'
という設定を足します。
webpack.config.js
const path = require('path');
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'src')
},
allowedHosts: [ 'all' ],
headers: {
'Ad-Auction-Allowed': true,
'SUPPORTS-LOADING-MODE': 'fenced-frame',
}
}
}
iframeのときと同様にhttps://publisher.paa-tips.com:44302/publisher/index.html にアクセスして、広告が表示されたら成功です。
この時点でのcommitはこちらです。https://github.com/k-o-ta/paa-tips/tree/1e2ddf574a182332e894da853d355b7a5ab876f4
import/exportを使いたい
ここまでPA APIの一通りの処理をできるようになりました。本格的にPA APIを利用した処理を実装していくと、複雑な入札ロジックを別ファイルに分離したい、というユースケースが発生すると思います。SSPの decisionLogicUrl
やDSPの biddingLogicUrl
で配信されるjsファイルはimport/exportをサポートしていません。これらのファイルは auctionWorklet
というworkletで実行されるのですが、Spec には以下のように記載されていおり、ES modulesの仕組みが利用できません。
However, some key differences from traditional Worklets motivate us to create a new kind of script execution environment. In particular, they:
- Are not module scripts, and are instead evaluated as if they were classic scripts.
そこでいよいよwebpackを利用してバンドリングして配信してみましょう。
まずはwebpackにbuildの設定をします。entryポイントが複数あるので、それぞれのentry point分設定を書きます。また、devServerのstaticファイルの読み込み元をbundlingした出力先に合わせます。
webpack.config.js
const path = require('path');
module.exports = {
devtool: 'inline-source-map',
mode: 'development',
entry: {
'dsp/join-ad-interest-group/index': './src/dsp/join-ad-interest-group/index.js',
'dsp/bidding-logic-url/index': './src/dsp/bidding-logic-url/index.js',
'ssp/run-ad-auction/index': './src/ssp/run-ad-auction/index.js',
'ssp/decision-logic-url/index': './src/ssp/decision-logic-url/index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/dev'),
},
devServer: {
static: {
directory: path.join(__dirname, 'dist/dev')
},
allowedHosts: ['all'],
headers: {
'Ad-Auction-Allowed': true,
'SUPPORTS-LOADING-MODE': 'fenced-frame',
}
}
}
次にhtmlやbanner用の静的ファイルもbundlingの出力先と同じディレクトリにcopyする必要があります。copy-webpack-pluginを使いましょう。
package.json
...
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
...
webpack.config.js
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin")
module.exports = {
devtool: 'inline-source-map',
mode: 'development',
entry: {
'dsp/join-ad-interest-group/index': './src/dsp/join-ad-interest-group/index.js',
'dsp/bidding-logic-url/index': './src/dsp/bidding-logic-url/index.js',
'ssp/run-ad-auction/index': './src/ssp/run-ad-auction/index.js',
'ssp/decision-logic-url/index': './src/ssp/decision-logic-url/index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/dev'),
},
// copy pluginの設定
plugins: [
new CopyPlugin({
patterns: [
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.html'),
to: path.resolve(__dirname, 'dist/dev/'),
},
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.png'),
to: path.resolve(__dirname, 'dist/dev/'),
}
]
})],
devServer: {
static: {
directory: path.join(__dirname, 'dist/dev')
},
allowedHosts: ['all'],
headers: {
'Ad-Auction-Allowed': true,
'SUPPORTS-LOADING-MODE': 'fenced-frame',
}
}
}
加えてwebpack-dev-serverのhot reload機能のweb socketがconnectionエラーにならないようにdevServerに host: ‘127.0.0.1’
の設定を入れておきます。
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin")
module.exports = {
devtool: 'inline-source-map',
mode: "development",
entry: {
'dsp/join-ad-interest-group/index': './src/dsp/join-ad-interest-group/index.js',
'dsp/bidding-logic-url/index': './src/dsp/bidding-logic-url/index.js',
'ssp/run-ad-auction/index': './src/ssp/run-ad-auction/index.js',
'ssp/decision-logic-url/index': './src/ssp/decision-logic-url/index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/dev'),
},
plugins: [
new CopyPlugin({
patterns: [
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.html'),
to: path.resolve(__dirname, 'dist/dev/'),
},
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.png'),
to: path.resolve(__dirname, 'dist/dev/'),
}
]
})],
devServer: {
static: {
directory: path.join(__dirname, 'dist/dev')
},
allowedHosts: ['all'],
host: '127.0.0.1',
headers: {
'Ad-Auction-Allowed': true,
'SUPPORTS-LOADING-MODE': 'fenced-frame',
}
}
}
npm start
を再起動して、 https://advertiser.paa-tips.com:44301/advertiser/index.html にアクセスしたのち、https://publisher.paa-tips.com:44302/publisher/index.html を開くと、 Worklet error: https://dsp.paa-tips.com:44303/dsp/bidding-logic-url/index.js:4079 Uncaught Error: Automatic publicPath is not supported in this browser.
というエラーがconsoleに表示されると思います。実はこれはwebpack-dev-serverを使ってbuildした時に生成されるコードの中にauction workletで利用できない処理が含まれていることが原因です。
そこでwebpack-dev-serverを使ったbuildはやめて、webpackのbuildを使うようにして、webpack-dev-serverは再びただのweb-serverとして使うようにします。
まずwebpack.config.jsをbuild用とwebpack-dev-server用に分離します。
webpack.config.js
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin')
module.exports = {
devtool: 'inline-source-map',
mode: 'development',
entry: {
'dsp/join-ad-interest-group/index': './src/dsp/join-ad-interest-group/index.js',
'dsp/bidding-logic-url/index': './src/dsp/bidding-logic-url/index.js',
'ssp/run-ad-auction/index': './src/ssp/run-ad-auction/index.js',
'ssp/decision-logic-url/index': './src/ssp/decision-logic-url/index.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/dev'),
},
plugins: [
new CopyPlugin({
patterns: [
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.html'),
to: path.resolve(__dirname, 'dist/dev/'),
},
{
context: path.resolve(__dirname, 'src'),
from: path.resolve(__dirname, 'src/**/*.png'),
to: path.resolve(__dirname, 'dist/dev/'),
}
]
})],
}
webpack.config.server.js
const path = require('path');
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'dist/dev')
},
allowedHosts: ['all'],
host: '127.0.0.1',
headers: {
'Ad-Auction-Allowed': true,
'SUPPORTS-LOADING-MODE': 'fenced-frame',
}
}
}
次にpackage.jsonの起動スクリプトを直しましょう
"scripts": {
"start": "webpack watch",
"serve": "webpack-dev-server --config webpack.config.server.js"
},
npm start
, npm run serve
をそれぞれ実行し、https://advertiser.paa-tips.com:44301/advertiser/index.html にアクセスしたのち、https://publisher.paa-tips.com:44302/publisher/index.html を開くと、今度は Worklet error: https://dsp.paa-tips.com:44303/dsp/bidding-logic-url/index.js generateBid is not a function.
というエラーが表示されるはずです。実際にbuildされたファイルを確認すると、 globalなgenerateBid関数がなくなっていることがわかると思います。
そこで、auction worklet上で実行されている関数の定義を globalThis.generateBid = function(…){}
のような形に変更します。
src/dsp/bidding-logic-url/index.js
globalThis.generateBid = function(group, auctionSignals, perBuyerSignals, trustedBiddingSignals, browserSignals) {
console.log('generateBid',
JSON.stringify({
group,
auctionSignals,
perBuyerSignals,
trustedBiddingSignals,
browserSignals,
})
)
return {
bid: 1,
ad: {
adName: "adName"
},
render: group.ads[0].renderUrl,
}
}
globalThis.reportWin = function(auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
console.log("reportWin", JSON.stringify({
auctionSignals,
perBuyerSignals,
sellerSignals,
browserSignals
}))
}
src/ssp/decision-logic-url/index.js
globalThis.scoreAd = function(adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
console.log('scoreAd', JSON.stringify({
adMetadata,
bid,
auctionConfig,
trustedScoringSignals,
browserSignals,
}))
return bid;
}
globalThis.reportResult = function(auctionConfig, browserSignals) {
console.log('reportResult', JSON.stringify({
auctionConfig,
browserSignals
}))
}
再度 https://publisher.paa-tips.com:44302/publisher/index.html を表示すると広告が表示されるはずです。ここまでくると、importを使うこともできます。
src/dsp/bidding-logic-url/calc.js
export const calc = () => {
return 10
}
src/dsp/bidding-logic-url/index.js
import {calc} from "./calc";
globalThis.generateBid = function(group, auctionSignals, perBuyerSignals, trustedBiddingSignals, browserSignals) {
console.log('generateBid',
JSON.stringify({
group,
auctionSignals,
perBuyerSignals,
trustedBiddingSignals,
browserSignals,
})
)
console.log('calc', calc())
return {
bid: 1,
ad: {
adName: "adName"
},
render: group.ads[0].renderUrl,
}
}
globalThis.reportWin = function(auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
console.log('reportWin', JSON.stringify({
auctionSignals,
perBuyerSignals,
sellerSignals,
browserSignals
}))
}
typescriptを使いたい
オークションに関する複雑な処理を実装するにあたってTypeScriptを利用したくなることがあると思います。
まずはpackage.jsonに依存関係を追加しましょう。一緒にnpm startの起動scriptを変更しておきます。(tsc-watchを使わずにこれまでのnpm startとnpm run serveを使う方法でも問題ありません)
package.json
...
"scripts": {
"start': "tsc-watch --onSuccess \"sh -c 'npx webpack --config webpack.config.js ; npx webpack-dev-server --config webpack.config.server.js'\""
},
...
"devDependencies": {
...,
"ts-loader": "^9.4.2",
"tsc-watch": "^6.0.0",
"typescript": "^4.9.4",
...
}
...
また、ts-loaderの設定をwebpack.config.jsに反映させる必要があります。
webpack.config.js
entry: {
'dsp/join-ad-interest-group/index': './src/dsp/join-ad-interest-group/index.ts',
'dsp/bidding-logic-url/index': './src/dsp/bidding-logic-url/index.ts',
'ssp/run-ad-auction/index': './src/ssp/run-ad-auction/index.ts',
'ssp/decision-logic-url/index': './src/ssp/decision-logic-url/index.ts',
},
module: {
rules: [
{
use: 'ts-loader',
exclude: [/node_modules/],
},
],
},
resolve: {
extensions: ['.ts'],
},
tsconfig.jsonの設定も必要です
{
"compilerOptions": {
"sourceMap": true,
"moduleResolution": "node",
"outDir": "build",
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
]
}
ここら辺はプロジェクトの事情に合わせて変更してください
最後にjsファイルをtsファイルに変更すれば完了ですが、 navigator.runAdAuction()
などは型定義に存在しない関数なので追加するなり @ts-ignore
するなりの対応が必要です。
ここまでのcommitはこちらです。https://github.com/k-o-ta/paa-tips/tree/6d0bf14efa9aaa1c085385ccb366f13a37bee468
backend serverとの通信を行いたい
最後に、webフロントエンドからbackend serverと通信をする場合の説明をします。これは通常のAPI通信に限らず、PA APIの仕様にあるkey value serverとの通信や、report用のAPIも含まれます。
現時点でのcaddyとwebpack-dev-serverとの関係は以下のようになっており、caddyがTLS通信を終端しています。
例としてDSPが入札額を決める generateBid
関数の実行時にアクセスできるkey valueサーバーを構築しましょう。エンドポイントは /getvalues
とします。このkey valueサーバーをlocalhost:9000で起動します。PA APIの仕様上、このkey valueサーバーはIGのownerと同じオリジン(https://dsp.paa-tips.com:44303)である必要があります。そこでwebpack-dev-serverのproxy機能を利用し、 /getvalues
へのrequestを localhost:9000で起動しているkey valueサーバーに流します。
webpack.config.server.js
...
proxy: {
'/getvalues': {
target: 'http://localhost:9000'
}
}
...
図にすると下記のような感じです。
browserにとっては https://dsp.paa-tipc.com:44303/getvaluesにアクセスしていますが、実際のrequestはlocalhost:9000で起動しているkey-value-serverに届きます。
もちろんhttps://advertiser.paa-tipc.com:44303/getvaluesにアセスしても同様の結果になるので配慮は必要です。
backend serverの種類を増やしたい場合は、webpack.config.server.jsのproxyの設定を増やすことで対応できます。
終わりに
PA APIの開発を開始するための一通りの説明と躓きやすいポイントなどを解説してきました。今まではプロダクション環境でPA APIを使うためにはOriginTrialの設定も必要でしたが、GA以降は不要となります。この記事がPA APIを利用するための助けになれば幸いです。