個人開発者として AdMob で月3万円前後を稼ぐような規模のアプリを運営していると、ある日突然 EEA(欧州経済領域)圏からの収益だけが急落することがあります。私自身、Rork で作って海外向けに出したアプリで、GDPR 同意周りの実装漏れを指摘する AdMob のメールを受け取ったことがあり、あのメールが届いたときの冷や汗は今でも覚えています。
原因を掘り下げていくと、ほぼ確実に UMP(User Messaging Platform)や ATT(App Tracking Transparency)の実装不備に行き着きます。Rork は本当に便利なツールで、UI から認証、データ管理まで自動生成してくれますが、広告収益化まわりの同意管理レイヤーは、現状ほぼ手動で組み込む必要があります。
ここではRork で生成したアプリに UMP と ATT を本番品質で組み込み、「GDPR 準拠」「AdMob ポリシー遵守」「収益を落とさない」の3つを同時に満たすための設計パターンを、実際に動くコードとともに解説します。
なぜ Rork アプリには同意管理の「盲点」が生まれやすいのか
Rork が自動生成する AdMob 連携コードは、多くの場合 AdMob.initializeAsync() を呼び出して広告リクエストを開始するシンプルな構成になっています。これだけでも広告は表示されますし、テストフェーズでは動作確認ができるので問題に気づきにくいのが落とし穴です。
しかし、この「素直な初期化」は以下の3つの重要なレイヤーを素通りしています。
ATT(iOS 14.5 以降) : IDFA(広告識別子)の取得には明示的なユーザー許可が必要
UMP(EEA・UK・スイス) : GDPR の下で、パーソナライズ広告を配信する前に同意ダイアログを表示する義務
初期化順序 : 同意情報を取得してから AdMob SDK を初期化しないと、非パーソナライズ広告モード(NPA)に固定され、CPM(千インプレッション単価)が2〜3割落ちる
EEA ユーザーからの収益は、実はアプリ全体収益の 30〜50% を占めるケースも多く、この「盲点」を放置すると月数万円単位で収益を落とす可能性があります。個人開発者にとっては看過できない金額です。
全体像:3つの同意レイヤーと正しい初期化順序
本番で正しく動作させるための初期化順序を、先に結論から示します。どの順番を守れば収益を落とさないかというと、以下の流れです。
アプリ起動直後に UMP の requestConsentInfoUpdate を呼び、同意状態を取得する
必要に応じて UMP の同意フォームを表示する(loadAndShowConsentFormIfRequired)
iOS の場合のみ、ATT ダイアログを表示する(requestTrackingAuthorization)
同意情報を AdMob / Firebase Analytics / RevenueCat などに伝播する
AdMob.initializeAsync() を呼び、広告リクエストを開始する
この順序を1箇所でも間違えると、広告リクエストが NPA モードで発行されてしまい、収益が目に見えて下がります。特に Rork が生成するデフォルト構成では、3と4の間に「ユーザーログイン画面」が挟まっていることが多いのですが、ここでログイン後に AdMob を初期化してしまうと、初回セッションの広告が全て NPA になってしまいます。
Step 1: UMP SDK の導入と初期化フロー
まずは UMP SDK を Rork プロジェクトに導入します。Rork は内部的に Expo/React Native を使っているので、react-native-google-mobile-ads に付属する UMP 実装を利用するのが最もスムーズです。
# UMP を含む AdMob SDK をインストール
npm install react-native-google-mobile-ads
# iOS の場合は Pod のインストールも必要
cd ios && pod install && cd ..
app.json(または app.config.ts)に AdMob の設定を追加します。Rork が生成する設定ファイルに直接追記する形です。
{
"expo" : {
"plugins" : [
[
"react-native-google-mobile-ads" ,
{
"androidAppId" : "ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY" ,
"iosAppId" : "ca-app-pub-XXXXXXXXXXXXXXXX~ZZZZZZZZZZ" ,
"userTrackingUsageDescription" : "広告のパーソナライズとアプリの改善のために、端末の識別情報を使用させていただきます。"
}
]
]
}
}
次に、UMP の初期化ロジックを実装します。以下は本番で動いているコードをベースにしたもので、エラーハンドリングも含まれています。
// src/consent/initializeConsent.ts
import mobileAds, {
AdsConsent,
AdsConsentStatus,
AdsConsentDebugGeography,
} from 'react-native-google-mobile-ads' ;
export type ConsentResult = {
canRequestAds : boolean ;
consentStatus : AdsConsentStatus ;
isGDPRRegion : boolean ;
error ?: string ;
};
export async function initializeConsent () : Promise < ConsentResult > {
try {
// Step 1: 同意情報をリクエスト
// __DEV__ では EEA ユーザーとしてデバッグする
const consentInfo = await AdsConsent. requestInfoUpdate (
__DEV__
? {
debugGeography: AdsConsentDebugGeography. EEA ,
testDeviceIdentifiers: [ 'YOUR_TEST_DEVICE_ID' ],
}
: undefined ,
);
// Step 2: 必要に応じて同意フォームを表示
if (
consentInfo.isConsentFormAvailable &&
consentInfo.status === AdsConsentStatus. REQUIRED
) {
await AdsConsent. loadAndShowConsentFormIfRequired ();
}
// Step 3: 広告リクエストが可能か判定
const canRequestAds = await AdsConsent. getConsentInfo (). then (
( info ) => info.canRequestAds ?? false ,
);
return {
canRequestAds,
consentStatus: consentInfo.status,
isGDPRRegion:
consentInfo.status === AdsConsentStatus. REQUIRED ||
consentInfo.status === AdsConsentStatus. OBTAINED ,
};
} catch (error) {
// ネットワーク切断などで同意情報取得に失敗した場合
// フェイルクローズ(広告配信を停止)ではなく、
// 前回の同意状態でフォールバックするのが Google 推奨パターン
console. error ( '[Consent] Failed to initialize:' , error);
return {
canRequestAds: false ,
consentStatus: AdsConsentStatus. UNKNOWN ,
isGDPRRegion: false ,
error: error instanceof Error ? error.message : 'Unknown error' ,
};
}
}
このコードの重要なポイントは、エラー時に canRequestAds: false を返してフェイルクローズする設計です。エラーを無視して広告を出すと、GDPR 違反になる可能性があります。ネットワークエラーの場合は少し我慢して次回起動時にリトライする方が安全です。
期待される動作は以下のとおりです。
EEA ユーザーが初めて起動したとき:同意フォームが表示され、選択後に canRequestAds: true が返る
非 EEA ユーザー(日本・米国など):同意フォームは表示されず、即座に canRequestAds: true
ネットワーク切断時:canRequestAds: false で広告リクエストはスキップされる
Step 2: ATT との統合パターン
iOS では UMP に加えて ATT(App Tracking Transparency)の許可も取得する必要があります。ここで注意したいのが、UMP → ATT の順番で表示する という点です。
なぜこの順番かというと、UMP の同意フォームには「広告のパーソナライズに関する詳細な選択肢」が含まれていて、ユーザーがここで「パーソナライズを許可しない」と選んだ場合、ATT ダイアログを出す必要すらなくなるからです。先に ATT を出してしまうと、「許可したのにパーソナライズされない」という矛盾状態になり、ユーザー体験が損なわれます。
// src/consent/requestATT.ts
import { Platform } from 'react-native' ;
import {
request,
PERMISSIONS,
RESULTS,
check,
} from 'react-native-permissions' ;
export async function requestATTIfNeeded () : Promise < boolean > {
if (Platform. OS !== 'ios' ) {
return true ; // Android は ATT 不要
}
// iOS 14.5 未満は ATT API 自体がない
const iosVersion = parseFloat (Platform.Version as string );
if (iosVersion < 14.5 ) {
return true ;
}
try {
// まず現在の状態をチェック(再表示を避ける)
const currentStatus = await check ( PERMISSIONS . IOS . APP_TRACKING_TRANSPARENCY );
if (currentStatus === RESULTS . GRANTED ) return true ;
if (currentStatus === RESULTS . DENIED ) return false ;
if (currentStatus === RESULTS . BLOCKED ) return false ;
// まだ未確定なら表示
const result = await request ( PERMISSIONS . IOS . APP_TRACKING_TRANSPARENCY );
return result === RESULTS . GRANTED ;
} catch (error) {
console. error ( '[ATT] Request failed:' , error);
return false ;
}
}
このコードは「既に回答済みなら再表示しない」という挙動になっています。ATT ダイアログは iOS の仕様上、ユーザーが一度回答したら設定アプリからしか変更できません。何度も request を呼んでも再表示はされないのですが、チェックせずに呼ぶと不要な API 呼び出しが発生するので、check を先に入れておくのがベストプラクティスです。
Step 3: 地域判定と同意状態の分岐
UMP 自体が内部で地域判定をしてくれるため、アプリ側で IP 判定などを実装する必要は基本的にありません。ただし、「EEA ユーザーだけに特定の UI を出す」といった用途では、UMP の consentStatus を地域判定の代替として使えます。
// src/consent/regionAware.ts
import { AdsConsent, AdsConsentStatus } from 'react-native-google-mobile-ads' ;
export async function isLikelyGDPRRegion () : Promise < boolean > {
const info = await AdsConsent. getConsentInfo ();
// REQUIRED = 同意が必要な地域(EEA/UK/スイス)
// OBTAINED = 同意取得済み(同意が必要な地域だが既に回答済み)
// NOT_REQUIRED = 同意不要(非 EEA)
return (
info.status === AdsConsentStatus. REQUIRED ||
info.status === AdsConsentStatus. OBTAINED
);
}
自前で IP 判定をやろうとすると、VPN 利用ユーザーで誤判定したり、IP データベースの更新コストがかかったりと、メンテナンスが大変です。UMP の判定は Google が管理している最新の地域データを使っているので、これに任せるのが最も安全で安定します。
Step 4: 同意状態を AdMob / Analytics / RevenueCat に伝播する
同意情報を取得しただけでは不十分で、実際に広告や分析ツールがその情報を反映する必要があります。react-native-google-mobile-ads は UMP 連携が内部で自動化されていますが、Firebase Analytics や RevenueCat では手動で設定する箇所があります。
// src/consent/propagateConsent.ts
import analytics from '@react-native-firebase/analytics' ;
import Purchases from 'react-native-purchases' ;
import { AdsConsent } from 'react-native-google-mobile-ads' ;
export async function propagateConsent () : Promise < void > {
const consentInfo = await AdsConsent. getConsentInfo ();
const canTrack = consentInfo.canRequestAds ?? false ;
// Firebase Analytics の同意モード
// これを呼ばないと EEA ユーザーに対して勝手にデータ収集してしまう
await analytics (). setAnalyticsCollectionEnabled (canTrack);
await analytics (). setConsent ({
ad_storage: canTrack ? 'granted' : 'denied' ,
ad_user_data: canTrack ? 'granted' : 'denied' ,
ad_personalization: canTrack ? 'granted' : 'denied' ,
analytics_storage: canTrack ? 'granted' : 'denied' ,
});
// RevenueCat の属性収集も同意に基づいて制御
await Purchases. setAttributes ({
$consentGiven: canTrack ? 'true' : 'false' ,
});
if ( ! canTrack) {
// 同意がない場合は IDFA 系の属性をクリア
await Purchases. collectDeviceIdentifiers (); // 内部で null に設定
}
}
このコードの肝は、Firebase Analytics の setConsent を必ず呼ぶことです。これを呼ばないと、setAnalyticsCollectionEnabled(false) にしていても、一部のバックグラウンドイベントが送信され続けてしまう仕様があります(v18.4.0 以降で修正されていますが、古いバージョンとの互換性のため明示的に呼ぶのが安全です)。
Step 5: 同意変更 UI — 設定画面から同意を撤回する
AdMob ポリシーでは、ユーザーが「後から同意を撤回できる導線」をアプリ内に用意することが必須とされています。これを用意していないと、AdMob の審査で警告が出たり、Play Store のポリシーチェックでリジェクトされることがあります。
// src/screens/SettingsScreen.tsx
import React from 'react' ;
import { View, Text, Button, Alert } from 'react-native' ;
import { AdsConsent, AdsConsentStatus } from 'react-native-google-mobile-ads' ;
export function PrivacySettingsSection () {
const [ isLoading , setIsLoading ] = React. useState ( false );
const handleRevokeConsent = async () => {
setIsLoading ( true );
try {
// 同意状態をリセット
await AdsConsent. reset ();
// 再取得を促す
await AdsConsent. requestInfoUpdate ();
const info = await AdsConsent. getConsentInfo ();
if (
info.isConsentFormAvailable &&
info.status === AdsConsentStatus. REQUIRED
) {
// 同意フォームを再表示
await AdsConsent. loadAndShowConsentFormIfRequired ();
}
Alert. alert (
'同意設定を更新しました' ,
'アプリを再起動すると新しい設定が完全に反映されます。' ,
);
} catch (error) {
Alert. alert (
'エラー' ,
'同意設定の更新に失敗しました。ネットワーク接続をご確認の上、もう一度お試しください。' ,
);
} finally {
setIsLoading ( false );
}
};
return (
< View style = {{ padding : 16 }} >
< Text style = {{ fontSize : 16 , fontWeight : '600' }} > プライバシー設定 </ Text >
< Text style = {{ marginTop : 8 , color : '#666' }} >
広告のパーソナライズに関する同意を変更できます。
</ Text >
< Button
title = {isLoading ? '処理中...' : '同意設定を変更する' }
onPress = {handleRevokeConsent}
disabled = {isLoading}
/>
</ View >
);
}
この画面は設定タブの「プライバシー」セクションなどに配置します。「同意設定を変更する」というボタン名にしているのは、「撤回」という言葉がネガティブに響くためです。ユーザーが心理的に押しやすい文言にすることで、UX を損なわずに法的要件を満たせます。
本番でよく詰まる落とし穴 6 選
実際に Rork アプリで UMP を実装していて、私や他の開発者が詰まったパターンを挙げます。どれも一度やらかすと修正に数時間〜数日かかる類いの問題なので、事前に頭に入れておく価値があります。
1. showIfRequired を呼ばずに AdMob を初期化してしまう
最も多い間違いです。requestInfoUpdate を呼んだだけで満足して、フォーム表示をスキップして AdMob を初期化すると、EEA ユーザーに対して同意を取っていない状態で広告リクエストが走ります。これが AdMob Publisher Policy 違反の典型パターンです。
2. ATT ダイアログを先に表示してしまう
iOS で request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY) を UMP より先に呼ぶと、ユーザーが混乱します。「許可」を押した後に UMP で「パーソナライズしない」を選ばれると、技術的には ATT は許可済みでも広告は非パーソナライズ配信になり、ユーザー体験としても矛盾します。必ず UMP → ATT の順番で。
3. Android 実機で同意ダイアログを確認しようとして出てこない
Android では AdsConsentDebugGeography.EEA を testDeviceIdentifiers と組み合わせないと、デバッグモードでも EEA 判定にならず同意フォームが出ません。テストデバイス ID は AdMob のログに出力されるので、最初の起動時にログを確認して設定ファイルに反映します。
4. 同意撤回 UI を作り忘れて App Store 審査でリジェクト
2024年以降、App Store Review Guidelines 5.1.1 に「ユーザーは同意を撤回できる」という項目が明文化されました。Play Store も同様です。撤回導線が設定画面にないと、審査でリジェクトされる可能性があります。
5. 非 EEA ユーザーに UMP を出してしまう
デバッグ時に debugGeography: AdsConsentDebugGeography.EEA を設定したまま本番リリースしてしまうと、非 EEA ユーザーにも UMP フォームが表示されます。これは UX 上の致命傷です(同意率が下がる → パーソナライズ広告が減る → CPM が落ちる)。__DEV__ で分岐するのを忘れずに。
6. 初回セッションだけ NPA モードになってしまう
UMP の同意取得が非同期なのに、AdMob の初期化を同期的に呼んでしまうと、初回起動時だけ「同意取得前の広告リクエスト」が走り、そのセッション中は NPA モードになります。必ず await initializeConsent() → await mobileAds().initialize() の順序で呼ぶこと。これだけで初回セッションの CPM が 20〜30% 改善することがあります。
AdMob Publisher Policy 違反から復旧する手順
もし既に AdMob から警告メールが来ている場合の復旧手順も書いておきます。私もこの手順で収益を回復させたことがあります。
まず上記の UMP 実装を入れた新バージョンをリリースする(TestFlight で動作確認してから本番反映)
AdMob 管理画面の「ポリシーセンター」で警告を受けたアプリを開き、「修正済みとしてマーク」を押す
48時間以内に Google 側の自動スキャンが走り、問題が解消されていれば警告が解除される
警告解除後、EEA からの広告配信が徐々に元に戻る(通常1週間以内)
収益が戻るまでには「新バージョン配布 → ユーザーのアップデート反映」のタイムラグがあるので、強制アップデートや更新促進のバナーを出すのも有効です。
テスト戦略 — 地域と状態のパターンを網羅する
UMP の動作確認は、大きく分けて以下の4パターンをテストすべきです。
EEA ユーザー・初回起動 : デバッグモードで EEA に設定 → 同意フォームが出る → 「許可」で canRequestAds: true になる
EEA ユーザー・同意拒否 : 同じくフォームを出し、「許可しない」で canRequestAds: false になる
非 EEA ユーザー : NOT_EEA に設定 → フォームは出ず即 canRequestAds: true
ネットワーク切断時 : 機内モードで起動 → requestInfoUpdate が失敗 → フォールバック動作
デバッグ用にビルドメニューから「同意状態をリセット」ボタンを出しておくと、何度もテストするときに便利です。本番ビルドでは __DEV__ ガードで隠します。
// デバッグビルドのみに表示
{__DEV__ && (
< Button
title = "[DEBUG] Reset Consent"
onPress = { async () => {
await AdsConsent . reset ();
Alert . alert ( 'Reset' , 'Consent state cleared. Restart the app.' );
}}
/>
)}
Rork の自動生成コードを本番品質まで引き上げるには、React Native 側の API の全体像を理解しておくと判断が速くなります。
Google 認定 CMP と自前 UMP のどちらを選ぶか
AdMob 内蔵の UMP SDK は無料で使えて、Rork アプリに最も導入しやすい選択肢です。ただし、機能面ではやや限定的で、「IAB TCF v2.2 の完全準拠」「Facebook や他のアドネットワークとの同意情報の連携」「同意履歴の法的証跡の保存」といった要件が必要になる規模の大きなアプリでは、Google 認定 CMP(Consent Management Platform)への移行を検討したくなる場面が出てきます。
代表的な Google 認定 CMP には Didomi、OneTrust、Usercentrics、iubenda などがあり、いずれも React Native 向けの SDK を提供しています。月額料金は MAU(月間アクティブユーザー)ベースで変動し、10 万 MAU 規模のアプリだと月額 2〜5 万円前後になる印象です。個人開発レベルでは AdMob 内蔵 UMP で十分ですが、以下のいずれかに該当するなら有料 CMP の検討価値があります。
AdMob 以外に Meta Audience Network や Unity Ads など複数のアドネットワークを統合している
広告以外にも Mixpanel や Amplitude などの分析ツールを使っていて、それぞれに同意情報を伝える必要がある
EEA 以外の地域法(米カリフォルニア州の CCPA、ブラジルの LGPD など)にも対応したい
GDPR の「同意履歴の証跡」としてサーバー側にログを残す必要がある
Rork アプリで AdMob のみを使っているうちは、内蔵 UMP で完結させるのが運用コストとのバランスが良いです。マネタイズ手段が増えてから、必要なタイミングで CMP に乗り換える、というのが個人開発者にとって現実的な判断だと思います。
EEA 以外で注意すべき規制地域 — CCPA・LGPD・PIPL
GDPR にばかり目が行きがちですが、近年は EEA 以外でも同種のプライバシー規制が増えています。Rork アプリを本格的にグローバル展開するなら、最低限以下の地域の規制も視野に入れる必要があります。
米カリフォルニア州(CCPA/CPRA) : 「Do Not Sell My Personal Information」リンクの設置義務。UMP は対応していないので、独自の Opt-Out 導線が必要
ブラジル(LGPD) : GDPR に近い内容だが、同意取得のタイミングと文言が一部異なる
中国(PIPL) : 個人情報の国外移転に事前同意が必要。Firebase など国外サーバーを使う場合は要注意
日本(改正個人情報保護法) : 2022年改正以降、Cookie 同様の識別子は「個人関連情報」として扱う必要あり
実装の観点では、まず AdMob 内蔵 UMP で EEA をカバーしつつ、CCPA については react-native-google-mobile-ads の設定で ccpaOptOut: true を渡す、日本向けには利用規約と同意チェックボックスを付ける、という段階的な対応で十分なケースが多いです。全規制に一度に対応しようとすると開発が止まるので、アプリのユーザー分布を見ながら影響の大きい地域から順に手を入れるのが現実的だと思います。
同意実装と Play の「データ収集の開示」がずれていないかを機械的に確かめる
UMP と ATT を正しく組み込んでも、それだけでは足りない場面があります。Google Play の「データセーフティ」セクションに申告した内容と、アプリが実際に組み込んでいる SDK の挙動が食い違っていると、同意フローが完璧でも審査で差し戻されます。
厄介なのは、この食い違いが自分の書いたコードからは生まれにくいところです。生まれるのは、ビルダーやテンプレートが自動で入れた SDK の分です。Rork が生成した雛形に解析ライブラリやクラッシュレポートが最初から入っていた場合、それらが集めるデータ種別まで自分で申告する必要があります。追加した記憶がないものほど、申告から漏れます。
そこで、依存関係から「申告が必要になりそうなデータ種別」を機械的に洗い出す小さなスクリプトを用意しておくと、提出前の確認が数秒で済みます。プロジェクト直下に置いて node audit-data-safety.mjs で実行します。
// audit-data-safety.mjs
import { readFileSync, existsSync } from "node:fs" ;
// SDK 名 → Play「データセーフティ」で申告が必要になりやすいデータ種別
const SDK_DATA_TYPES = {
"react-native-google-mobile-ads" : [ "Device or other IDs" , "App interactions" ],
"@react-native-firebase/analytics" : [ "Device or other IDs" , "App interactions" ],
"@react-native-firebase/crashlytics" : [ "Crash logs" , "Diagnostics" ],
"react-native-purchases" : [ "Purchase history" ],
"expo-location" : [ "Approximate location" , "Precise location" ],
"@amplitude/analytics-react-native" : [ "Device or other IDs" , "App interactions" ],
"posthog-react-native" : [ "Device or other IDs" , "App interactions" ],
};
function readJson ( path ) {
if ( ! existsSync (path)) return null ;
return JSON . parse ( readFileSync (path, "utf8" ));
}
const pkg = readJson ( "./package.json" );
if ( ! pkg) {
console. error ( "package.json が見つかりません。プロジェクト直下で実行してください。" );
process. exit ( 2 );
}
const deps = Object. keys ({ ... pkg.dependencies, ... pkg.devDependencies });
const required = new Map (); // データ種別 → 由来 SDK の配列
for ( const dep of deps) {
for ( const type of SDK_DATA_TYPES [dep] ?? []) {
if ( ! required. has (type)) required. set (type, []);
required. get (type). push (dep);
}
}
const declared = new Set ( readJson ( "./data-safety.declared.json" )?.declared ?? []);
const missing = [ ... required. keys ()]. filter (( t ) => ! declared. has (t));
const extra = [ ... declared]. filter (( t ) => ! required. has (t));
console. log ( `検出した SDK: ${ deps . filter (( d ) => SDK_DATA_TYPES [ d ]). length } 件` );
for ( const [ type , sources ] of required) {
const mark = declared. has (type) ? "OK " : "MISS" ;
console. log ( ` ${ mark } ${ type } ← ${ sources . join ( ", " ) }` );
}
if (extra. length ) console. log ( ` \n 申告済みだが SDK 由来が見つからない項目: ${ extra . join ( ", " ) }` );
if (missing. length ) {
console. error ( ` \n 申告漏れの可能性: ${ missing . length } 件` );
process. exit ( 1 );
}
console. log ( " \n 申告漏れは検出されませんでした。" );
data-safety.declared.json には、Play Console で実際に申告した種別をそのまま書き写しておきます。Console の画面が正、こちらは写しという関係です。
{
"declared" : [ "Device or other IDs" , "Crash logs" , "Purchase history" ]
}
広告・解析・クラッシュ・課金・位置情報を入れた構成で実行したところ、出力はこうなりました。
検出した SDK: 5 件
OK Device or other IDs ← react-native-google-mobile-ads, @react-native-firebase/analytics
MISS App interactions ← react-native-google-mobile-ads, @react-native-firebase/analytics
OK Crash logs ← @react-native-firebase/crashlytics
MISS Diagnostics ← @react-native-firebase/crashlytics
OK Purchase history ← react-native-purchases
MISS Approximate location ← expo-location
MISS Precise location ← expo-location
申告漏れの可能性: 4 件
意図的に3種別だけ申告した状態を作ったところ、4件の漏れが出ました。注目したいのは位置情報です。expo-location は画面のどこかに「近くのお店を探す」程度の機能を足したときに入りがちで、広告の同意設計とは別の話として頭から抜け落ちます。それでも Play から見れば同じ「収集しているデータ」です。
process.exit(1) で終わるので、EAS Build の事前フックや CI の一段目に差し込めば、申告漏れのまま提出する経路そのものを塞げます。
この対応表は完全なものではありません。自分が使っている SDK を足していく前提の骨組みです。SDK を追加した日に一行足す、という運用にしておくと、半年後の自分が助かります。
対象 API レベルの引き上げと同時に壊れやすい箇所
Google Play は 2026年8月31日以降、新規アプリと既存アプリの更新の双方に対象 API レベル 36(Android 16)以上を求めます。間に合わない場合は Play Console の期限延長フォームから申請すれば11月1日まで配信を継続できますが、申請そのものを期限内に済ませる必要があります。
これは同意管理とは別のテーマに見えて、実際には同じ作業日にぶつかります。対象 API レベルを上げると、広告 SDK と Firebase 系の更新が芋づる式に必要になり、UMP を呼んでいる箇所が一緒に動くためです。
生成物の対象 API レベルは、まず自分の目で確かめてください。ビルダーが出力した雛形の値をそのまま信じないことが要点です。
# app.json / app.config.js 側の指定を確認する
grep -rn "targetSdkVersion\|compileSdkVersion" app.json app.config.js 2> /dev/null
# prebuild 済みなら生成された Gradle 側も確認する
grep -rn "targetSdkVersion" android/build.gradle android/app/build.gradle 2> /dev/null
値が見つからない場合は Expo SDK の既定値が使われています。既定値は SDK のバージョンによって変わるため、npx expo config --type introspect で解決後の設定を出力し、そこに現れる値を根拠にするほうが確実です。
引き上げの前後で、同意まわりについては次の3点を実機で確認しています。
確認項目 確認方法 見落としたときの症状
UMP フォームが引き続き表示されるか デバッグ地域を EEA に固定して初回起動 同意なしで広告リクエストが飛び、非パーソナライズに固定される
同意撤回 UI が動くか 設定画面から撤回 → 再起動 → 再表示されるか 審査で撤回導線なしと判断される
広告 SDK の初期化が同意取得より後か 初期化前後にログを入れて順序を目視 初回セッションだけ収益が落ちる
順序の確認は、凝ったツールを持ち出すより console.log を2行入れて実機のログを眺めるほうが速いです。SDK を上げたあとに初期化のタイミングが変わっていた、という経験を何度かしてから、この地味な確認を省かないようにしています。
期限が迫っている時期ほど、対象 API レベルだけを上げて配信を通し、同意まわりの再確認を後回しにしたくなります。ただ、収益が落ちる形の不具合は静かに進行して、気づいたときには数週間分の差になっています。上げた日に、同じ日のうちに見ておくことをおすすめしたいところです。
収益への影響をモニタリングする
UMP を実装したあと、「収益が本当に回復したか」を継続的に見るためのダッシュボードを作っておくと、次のトラブル時の発見が早くなります。AdMob 管理画面の「レポート」で以下の指標を週次で見る習慣をつけるのがおすすめです。
国別 eCPM : EEA 圏だけ極端に低い場合は UMP の同意率が低い(= フォームの文言が悪い、ボタンの配置が悪いなど)
広告リクエスト数 vs マッチ率 : マッチ率が 80% を大きく下回っていたら NPA モードに固定されている可能性
広告フォーマット別の収益 : リワード広告など NPA でも単価が落ちにくいフォーマットの割合
同意率は UMP のフォーム文言やボタンの配置で 10〜20% 変わることがあります。私は「パーソナライズ広告にご協力いただくと、より関連性の高い広告が表示されます」のように、ユーザーにとってのメリットを一言添えるだけで、同意率が上がった経験があります。押し付けがましくない範囲で工夫する価値はあります。
全体を振り返って — 今日着手すべき最初の一歩
UMP と ATT の実装は、一度きちんと組み込んでしまえばその後ほぼメンテナンスが不要な領域です。逆に言えば、放置したままグローバル展開すると、ある日突然 AdMob から警告が来て数日間収益がゼロになる、というリスクが常につきまといます。
まずやるべきは、自分のアプリが今 EEA ユーザーに対して UMP フォームを表示しているかを確認することです。TestFlight や Android 実機で AdsConsentDebugGeography.EEA を設定して起動し、フォームが出なければ即実装タスクとして優先度を上げるのがおすすめです。実装自体はこの記事のコードをそのままコピーして調整すれば、数時間で本番に出せるレベルにまで仕上がります。
関連する実装パターンについては、Rork の ATT 許諾率を最大化する AdMob 収益化戦略 と Google Play のデータセーフティ申告を直すための手順 も併せて読むと、iOS と Android の両方で完結した知識が身につきます。