SKAdNetwork 4が利用可能になりました:SKAdNetworkの最新バージョンについては、SKAdNetwork 4インプリメンテーションガイドをご参照ください。
SKAdNetwork 3.0 S2S実装ガイド
AppleのSKAdNetwork 3.0フレームワークを実装し、プライバシーに準拠したiOSアトリビューションを実現します。サーバー間の統合を使用することで、ユーザー識別情報を損なうことなく、アプリインストールキャンペーンのパフォーマンスを測定します。
概要
SKAdNetworkとは?
SKAdNetworkはAppleのプライバシーを保護するアトリビューションフレームワークで、ユーザーレベルの識別子を必要とすることなく、iOSのアプリインストール広告キャンペーンのコンバージョン率を測定することができます。
このフレームワークは、App Storeサーバを通じてアトリビューションを処理し、広告ネットワークに送信する前に、アトリビューション情報をユーザー識別子や一時的なデータから切り離します。
Appleドキュメント:SKAdNetworkフレームワークリファレンス
SKAdNetworkの仕組み
アトリビューションプロセスはすべてAppleのインフラを通して行われるため、キャンペーンパフォーマンス測定を可能にしながら、ユーザーのプライバシーを確保します。
アトリビューションの流れ
- 広告クリック:ユーザーがネットワーク、パブリッシャー、キャンペーンIDを含むSKAdNetworkシグネチャを含む広告をクリック。
- App Storeを開く:デバイスがアトリビューションデータを保存し、インストールのためにApp Storeを開きます。
- アプリの起動:ユーザーが初めてアプリをインストールし、起動する。
- 登録:アプリがSKAdNetworkフレームワークに登録され、インストールが成功したことを知らせる
- コンバージョン値:アプリはオプションで、インストール後のアクティビティを表すコンバージョン値(0~63)を更新します。
- タイマーウィンドウ:デバイスは、最初の起動または最後の変換値更新から24時間以上待つ。
- ポストバック:App Storeは、キャンペーンID、コンバージョン値、暗号署名を含むアトリビューションポストバックを広告ネットワークに送信します。
プライバシーバイデザイン:
- ポストバックには、デバイスやユーザーの識別情報は含まれません。
- 最小24時間の遅延により、時間的相関を防止
- ユーザーがどの広告をクリックしたかをアプリが知ることはない
- どのユーザーがインストールしたかをネットワークが知ることはない
機能と制限
SKAdNetworkが提供する機能
- ラストクリック表示:ユーザーの同意やATTオプトインなしで機能
- キャンペーンの内訳ソース、キャンペーン、パブリッシャー単位
- コンバージョン値インストール後の計測のための64の離散値(0~63
- 不正防止:暗号化署名によるアトリビューションの信頼性検証
現在の制限事項
- ユーザーレベルのデータなし:個々のユーザー・ジャーニーを追跡できない
- ビュースルーなし:クリックベースのアトリビューションのみ
- コンバージョン値の制限:インストールごとに単一の6ビット値(0~63
- 粒度の制限:最大100キャンペーンID、広告グループやクリエイティブの内訳なし
- ロングコホートなし:コンバージョン測定の時間枠に制限あり
- 不正の可能性:コンバージョン値自体が署名されていない、ポストバックが複製される可能性がある
Singularリソース
SingularはSKAdNetworkの実装と最適化のための包括的なリソースを提供しています。
SingularのSKAdNetworkソリューション
SingularのSKAdNetworkソリューションは、クライアントサイドの実装からポストバック処理、キャンペーンの最適化まで、エンドツーエンドのアトリビューション管理を提供します。
ソリューションコンポーネント
プラットフォーム機能
アトリビューションとアナリティクスのワークフロー全体でSKAdNetworkを包括的にサポートします。
| コンポーネント | 機能 |
|---|---|
| クライアントサイドコード | SKAdNetworkフレームワーク登録とコンバージョンバリュー管理用のネイティブiOSコードサンプル |
| ポストバック処理 | すべての広告ネットワークからのポストバックの検証と集計と統一レポート |
| 不正防止 | 暗号化署名検証、トランザクションID重複排除、セキュアパラメータ検証 |
| コンバージョン管理 | インストール後の活動をコンバージョン値にエンコードするためのダイナミックなダッシュボード設定 |
| レポーティング | きめ細かな分析のためのキャンペーンIDの変換とマーケティングパラメータによるエンリッチメント |
| パートナーのポストバック | パートナーの最適化のために、デコードされたコンバージョン値がイベントと収益として送信されます。 |
S2S統合アーキテクチャ
実装コンポーネント
サーバー間のSKAdNetworkインテグレーションは、2つの主要な実装エリアで構成されています。
1.クライアント側の実装(必須)
- アプリ起動時のSKAdNetworkフレームワーク登録
- インストール後のアクティビティに基づくインテリジェントなコンバージョンバリュー管理
- SKAdNetworkアトリビューションを利用したキャンペーン最適化に必須
2.S2S統合アップデート(オプション):
- SingularS2SのイベントとセッションをSKAdNetworkのメタデータで強化します。
- 実装の検証とトラブルシューティングを可能にする
- デバッグ用にコンバージョン値のタイムスタンプを提供します。
クライアント側の実装
SingularのネイティブiOSコードサンプルを使用してSKAdNetworkフレームワーク登録とコンバージョン値管理を実装し、最適なキャンペーン測定を実現します。
実装の責任
コア機能
クライアントサイドコードはSKAdNetworkの測定に重要な2つの機能を処理します。
- SKAdNetwork 登録:ローンチ直後にフレームワークにアプリを登録し、アトリビューションを可能にします。
-
コンバージョンバリュー管理:
- Singularエンドポイントと同期的に通信し、次のコンバージョンバリューを受け取ります。
- セッション、イベント、収益をSingularに報告
- 設定されたインストール後のアクティビティを表すエンコードされたコンバージョン値を受信
- SKAdNetworkのメタデータを収集(最初のコールのタイムスタンプ、最後のコールのタイムスタンプ、現在の値
SKAdNetwork インターフェース
メソッド定義
インターフェースには、フレームワーク登録、セッショントラッキング、イベントトラッキング、収益測定用のメソッドが含まれます。
// SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
// Register for SKAdNetwork attribution.
// Call this method as soon as possible once app is launched
+ (void)registerAppForAdNetworkAttribution;
// Track retention and cohorts by calling after each session.
// Reports session details and updates conversion value if needed.
// Conversion value updates only if new value greater than previous.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;
// Track conversion events by calling after each event.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(NSString*)eventName
withCompletionHandler:(void(^)(int, NSError*))handler;
// Track revenue by calling before each revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValueAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency;
// Gets current conversion value (nil if none set)
+ (NSNumber *)getConversionValue;
@end
登録メソッド
registerAppForAdNetworkAttribution アプリをSKAdNetworkフレームワークに登録し、24時間アトリビューションタイマーを開始します。
タイマーの動作:
- タイマーが切れた0~24時間後にデバイスがインストール通知を送信。
- コンバージョン値をより高い値で更新すると、新しい24時間間隔のタイマーがリセットされます。
- Apple reference:updateConversionValue ドキュメント
変換値メソッド
メソッドは、インストール後のアクティビティと設定された変換モデルに基づいて変換値を計算し、更新します。
サポートされているアクティビティ
- セッション:リテンション測定に重要
- コンバージョンイベント:インストール後のイベント測定に不可欠
- 収益イベント:収益測定に不可欠
インターフェースの実装
完全な実装コード
完全な実装により、登録、コンバージョン管理、Singular API通信を処理します。
// SKANSnippet.m
#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#define SESSION_EVENT_NAME @"__SESSION__"
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v1/conversion_value"
// Keys for UserDefaults storage
#define CONVERSION_VALUE_KEY @"skan_conversion_value"
#define FIRST_SKAN_CALL_TIMESTAMP @"skan_first_call_to_skadnetwork_timestamp"
#define LAST_SKAN_CALL_TIMESTAMP @"skan_last_call_to_skadnetwork_timestamp"
#define TOTAL_REVENUE_BY_CURRENCY_KEY @"skan_total_revenue_by_currency"
#define SECONDS_PER_DAY 86400
static NSLock *lockObject;
@implementation SKANSnippet
+ (void)registerAppForAdNetworkAttribution {
if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
return;
}
if (@available(iOS 11.3, *)) {
[SKAdNetwork registerAppForAdNetworkAttribution];
[SKANSnippet setFirstSkanCallTimestamp];
[SKANSnippet setLastSkanCallTimestamp];
}
}
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler {
[SKANSnippet updateConversionValueAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}
+ (void)updateConversionValueAsync:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (@available(iOS 14, *)) {
if ([SKANSnippet isSkanUpdateWindowOver]) {
return;
}
[SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}
}
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency {
if (@available(iOS 14, *)) {
// Update total revenues
if (amount != 0 && currency) {
NSMutableDictionary* revenues = [[SKANSnippet getTotalRevenue] mutableCopy];
NSNumber* currentRevenue = @(0);
if ([revenues objectForKey:currency]) {
currentRevenue = [revenues objectForKey:currency];
}
currentRevenue = @([currentRevenue floatValue] + amount);
[revenues setObject:currentRevenue forKey:currency];
[SKANSnippet setTotalRevenue:revenues];
}
}
}
+ (NSNumber *)getConversionValue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
return nil;
}
return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}
+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (!lockObject) {
lockObject = [NSLock new];
}
// Making the lock async so it will not freeze the calling thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[lockObject lock];
NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"a" value:@"YOUR_SDK_KEY"],
[NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
[NSURLQueryItem queryItemWithName:@"app_v" value:@"YOUR_APP_VERSION"],
[NSURLQueryItem queryItemWithName:@"n" value:eventName],
[NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
[NSURLQueryItem queryItemWithName:@"idfv" value:@"YOUR_IDFV"],
[NSURLQueryItem queryItemWithName:@"idfa" value:@"YOUR_IDFA"],
[NSURLQueryItem queryItemWithName:@"conversion_value" value:[[SKANSnippet getConversionValue] stringValue]],
[NSURLQueryItem queryItemWithName:@"total_revenue_by_currency" value:[SKANSnippet dictionaryToJsonString:[SKANSnippet getTotalRevenue]]],
[NSURLQueryItem queryItemWithName:@"first_call_to_skadnetwork_timestamp" value:[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]]],
[NSURLQueryItem queryItemWithName:@"last_call_to_skadnetwork_timestamp" value:[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]],
];
[[[NSURLSession sharedSession] dataTaskWithURL:components.URL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[lockObject unlock];
if (handler) {
handler(-1, error);
}
return;
}
NSDictionary* parsedResponse = [SKANSnippet jsonDataToDictionary:data];
if (!parsedResponse) {
[lockObject unlock];
if (handler) {
handler(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
}
return;
}
NSNumber *conversionValue = [parsedResponse objectForKey:@"conversion_value"];
if (!conversionValue) {
[lockObject unlock];
NSString *status = [parsedResponse objectForKey:@"status"];
if (!status || ![status isEqualToString:@"ok"]) {
if (handler) {
NSString *reason = [parsedResponse objectForKey:@"reason"];
if (!reason) {
reason = @"Got error from server";
}
handler(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:reason}]);
}
}
return;
}
NSNumber* currentValue = [SKANSnippet getConversionValue];
if ([conversionValue intValue] <= [currentValue intValue]) {
[lockObject unlock];
return;
}
[SKANSnippet setConversionValue:[conversionValue intValue]];
[SKANSnippet setLastSkanCallTimestamp];
if (![SKANSnippet getFirstSkanCallTimestamp]) {
[SKANSnippet setFirstSkanCallTimestamp];
}
[lockObject unlock];
if (handler) {
handler([conversionValue intValue], error);
}
}] resume];
});
}
+ (BOOL)isSkanUpdateWindowOver {
NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getLastSkanCallTimestamp];
return SECONDS_PER_DAY <= timeDiff;
}
@end
ユーティリティ・メソッド
メタデータ管理
ユーティリティメソッドは、UserDefaults内のSKAdNetworkメタデータの保存を管理します。
クリティカルな実装:これらのユーティリティは、正確な変換値計算に必要なメタデータを管理します。必要な場合以外は変更しないでください。
+ (NSInteger)getFirstSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
return [userDefaults integerForKey:FIRST_SKAN_CALL_TIMESTAMP];
}
+ (NSInteger)getLastSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
return [userDefaults integerForKey:LAST_SKAN_CALL_TIMESTAMP];
}
+ (NSDictionary*)getTotalRevenue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY]) {
return [NSDictionary new];
}
return [userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
}
+ (NSInteger)getCurrentUnixTimestamp {
return [[NSDate date]timeIntervalSince1970];
}
+ (void)setConversionValue:(int)value {
if (@available(iOS 14.0, *)) {
if (value <= [[SKANSnippet getConversionValue] intValue]) {
return;
}
[SKAdNetwork updateConversionValue:value];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:value forKey:CONVERSION_VALUE_KEY];
[userDefaults synchronize];
}
}
+ (void)setFirstSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:FIRST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setLastSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:LAST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setTotalRevenue:(NSDictionary *)values {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:values forKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
[userDefaults synchronize];
}
+ (NSString*)dictionaryToJSONString:(NSDictionary*)dictionary {
if (!dictionary || [dictionary count] == 0){
return @"{}";
}
NSError *error;
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:dictionary
options:0
error:&error];
if (error || !JSONData) {
return @"{}";
}
return [[NSString alloc] initWithData:JSONData
encoding:NSUTF8StringEncoding];
}
+ (NSDictionary*)JSONDataToDictionary:(NSData*)JSONData {
if (!JSONData) {
return nil;
}
NSError * error;
NSDictionary * parsedData = [NSJSONSerialization
JSONObjectWithData:JSONData
options:kNilOptions error:&error];
if (error || !parsedData) {
return nil;
}
return parsedData;
}
@end
S2S統合の更新
オプションとして、実装の検証やトラブルシューティングのために、SKAdNetworkメタデータとのサーバ間統合を強化する。
必須アップデート
コンバージョン値レスポンス
コンバージョンモデルが有効化されると、S2Sエンドポイントはクライアント側更新用の次の値を含むconversion_value 整数フィールドを返す。
レスポンス処理:S2Sエンドポイントのレスポンスからconversion_valueを解析し、クライアント側のコードを介してSKAdNetworkフレームワークに適用する。
オプション機能拡張
メタデータパラメータ
S2SセッションとイベントリクエストにSKAdNetworkメタデータを付加し、統合の検証や実装上の問題のトラブルシューティングを行う。
| パラメータ | 説明 |
|---|---|
skan_conversion_value
|
リクエスト時の最新の変換値 |
skan_first_call_timestamp
|
SKAdNetwork APIを最初に呼び出したUnixタイムスタンプ。 |
skan_last_call_timestamp
|
SKAdNetwork APIへの最新コールのUnixタイムスタンプ |
メタデータ抽出コード
S2S伝送用ユーティリティメソッドを用いてSKAdNetworkメタデータ値を抽出。
NSDictionary *skanMetadata = @{
@"skan_conversion_value":
[[SKANSnippet getConversionValue] stringValue],
@"skan_first_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]],
@"skan_last_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]
};
// Forward skanMetadata to your server for S2S API enrichment
パラメータをサーバーサイドに転送し、Singular S2S APIリクエストに追加する。 パラメータに関する完全なドキュメント:S2S API Reference
統合フロー
クライアント側の変換管理からポストバック処理まで、S2S顧客向けの完全なSKAdNetworkフロー。
フローステージ
エンドツーエンドプロセス
- コンバージョン値のリクエスト:アプリコードがSingularエンドポイントと同期的に通信し、セッション、イベント、収益に基づいた最新のコンバージョン値を取得します。
- フレームワークの更新:アプリは受け取ったコンバージョン値でSKAdNetworkフレームワークを更新します。
- メタデータのエンリッチメント:アプリはS2SイベントとセッションをSKAdNetworkメタデータでエンリッチし、検証する。
- タイマー満了:最終更新から24時間以上経過後、SKAdNetworkは広告ネットワークにポストバックを送信します。
- ポストバック転送:ネットワークがポストバックをSingularに転送(セキュア設定または通常設定
-
ポストバック処理Singularは以下の方法でポストバックを処理します:
- 暗号署名の検証
- 設定されたモデルを使用したコンバージョン値のデコード
- パートナーとの統合に基づくマーケティングパラメーターのエンリッチ化
- ポストバックを介して、デコードされたデータを BI およびパートナーに送信します。
データの分離:SKAdNetworkのデータ(インストールとデコードされたイベント)は、テストと検証中に既存のデータセットとの混合を防ぐために、別々のレポート、API、ETLテーブル、ポストバックを介してアクセス可能。
アプリライフサイクルの実装
完全なアトリビューションカバレッジのために、適切なアプリライフサイクルポイントにSKAdNetworkのメソッドを統合します。
実装例
メソッドの配置
// On app launch (in applicationDidFinishLaunching)
[SKANSnippet registerAppForAdNetworkAttribution];
// After each session handled
[SKANSnippet updateConversionValueAsync:^(int value, NSError *error) {
if (error) {
NSLog(@"Conversion value update failed: %@", error);
} else {
NSLog(@"Conversion value updated to: %d", value);
}
}];
// After handling non-revenue events
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Event conversion value update failed: %@", error);
} else {
NSLog(@"Event conversion value updated to: %d", value);
}
}];
// After handling revenue events
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Revenue conversion value update failed: %@", error);
} else {
NSLog(@"Revenue conversion value updated to: %d", value);
}
}];
テスト実施
本番デプロイ前にSingularのテストエンドポイントを使用してSKAdNetworkの実装を検証します。
テスト環境
テストエンドポイントの構成
アプリのフローをシミュレートするために、本番環境のコンバージョン管理エンドポイントをテストエンドポイントに置き換えます。
テストエンドポイントのURL
https://skadnetwork-testing.singular.net/api/v1/conversion_value
- テストエンドポイントにAPIキーは不要
- 変換値0~2を順次返す
- 3回目の呼び出し後に空文字列を返す
テスト手順
検証ステップ
-
エンドポイントを更新する:
SINGULAR_API_URL定数をテストエンドポイントの URL に置き換える。 - 変換値を初期化する:デフォルトの変換値が0に初期化されていることを確認する。
- イベントを生成する:アプリ内で3つの異なるイベントをトリガーする
-
更新の確認:各イベント後に呼び出される
updateConversionValueAsyncを確認 - 値のログ:正しい進行(0→1→2)を確認するために、返された変換値をログに残す。
- 完了の確認:3回目の呼び出しの後、空の応答と最終的な値の保持を確認する。
期待される動作
- 最初のイベント:変換値0を受け取る
- 2番目のイベント:変換値1を受け取る
- 3番目のイベント変換値2を受け取る
- 4回目以上のイベント空のレスポンス、値2が持続
コード変更履歴
時間の経過とともに適用されたコードサンプルの更新と重要な修正を追跡します。
バージョン履歴
| 日付 | 変更点 |
|---|---|
| 2020年10月1日 |
|
| 2020年9月23日 |
|
| 2020年9月15日 |
|