私が iOS 14.5 のアップデート後に最初に痛感したのは、「広告を打っても、どのキャンペーンが効いているのか以前のように見えない」という事実でした。前まではざっくり眺めていれば成果が分かっていたのに、ある日急に管理画面のインストール数が半分になり、CPA が二倍になったように見える。実際にはユーザーが減ったわけではなく、IDFA を取得できないことで計測がズレているだけだったのですが、原因に辿り着くまでに数週間を消費しました。
Rork で作ったアプリでも事情はまったく同じで、SDK 統合や SKAdNetwork 4.0 の Conversion Value 設計を疎かにすると、広告予算をいくら積んでも判断材料が手に入りません。ここでは私が個人開発のアプリ複数本で運用してきた中で「これだけはやっておくべき」と感じた構成を、コードと判断軸まで含めてまとめます。読み終えるころには、AppsFlyer / Adjust のどちらを選ぶべきかの基準と、Rork (Expo + React Native) プロジェクトで本番運用に耐える実装が手元に揃っているはずです。
なぜ iOS 14.5 以降のアトリビューションは難しくなったのか
iOS 14.5 で App Tracking Transparency(以下 ATT)が必須化されてから、IDFA を使ったクロスアプリ計測は事実上不可能になりました。代わりに登場したのが Apple 純正の SKAdNetwork(SKAN)で、媒体(広告ネットワーク)が広告を表示し、アプリ側がインストール後の特定イベントを発火すると、Apple のサーバを経由して匿名化された Conversion Value だけが媒体に返ります。
SKAN 4.0 ではこれが大幅に拡張され、fine(0〜63 の数値)と coarse(low / medium / high)の二段階で値を返せるようになり、ポストバックも最大3回(postback windows 0〜2)届くようになりました。LTV の長期的な変化を計測できるようになった一方、設計の複雑さは数倍に増えています。
ここで多くの開発者が陥るのが、「AppsFlyer / Adjust を入れれば自動的に計測できる」という誤解です。実際には Conversion Value のスキーマは自分のアプリに合わせて定義する必要があり、ATT の許諾フローもアプリ側で実装しなければ何も計測されません。SDK は箱を開けただけで動くものではなく、設計と運用が伴って初めて意味を持ちます。
AppsFlyer / Adjust のどちらを選ぶべきか — 実体験に基づく判断軸
私はこれまでに同じアプリを AppsFlyer と Adjust の両方で運用したことがあり、両者には明確な向き不向きがあると感じています。先に結論を書くと、初めて MMP(Mobile Measurement Partner)を導入する個人開発者には AppsFlyer を、エージェンシーや広告代理店経由で大規模出稿する場合は Adjust を推す、というのが私の今の判断です。
AppsFlyer は管理画面のグラフが直感的で、SKAN ポストバックの可視化も分かりやすく、無料枠で始められる点が明確に優しいです。OneLink(同社のディープリンクサービス)との統合もスムーズで、TikTok や Meta の管理画面とも連携が手早く済みます。一方で SDK サイズはやや重く、Hermes 環境で稀にビルド時間が伸びる印象があります。
Adjust はエンタープライズ寄りで、データの精度と監査ログの品質が高いです。広告詐欺(広告クリック詐称)対策が標準で組み込まれており、月間広告予算が数千万円を超える運用には強い味方です。ただし無料プランはなく、料金は従量課金で、月間トラッキングイベント数が増えるとコストが急に膨らむ点は覚悟が必要です。
迷ったら AppsFlyer から始めて、規模に応じて Adjust への移行を検討するのが現実的です。両者の SDK は仕様こそ違いますが、抽象化レイヤーを自分で1枚かませておけば乗り換えはそれほど大変ではありません。ここでは AppsFlyer を主軸に、Adjust 固有の差分も都度補足する形で進めます。
AppsFlyer SDK を Rork(Expo + React Native)に正しく組み込む
Rork は内部的に Expo の Managed Workflow をベースにしているので、ネイティブモジュールを追加する場合は Config Plugin を使います。AppsFlyer は公式に react-native-appsflyer を提供しており、Expo SDK 51 以降であれば Prebuild 経由でほぼコードを書かずに導入できます。
# Step 1: 依存パッケージの追加
npx expo install react-native-appsflyer
# Step 2: app.json に Config Plugin を登録(必須)
# プラグインが Info.plist に SKAdNetworkItems を自動注入する
app.json の plugins には次の形で記述します。isDebug を true にすると AppsFlyer のデバッグログが Xcode コンソールに流れるので、開発中は有効化しておくと配線確認が楽です。
{
"expo" : {
"plugins" : [
[
"react-native-appsflyer" ,
{
"shouldUseStrictMode" : true
}
]
],
"ios" : {
"infoPlist" : {
"NSUserTrackingUsageDescription" : "あなたに合った広告とコンテンツをお届けするために、限定的な情報を計測させてください。設定からいつでも変更できます。"
}
}
}
}
SDK の初期化は、ATT のステータスが確定してから呼び出すのが鉄則です。アプリ起動と同時に初期化してしまうと、IDFA がまだ解放されていない状態で計測が始まり、後から ATT を許可してもデータの先頭部分が欠損したまま固定されてしまいます。
// src/lib/attribution.ts
import appsFlyer from 'react-native-appsflyer' ;
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, PermissionStatus } from 'expo-tracking-transparency' ;
import { Platform } from 'react-native' ;
const APPSFLYER_DEV_KEY = process.env. EXPO_PUBLIC_APPSFLYER_DEV_KEY ! ;
const APPSFLYER_APP_ID = process.env. EXPO_PUBLIC_APPSFLYER_APP_ID ! ; // App Store の数値ID
export async function initAttribution () : Promise < void > {
// 1. ATT のステータスを最終確定させる(許諾済み/拒否済みなら何もしない)
if (Platform. OS === 'ios' ) {
const current = await getTrackingPermissionsAsync ();
if (current.status === PermissionStatus. UNDETERMINED ) {
// 必要に応じて事前説明 UI を出してから次の関数を呼ぶ
await requestTrackingPermissionsAsync ();
}
}
// 2. ATT 確定後に SDK を初期化(順序が逆だと IDFA を取りこぼす)
return new Promise (( resolve , reject ) => {
appsFlyer. initSdk (
{
devKey: APPSFLYER_DEV_KEY ,
isDebug: __DEV__,
appId: APPSFLYER_APP_ID ,
onInstallConversionDataListener: true , // 初回インストール属性を取得
onDeepLinkListener: true , // OneLink ディープリンクをハンドリング
timeToWaitForATTUserAuthorization: 60 , // ATT 許諾待ち最大秒数
},
( result ) => {
console. log ( '[Attribution] init success' , result);
resolve ();
},
( error ) => {
console. warn ( '[Attribution] init failed' , error);
reject (error);
},
);
});
}
timeToWaitForATTUserAuthorization を 60 秒に設定しているのが重要なポイントです。ATT のシステムダイアログを出してから許諾結果を返すまでに時間がかかる場合があり、SDK 初期化を急ぎすぎると IDFA を空のまま送ってしまいます。十分な余裕を持たせ、許諾後に SDK 側で改めて IDFA を読み直してくれる挙動を活かしましょう。
期待する動作: 初回起動時に ATT ダイアログが表示され、ユーザーが許可した瞬間に AppsFlyer の appsflyer_id と idfa が紐づいた状態で初回インストールイベントが送信されます。Xcode のコンソールに [Attribution] init success が出力されれば配線は OK です。
SKAdNetwork 4.0 の Conversion Value 設計
SKAN 4.0 で最も時間をかけて設計すべきなのが Conversion Value のスキーマです。fine 値は 0〜63、coarse 値は low / medium / high の三段階。一度配信が始まったあとに変えるとデータが分断されるので、最初に「どのイベントを何点に重み付けするか」を綿密に決めましょう。
私は次のような重み付けを基本形として使っています。アプリの種類によって変える必要はありますが、考え方の出発点として参考にしてください。
起動完了: 1 点
オンボーディング完了: 4 点
初回コンテンツ閲覧: 8 点
アカウント作成: 16 点
課金トライアル開始: 32 点
これを fine の 0〜63 にマッピングし、coarse は LTV の上位 30% を high、上位 70% を medium、それ以外を low に振り分けます。coarse は postback window 1(インストール後 3〜7 日)で最も効果を発揮するので、サブスクリプション型のアプリでは特に重要です。
// src/lib/conversion-value.ts
import appsFlyer from 'react-native-appsflyer' ;
type Event =
| 'app_open'
| 'onboarding_completed'
| 'first_content_view'
| 'account_created'
| 'trial_started' ;
const SCORE_MAP : Record < Event , number > = {
app_open: 1 ,
onboarding_completed: 4 ,
first_content_view: 8 ,
account_created: 16 ,
trial_started: 32 ,
};
let cumulativeScore = 0 ;
export async function reportEvent ( event : Event , payload ?: Record < string , unknown >) : Promise < void > {
// 1. AppsFlyer に通常イベントを送信(ダッシュボードで分析用)
appsFlyer. logEvent (event, payload ?? {}, () => {}, ( err ) => {
console. warn ( '[Attribution] logEvent error' , err);
});
// 2. SKAN 用に累積スコアを更新(既存値より下げない)
const next = Math. min ( 63 , cumulativeScore + SCORE_MAP [event]);
if (next > cumulativeScore) {
cumulativeScore = next;
appsFlyer. updateConversionValueWithErrorCallback (
next,
( e ) => console. warn ( '[Attribution] updateCV error' , e),
);
}
}
Conversion Value を「下げてはいけない」という制約は重要です。SKAN は単調増加でしか値を更新できず、誤って下方修正しようとすると無視されます。設計段階で「最大値が 63 を超えないか」「途中の重み付けが反転しないか」を必ず机上検証してください。
期待する出力: AppsFlyer のダッシュボードで Cohort Report → SKAN Postbacks を開くと、postback window 0/1/2 のそれぞれで Conversion Value の分布が確認できるようになります。
ATT 同意 UI を設計し、opt-in 率を最大化する
ATT の opt-in 率はアプリの収益を直接左右します。私の感覚では、デフォルトの ATT ダイアログだけを出した場合の opt-in 率は 25〜35% 程度ですが、事前説明 UI(pre-prompt)をきちんと作り込むと 55〜65% まで上がります。倍近い違いです。
ここでの設計の肝は「許可した方がユーザーにとってもメリットがある」と感じてもらえる文言にすることです。「広告のために必要です」と書くと拒否率が跳ね上がり、「あなたに合ったコンテンツを届けるために」と書くと許諾率が上がる、という現象は何度も観測しました。
// src/components/PrePromptATT.tsx
import { useEffect, useState } from 'react' ;
import { Modal, Text, View, Pressable, StyleSheet } from 'react-native' ;
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, PermissionStatus } from 'expo-tracking-transparency' ;
import { initAttribution } from '../lib/attribution' ;
export function PrePromptATT () {
const [ visible , setVisible ] = useState ( false );
useEffect (() => {
( async () => {
const { status } = await getTrackingPermissionsAsync ();
if (status === PermissionStatus. UNDETERMINED ) setVisible ( true );
})();
}, []);
const onAllow = async () => {
setVisible ( false );
await requestTrackingPermissionsAsync (); // ここで OS のダイアログが出る
await initAttribution (); // 許諾結果が確定してから SDK を立ち上げる
};
if ( ! visible) return null ;
return (
< Modal animationType = "fade" transparent >
< View style = { styles.backdrop } >
< View style = { styles.card } >
< Text style = { styles.title } >あなたに合ったおすすめのために</ Text >
< Text style = { styles.body } >
次の画面で「許可」を選んでいただくと、興味に近いコンテンツや
お得な情報を表示できるようになります。設定からいつでも変更できます。
</ Text >
< Pressable onPress = { onAllow } style = { styles.cta } >
< Text style = { styles.ctaText } >つづける</ Text >
</ Pressable >
</ View >
</ View >
</ Modal >
);
}
const styles = StyleSheet. create ({
backdrop: { flex: 1 , backgroundColor: 'rgba(0,0,0,0.5)' , justifyContent: 'center' , padding: 24 },
card: { backgroundColor: '#fff' , borderRadius: 16 , padding: 24 },
title: { fontSize: 18 , fontWeight: '700' , marginBottom: 8 },
body: { fontSize: 14 , lineHeight: 22 , color: '#444' , marginBottom: 16 },
cta: { backgroundColor: '#000' , paddingVertical: 14 , borderRadius: 12 , alignItems: 'center' },
ctaText: { color: '#fff' , fontSize: 15 , fontWeight: '600' },
});
この pre-prompt は、オンボーディングの最後(アプリの価値をある程度体験してもらった後)に出すのがおすすめです。起動直後に出すと「まだ何もしていないのに広告のために確認された」という印象になりやすく、拒否率が上がります。
ATT 同意管理を CMP(Consent Management Platform)と組み合わせて GDPR と統合的に処理したい場合は、別記事のRork × UMP・ATT で広告同意を本番運用する も合わせて読んでみてください。
ディープリンク(Universal Links / OneLink / Adjust Link)統合
広告流入を計測するうえで欠かせないのがディープリンクです。Universal Links だけだと「広告クリックからインストールまで」が紐づかないため、AppsFlyer の OneLink もしくは Adjust の Tracker Link を併用して、Deferred Deep Link(インストール後に意図したページへ遷移する仕組み)を実装する必要があります。
// src/lib/deep-link.ts
import appsFlyer from 'react-native-appsflyer' ;
import { router } from 'expo-router' ;
let unsubscribeOnDeepLink : (() => void ) | null = null ;
export function setupDeepLinking () {
// OneLink からのリンクを受け取り、適切な画面へ遷移
unsubscribeOnDeepLink = appsFlyer. onDeepLink (( res ) => {
if (res.deepLinkStatus !== 'FOUND' ) return ;
const data = res.data as { deep_link_value ?: string ; sub1 ?: string };
// deep_link_value にスキーマを定義(例: "campaign:summer-sale")
if (data.deep_link_value?. startsWith ( 'campaign:' )) {
const campaignId = data.deep_link_value. split ( ':' )[ 1 ];
router. push ( `/campaign/${ campaignId }` as never );
return ;
}
if (data.deep_link_value?. startsWith ( 'content:' )) {
const contentId = data.deep_link_value. split ( ':' )[ 1 ];
router. push ( `/content/${ contentId }` as never );
}
});
// 念のため初回インストール時の Conversion Data も拾う
appsFlyer. onInstallConversionData (( res ) => {
if (res.data?.is_first_launch === 'true' && res.data?.deep_link_value) {
const value = String (res.data.deep_link_value);
console. log ( '[DeepLink] first launch with' , value);
}
});
}
export function teardownDeepLinking () {
unsubscribeOnDeepLink ?.();
unsubscribeOnDeepLink = null ;
}
deep_link_value という汎用キーに「ドメイン:ID」形式で値を入れる設計にすると、media partner が変わってもアプリ側のロジックを書き換えずに済みます。私は campaign:, content:, referral: の三種類を最初から決めておき、新しい広告先が増えてもこの命名規則で吸収できる形にしています。
ディープリンクの基礎についてはRork × Universal Links と Deep Linking の完全ガイド で図解しているので、Universal Links の apple-app-site-association ファイル設置でつまずいている方はそちらも参照してください。
広告 SDK を本番リリース前にデバッグする実践テクニック
ここまでの実装が一通り終わったら、必ず本番リリース前に「テスト計測」を1サイクル回してください。本番でズレが発覚すると、広告予算を消費しながらの修正となり傷が大きくなります。
私は次の手順をリリース前のチェックリストにしています。
AppsFlyer 管理画面の「Test Devices」に自分の端末を登録(IDFV か Custom ID で識別)
アプリをデバッグビルドで起動し、logEvent 呼び出しが Live Activity に流れるか確認
SKAN ポストバックは In-app Events → SKAN のセクションで擬似ポストバックを発生させ、コンバージョン値が想定通りに更新されるかを目視
App Store の TestFlight ビルドで、別のデバイスから OneLink を踏み、インストール後に Deferred Deep Link が正しく発火するかを実機検証
1〜4 が確認できたら本番ストアへの提出
// src/lib/attribution-debug.ts
import appsFlyer from 'react-native-appsflyer' ;
/**
* 本番リリース直前にだけ呼び出すデバッグ用ヘルパー。
* Test Devices 登録後にこの関数を1度実行し、
* AppsFlyer 管理画面の Live Events に表示されるか確認する。
*/
export function fireDebugEvents () {
if ( ! __DEV__) return ; // 本番ビルドでは絶対に実行しない
appsFlyer. logEvent ( 'debug_app_open' , { source: 'self_test' }, () => {}, () => {});
appsFlyer. logEvent ( 'debug_onboarding_completed' , { variant: 'A' }, () => {}, () => {});
appsFlyer. logEvent ( 'debug_account_created' , { provider: 'apple' }, () => {}, () => {});
}
期待する出力: AppsFlyer の Live Events 画面に 3 つのイベントが 1〜2 分以内に表示されれば、ATT・SDK 初期化・ネットワーク到達がすべて正常に機能している証拠です。
よくある間違い・落とし穴
ここまで実装してもまだ計測がズレるとしたら、十中八九は次のいずれかに当てはまっています。私自身が一度はやらかしたものばかりなので、リリース前のチェックリストとして使ってください。
ATT 許諾を求めるタイミングが早すぎる : 起動と同時に ATT ダイアログを出すと拒否率が 70% を超えがちです。ユーザーがアプリの価値を体験した後に求めるのが鉄則で、私はオンボーディング最終ステップに配置しています。
SKAdNetwork ID リストが古い : 広告ネットワークごとに SKAdNetwork ID が違い、Info.plist の SKAdNetworkItems に追加されていないとそのネットワーク経由のインストールが計測されません。react-native-appsflyer は最新の ID をプラグイン経由で注入してくれますが、自前で書き加えた他媒体は手動更新が必要です。半年に一度はチェックしましょう。
Conversion Schema を後から大幅変更する : SKAN は単調増加でしか値を更新できません。途中で重み付けを反転させると過去データと整合しなくなり、レポートが事実上使い物になりません。スキーマは仮置きでもよいので、配信前に必ず固定してから走らせましょう。
Hermes / Expo Modules のバージョン不整合で SDK 初期化失敗 : Expo SDK のメジャーアップデート後に react-native-appsflyer のメジャーバージョンを上げ忘れると、ネイティブモジュールのリンクが解けて起動時にクラッシュします。アップデートは Expo SDK と AppsFlyer SDK をセットで揃えるのが安全です。
デバッグビルドで本番イベントが流れる : __DEV__ ガードを忘れると、開発中の logEvent 呼び出しが本番計測に混入し、Conversion Rate を歪めます。ヘルパー関数で必ず if (__DEV__) を挟むか、AppsFlyer の setIsSandbox(true) をデバッグ時に呼んでサンドボックス環境に隔離しましょう。
App Store Connect の Bundle ID と AppsFlyer の App ID が一致していない : 私はリリースの直前にこれで一度詰まりました。Bundle ID をリブランドした際に AppsFlyer 側の設定を更新し忘れ、初日の計測がすべて欠損しました。リネーム時は両側を必ず確認してください。
TikTok / Meta / Apple Search Ads と組み合わせた最適化ループ
ここからは、計測した数値をどう運用に活かすかという視点で締めくくります。アトリビューションは「導入して終わり」ではなく、媒体ごとに Conversion Value 設計を微調整して LTV を最大化するループに入ってこそ意味を持ちます。
媒体ごとの特性を私は次のように捉えています。
TikTok Ads : 認知獲得に強く、fine の低スコア帯(起動・オンボーディング完了)でターゲティング最適化が効きやすい。クリエイティブの賞味期限が短い(2〜3週間で枯れる)ため、coarse まで見て高 LTV クリエイティブを残す運用が向いています。
Meta Ads : 高 LTV ユーザーの獲得に強く、coarse の high レンジで配信最適化を回すと CPI は上がるが ROAS は伸びる傾向。AAA(Advantage+ App Campaigns)と SKAN 4.0 の相性は良い。
Apple Search Ads : 検索意図が明確なユーザーが入ってくるので、課金率と継続率がもっとも高い。Apple Search Ads は SKAN ポストバックを直接受け取るため、AppsFlyer 経由でも独自の Search Ads Attribution API を併用すると精度が上がる。
Apple Search Ads の運用と組み合わせる場合は、Apple Search Ads の高度な運用戦略 で書いたキーワード階層化と組み合わせると効果が読みやすくなります。
媒体ごとに Conversion Value を分けたい場合は、SKAN 4.0 の lockWindow を使って postback window 1 で値を固定し、media partner ごとに別々のコホートとして分析するのが定番です。AppsFlyer のダッシュボードでは SKAN Cohort Builder から数クリックで設定できます。
Adjust 固有の差分と乗り換え時のコツ
AppsFlyer から Adjust に移行する場合、もしくは最初から Adjust で構築する場合、SDK の API そのものは似ていますが、Conversion Value の更新方法と Callback Parameter の概念が少し異なります。Adjust は addCallbackParameter で任意のキー値を後から追加できる柔軟性があり、特定の媒体(例: TikTok)からの流入だけ別の Conversion Schema を当てたい場合に便利です。
// src/lib/attribution-adjust.ts
import { Adjust, AdjustConfig, AdjustEvent } from 'react-native-adjust' ;
export function initAdjust ( appToken : string ) {
const config = new AdjustConfig (appToken, AdjustConfig.EnvironmentProduction);
config. setLogLevel (__DEV__ ? AdjustConfig.LogLevelDebug : AdjustConfig.LogLevelError);
config. setAttributionCallbackListener (( attribution ) => {
console. log ( '[Adjust] attribution' , attribution);
});
Adjust. create (config);
}
export function trackPurchase ( eventToken : string , amount : number , currency : string ) {
const event = new AdjustEvent (eventToken);
event. setRevenue (amount, currency);
// Conversion Value の更新は OS 側に任せ、媒体別の解析はコールバックパラメータで管理する
event. addCallbackParameter ( 'plan_id' , 'pro_monthly' );
Adjust. trackEvent (event);
}
私の経験上、AppsFlyer から Adjust への移行時に最も詰まりやすいのは「ATT 同意フローを再実装する手間」です。両 SDK ともネイティブの ATT API をラップしてはいますが、初期化順序の最適解が微妙に違います。Adjust の場合は Adjust.create を ATT 確定前に呼んでも内部で待機してくれるので、起動時の体感速度が AppsFlyer より一段速く感じられます。
RevenueCat / StoreKit のサブスクリプションイベントを SKAN に紐付ける
サブスクリプション型のアプリでは、トライアル開始から本課金、そして解約までの一連の遷移を SKAN の Conversion Value に反映させると、媒体別の真の ROAS を測れるようになります。RevenueCat を使っている場合、Purchases.addCustomerInfoUpdateListener のフックから直接 Conversion Value を更新するのが最も整理しやすい配線です。
// src/lib/revenuecat-skan-bridge.ts
import Purchases, { CustomerInfo } from 'react-native-purchases' ;
import { reportEvent } from './conversion-value' ;
export function bindRevenueCatToSKAN () {
Purchases. addCustomerInfoUpdateListener (( info : CustomerInfo ) => {
const active = info.entitlements.active[ 'pro' ];
if (active) {
if (active.periodType === 'trial' ) {
void reportEvent ( 'trial_started' , { product: active.productIdentifier });
} else {
// 本課金開始時は累積スコアを最大値(63)にクランプして、媒体側の ROAS 最大値を確実に反映
void reportEvent ( 'trial_started' , { product: active.productIdentifier });
}
}
});
}
注意点として、RevenueCat の customerInfo は購読の更新(リニューアル)でも発火するため、初回のみ送信したい場合は originalPurchaseDate を見て初回かどうかを判定する処理を挟みましょう。リニューアルのたびに trial_started を送ると Conversion Value が単調増加の制約に当たり、結果的に何も更新されなくなります。
サーバ to サーバ ポストバック検証のセットアップ
SKAN ポストバックは Apple のサーバから直接広告ネットワークへ送られますが、自社サーバでもコピーを受け取る S2S(Server-to-Server)構成にしておくと、データが第三者にロックインされず、後から自由に再分析できます。AppsFlyer / Adjust 双方が S2S ポストバックの転送機能を提供しているので、Cloudflare Workers など軽量なエンドポイントを 1 本立てておくのが手堅い構成です。
// Cloudflare Workers — SKAN postback receiver
export default {
async fetch ( request : Request , env : { SKAN_KV : KVNamespace }) : Promise < Response > {
if (request.method !== 'POST' ) return new Response ( 'OK' , { status: 200 });
const payload = await request. json < Record < string , unknown >>();
// 必要最小限のスキーマで保存(個人情報は含めない)
const key = `${ payload [ 'source-app-id' ] ?? 'unknown'}:${ payload [ 'transaction-id' ] }` ;
await env. SKAN_KV . put (key, JSON . stringify (payload), {
expirationTtl: 60 * 60 * 24 * 90 , // 90 日保存
});
return new Response ( 'stored' , { status: 200 });
} ,
} ;
このように KV や D1 に蓄積しておくと、媒体管理画面のレポートが分断されている場合でも、自前で横串の集計が可能になります。期待する動作: SKAN のポストバックが到達するたびに source-app-id:transaction-id をキーとした JSON が KV に書き込まれ、wrangler kv:key list で確認できます。
ダッシュボードの数値をどう読み解くか — 私の毎週の運用ルーティン
最後に、計測データを実際にどう運用に活かしているかをお伝えします。私は毎週月曜の朝に 30 分だけダッシュボードを開く時間を取り、「先週の Conversion Value 分布が直近 4 週の平均からズレていないか」「coarse の high の割合が想定通りか」「特定の媒体だけ fine の最大値が頭打ちになっていないか」の 3 点だけを確認します。
これら 3 点だけに絞っているのは、過剰な細かいレポートに溺れると判断が遅くなるためです。逆に「いつもより low の割合が増えている」と分かれば、クリエイティブの賞味期限が来た合図と捉え、即日広告セットを差し替えます。逆に high が増えていれば、広告予算の上限を 20% 増やして翌週まで様子を見ます。判断基準を少なく保つことが、個人開発で続けるコツでもあります。
全体を振り返って
アトリビューションは導入のハードルが高い割に、一度配線が整えば広告予算の意思決定の速度を一変させてくれます。今日この記事を閉じる前に、まず手元の Rork プロジェクトに react-native-appsflyer を入れて、initAttribution() を 1 回だけ呼ぶところまで進めてみてください。Conversion Value 設計や ATT pre-prompt は、SDK が動くと確認できてから腰を据えて取り組むほうが、結果的に短い時間で完成します。
媒体側の最適化は計測が正しく取れていることを前提にした世界です。先に SDK と Conversion Value 設計を固め、それから広告予算を増やしていく順序を守ることで、個人開発でも代理店並みの精度で運用できる土台が手に入ります。私自身もまだ毎月のように設計を見直していますが、一度この型を持っておくと媒体が変わっても応用が効きます。一緒に磨いていきましょう。