はじめに

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と一致している必要があります
        • 今の段階ではまだ実装する必要はなく、オンデバイスオークションを実行する段階で実装します
      • ads: 入札に勝利した際に表示される広告のURL
        • このURLは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に何かエラーメッセージが表示されている可能性がありますので確認してください。

joinAdInterestGroup実行結果

オンデバイスオークションを実行する

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を実装したURLseller 引数のオリジンと同一である必要があります。

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()
        • 入札結果などの通知を行う

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 のいずれかひとつであることに注意してください。
    • 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 を再実行

広告を描画する

さて、ここまででオンデバイスオークションの実行までが完了しました。オークションに勝利した場合、広告を表示させることができますが、ここにもいくつか注意するポイントがあります。まず、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 にアクセスして、以下のような表示がされたら成功です。

広告が表示されているpublisherページ

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を利用するための助けになれば幸いです。