ある晩、壁紙アプリの課金導線を Antigravity 2.0 に直させていました。指示は「レビュー導線のモーダルが二重に開くのを直して」。エージェントは実際の Chrome を起動し、アプリのプレビュー画面をボタン操作で何度も往復して修正を検証してくれます。ありがたい仕組みです。
ところが翌朝、AdMob の管理画面を開いて手が止まりました。前夜だけインプレッションが不自然に伸びていたのです。原因はすぐに分かりました。修正対象のモーダルは、広告を挟んだ直後に開く導線でした。エージェントは検証のたびにその画面を描画し、そのたびに本番の広告ユニットが実インプレッションを積んでいたのです。
個人開発では、広告収益が一つのアカウントに集約されています。無効なトラフィックが積み重なれば、収益が無効化されるだけでは済まず、アカウント全体が制限される可能性もあります。自己デバッグは強力ですが、その足元で広告が動いていることに、私自身しばらく気づいていませんでした。
なぜエージェントの自己デバッグが広告にとって危険なのか
人間が手で確認するときは、無意識に広告を避けます。「これはテストだから広告は見なかったことにしよう」という忖度が働きます。エージェントにその配慮はありません。画面に広告が表示されれば、それは Google から見れば一件の実インプレッションです。エージェントがボタンを探してクリックする過程で、広告の領域に触れてしまえば、それは実クリックとして記録されます。
しかも、自己デバッグが走る環境は開発マシンやクラウドのサンドボックスです。データセンターのIPや、短時間に同じ画面を何十回も往復する挙動は、Google の無効トラフィック検知がもっとも嫌うパターンそのものです。悪意はなくても、痕跡は「作為的な水増し」と見分けがつきません。
ここで大切なのは、一箇所の対策では漏れるという点です。テスト広告を強制しても、メディエーションのアダプターが別経路で本番のリクエストを出すことがあります。ネットワークを遮断しても、同意管理(UMP)のフォーム取得だけは通したいときがあります。だからこそ、性質の異なる三つの層を重ねます。
第一層: ビルド時にテスト広告ユニットを強制する
最初の層は、広告ユニットIDそのものを切り替えることです。Google はテスト専用の広告ユニットIDを公開しています。これらは常にテスト広告を返し、収益にも無効トラフィックにも影響しません。ビルド構成に「エージェント検証モード」を一つ足し、そのときはテストIDだけを返す関数に集約します。
iOS(Swift)では、広告ユニットの解決を一箇所に閉じ込めます。
import Foundation
enum AdEnvironment {
// 環境変数 or ビルド設定で注入。CI やエージェント実行時に AGENT_VERIFY=1 を渡す
static var isAgentVerify: Bool {
ProcessInfo.processInfo.environment[ "AGENT_VERIFY" ] == "1"
}
}
enum AdUnit {
// Google 公開のテスト広告ユニット(常にテスト広告を返す・収益ゼロ)
private static let testBanner = "ca-app-pub-3940256099942544/2934735716"
private static let testInterstitial = "ca-app-pub-3940256099942544/4411468910"
// 本番IDは Info.plist / 環境変数から。ここにハードコードしない
private static var prodBanner: String {
Bundle.main. object ( forInfoDictionaryKey : "AD_BANNER_UNIT" ) as? String ?? ""
}
private static var prodInterstitial: String {
Bundle.main. object ( forInfoDictionaryKey : "AD_INTERSTITIAL_UNIT" ) as? String ?? ""
}
static var banner: String {
AdEnvironment.isAgentVerify ? testBanner : prodBanner
}
static var interstitial: String {
AdEnvironment.isAgentVerify ? testInterstitial : prodInterstitial
}
}
さらに、テスト端末として自分の検証環境を登録しておくと、万一 ID の切り替えが漏れても、その端末からのリクエストはテスト扱いになります。二重の保険です。
import GoogleMobileAds
func configureAdsForVerification () {
let config = MobileAds.shared.requestConfiguration
if AdEnvironment.isAgentVerify {
// 検証環境の端末ハッシュを登録(本番ユーザーには影響しない)
config.testDeviceIdentifiers = [ "SIMULATOR" , "AGENT_DEVICE_HASH" ]
}
}
Android(Kotlin)でも考え方は同じです。BuildConfig にフラグを一つ足し、テスト端末を登録します。
import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.RequestConfiguration
fun configureAdsForVerification () {
if (BuildConfig.AGENT_VERIFY) {
val config = RequestConfiguration. Builder ()
. setTestDeviceIds ( listOf ( "AGENT_DEVICE_HASH" ))
. build ()
MobileAds. getInstance ().requestConfiguration = config
}
}
この層だけで、意図した広告表示はテスト広告に置き換わります。けれど、これは「アプリが素直に振る舞えば」の話です。次の層で、素直でない経路をふさぎます。
第二層: エージェント実行環境でネットワークを遮断する
自己デバッグは、あくまでアプリやサイトを外側から操作します。ならば、エージェントが動く環境そのもので、広告配信ドメインへの通信を止めてしまうのが確実です。テストIDの切り替えが漏れても、リクエストが外に出なければ本番のインプレッションは発生しません。
エージェントのサンドボックスや検証用コンテナで、広告配信の主要ドメインを解決不能にします。
#!/usr/bin/env bash
# block-ad-domains.sh — エージェント検証環境で広告配信ドメインを遮断する
set -euo pipefail
HOSTS = /etc/hosts
MARKER = "# --- agent-verify ad block ---"
if grep -q " $MARKER " " $HOSTS " ; then
echo "既に適用済みです"
exit 0
fi
cat << 'BLOCK' | sudo tee -a " $HOSTS " > /dev/null
# --- agent-verify ad block ---
127.0.0.1 googleads.g.doubleclick.net
127.0.0.1 pagead2.googlesyndication.com
127.0.0.1 securepubads.g.doubleclick.net
127.0.0.1 tpc.googlesyndication.com
127.0.0.1 www.googleadservices.com
# --- end agent-verify ad block ---
BLOCK
echo "広告配信ドメインを遮断しました(同意フォーム取得は別ドメインのため影響しません)"
ここでの落とし穴は、遮断しすぎることです。同意管理プラットフォーム(UMP)のフォーム取得や、アプリ本体のAPIまで止めてしまうと、検証したい導線そのものが動かなくなります。広告配信のドメインだけを狙って止め、それ以外は通す。この線引きが、検証の再現性を保つ鍵になります。同意フォームの初期化順序でつまずいた経験がある方は、ATT を取る前に広告SDKを初期化していた話 も合わせて読んでいただくと、順序と遮断の関係が掴みやすいはずです。
第三層: preflight ゲートで本番広告ビルドを弾く
三層目は、人間の判断ミスを機械で止める層です。第一層と第二層をどれだけ整えても、「今日はテストモードのフラグを立て忘れた」という一回で崩れます。そこで、自己デバッグを起動する直前に、本番の広告ユニットIDが有効なビルドかどうかを検査し、該当すれば起動を拒否します。
#!/usr/bin/env python3
"""preflight_ad_guard.py — 本番広告ビルドに対する自己デバッグ起動を拒否する。
ビルド生成物・設定ファイルに本番の AdMob ユニットIDが残っていれば exit 1。"""
import os
import re
import sys
from pathlib import Path
# Google 公開のテスト用アプリ/ユニットの共通プレフィックス
TEST_PUB = "ca-app-pub-3940256099942544"
# 本番ユニットの形式(テストプレフィックス以外の ca-app-pub-XXXX/YYYY)
UNIT_RE = re.compile( r "ca-app-pub- \d {16} / \d {6,} " )
def find_prod_units (root: Path) -> list[ str ]:
hits = []
targets = [ ".plist" , ".xml" , ".json" , ".js" , ".ts" , ".dart" , ".kt" , ".swift" ]
for path in root.rglob( "*" ):
if path.suffix not in targets or not path.is_file():
continue
try :
text = path.read_text( encoding = "utf-8" , errors = "ignore" )
except OSError :
continue
for m in UNIT_RE .findall(text):
if not m.startswith( TEST_PUB ):
hits.append( f " { path } : { m } " )
return hits
def main () -> int :
if os.environ.get( "AGENT_VERIFY" ) != "1" :
print ( "AGENT_VERIFY=1 が未設定です。検証は必ずこのフラグ下で行ってください。" )
return 1
root = Path(sys.argv[ 1 ]) if len (sys.argv) > 1 else Path( "." )
hits = find_prod_units(root)
if hits:
print ( "本番の広告ユニットIDを検出しました。自己デバッグを中止します:" )
for h in hits[: 20 ]:
print ( " -" , h)
return 1
print ( "本番広告ユニットは検出されませんでした。自己デバッグを許可します。" )
return 0
if __name__ == "__main__" :
sys.exit(main())
このスクリプトを、エージェントに自己デバッグを頼むワークフローの先頭に必ず挟みます。Antigravity のタスク定義でも、CI の検証ジョブでも、最初のステップを「preflight を通ること」に固定すれば、フラグの立て忘れ一回で本番広告が動く事故は起きません。使い捨てのプレビューへ検証を向ける発想については、実ブラウザ自己デバッグを使い捨てプレビューに向ける で扱った「向き先の設計」とも噛み合います。三層は、その一般原則を広告という具体的な副作用に落とし込んだものだと考えていただければと思います。
三層を入れて、実際に何が変わったか
導入の前後で、二週間ずつ計測しました。対象は、広告を挟む導線を持つ壁紙アプリ一本です。自己デバッグの実行回数はほぼ同数に揃えています。
指標
導入前(2週間)
導入後(2週間)
自己デバッグ由来と推定される計上インプレッション
約 480 件
0 件
広告領域への誤クリック(推定)
3 件
0 件
本番 eCPM への影響
ノイズで 約 4% 低下した週あり
実測で有意差なし
検証フローの追加所要時間
—
1 回あたり 約 2 秒 (preflight 実行)
数字としては地味です。けれど、自己デバッグ由来のインプレッションがゼロになったことの意味は、収益の増減では測れません。無効トラフィックの疑いをアカウントに残さないこと。それが、個人開発で広告を主収入に据える者にとっての本当の利得です。App Store でも Google Play でも、アカウントの健全性は一度失うと取り戻すのに時間がかかります。
一つだけ、想定と違ったことがあります。第一層のテストID切り替えだけで十分だろうと当初は考えていました。ところが、メディエーションを噛ませたインタースティシャルで、アダプターの一つが本番のエンドポイントへ問い合わせを出していました。第二層のネットワーク遮断がなければ気づかなかったはずです。私はこの経験から、三層は冗長ではなく、それぞれが別の穴をふさいでいると確信しました。
まとめ — まず preflight から始める
三層すべてを一度に入れる必要はありません。もし今日一つだけ足すなら、第三層の preflight ゲートをお勧めします。既存のアプリに手を入れずに、自己デバッグを起動する手前へ一枚の検査を挟むだけで、最悪の事故(本番広告ビルドでの検証)を止められるからです。テストIDの強制とネットワーク遮断は、そのうえで一つずつ重ねていけば十分に間に合います。
エージェントに任せる範囲が広がるほど、任せた先で何が動いているかを人間が把握しておく責任も増していきます。私自身、この一件で足元の広告に気づけたのは幸運でした。同じ場面で立ち止まる方の助けになれば幸いです。お読みいただき、ありがとうございました。