レガシー — SKAdNetwork 3。 このガイドはレガシーの SKAdNetwork 3.0 実装を対象としており、既存の連携のためだけに 残されています。新規およびすべての現行実装には、 SKAdNetwork 4 実装ガイド をご利用ください。
SKAdNetwork 3.0 S2S 実装ガイド (レガシー)
Apple の SKAdNetwork 3.0 フレームワークを実装し、server-to-server 連携を使用して、 ユーザー識別子を損なうことなくアプリインストールキャンペーンのパフォーマンスを測定する、 プライバシーに準拠した iOS アトリビューションを実現します。
概要
SKAdNetwork とは
SKAdNetwork は Apple のプライバシー保護型アトリビューションフレームワークであり、 ユーザーレベルの識別子を必要とせずに、iOS 上でアプリインストール広告キャンペーンの コンバージョン率を測定できるようにします。
このフレームワークは App Store サーバーを通じてアトリビューションを処理し、 アトリビューション情報を広告ネットワークに送信する前に、ユーザー識別子や時間データから 切り離します。
Apple のドキュメント: SKAdNetwork Framework Reference
SKAdNetwork の仕組み
アトリビューションの処理は完全に Apple のインフラを通じて行われ、キャンペーンの パフォーマンス測定を可能にしながらユーザーのプライバシーを確保します。
アトリビューションフロー:
- 広告クリック: ユーザーが、ネットワーク、パブリッシャー、キャンペーン ID を含む SKAdNetwork 署名付きの広告をクリックします
- App Store の起動: デバイスがアトリビューションデータを保存し、 インストールのために App Store を開きます
- アプリの起動: ユーザーがアプリをインストールし、初めて 起動します
- 登録: アプリが SKAdNetwork フレームワークに登録し、 インストールの成功を通知します
- conversion value: アプリは任意で conversion value (0~63) を更新し、インストール後のアクティビティを表現します
- タイマーウィンドウ: デバイスは初回起動または 最後の conversion value 更新から 24 時間以上待機します
- ポストバック: App Store が、キャンペーン ID、conversion value、 暗号署名を含むアトリビューションポストバックを広告ネットワークに送信します
プライバシー・バイ・デザイン:
- ポストバックにはデバイスやユーザーの識別子が含まれません
- 最短 24 時間の遅延により、時間的な相関を防ぎます
- アプリはユーザーがどの広告をクリックしたかを知ることはありません
- ネットワークはどの特定のユーザーがインストールしたかを知ることはありません
機能と制限
SKAdNetwork が提供するもの:
- ラストクリックアトリビューション: ユーザーの同意や ATT オプトインなしで機能します
- キャンペーンの内訳: ソース、キャンペーン、パブリッシャー 単位の粒度
- conversion value: インストール後の測定のための 64 個の離散値 (0~63)
- 不正防止: 暗号署名がアトリビューションの 真正性を検証します
現在の制限:
- ユーザーレベルのデータなし: 個々のユーザーの 行動を追跡できません
- ビュースルーなし: クリックベースのアトリビューションのみ
- conversion value の制限: インストールごとに単一の 6 ビット値 (0~63)
- 粒度の制限: 最大 100 個のキャンペーン ID、 広告グループやクリエイティブ単位の内訳はなし
- 長期コホートなし: コンバージョン測定のための 期間が限られています
- 不正リスク: conversion value 自体は署名されておらず、 ポストバックが重複する可能性があります
Singular のリソース
Singular は SKAdNetwork の実装と最適化のための包括的なリソースを 提供しています。
- SKAN: Practical Standard for SKAdNetwork Implementation
- SKAdNetwork 101: What It Means for You
- SKAdNetwork Code: GitHub Repository for Networks, Publishers, Advertisers
- How to Test SKAdNetwork: Step-by-Step Instructions
- Singular Announces First-to-Market IDFA Replacement Support
- Advanced Measurement Using SKAdNetwork: Unlocking ROAS
- Secure SKAdNetwork 2.0: Seamless Trust Establishment Setup
Singular SKAdNetwork ソリューション
Singular の SKAdNetwork ソリューションは、クライアントサイドの実装から ポストバック処理、キャンペーン最適化まで、エンドツーエンドのアトリビューション管理を提供します。
ソリューションの構成要素
プラットフォーム機能
アトリビューションと分析のワークフロー全体にわたる包括的な SKAdNetwork サポート。
| 構成要素 | 機能 |
|---|---|
| クライアントサイドコード | SKAdNetwork フレームワーク登録と conversion value 管理のための ネイティブ iOS コードサンプル |
| ポストバック処理 | すべての広告ネットワークからのポストバックの検証と集計、および 統合されたレポート |
| 不正対策 | 暗号署名の検証、トランザクション ID の重複排除、 および安全なパラメータの検証 |
| コンバージョン管理 | インストール後のアクティビティを conversion value にエンコードするための 動的なダッシュボード設定 |
| レポート | 粒度の高い分析のための、キャンペーン ID の変換とマーケティングパラメータによる エンリッチメント |
| パートナーポストバック | パートナー最適化のために、デコードされた conversion value を イベントおよび収益として送信 |
S2S 連携アーキテクチャ
実装の構成要素
server-to-server の SKAdNetwork 連携は、2 つの主要な実装領域で 構成されます。
1. クライアントサイド実装 (必須):
- アプリ起動時の SKAdNetwork フレームワーク登録
- インストール後のアクティビティに基づくインテリジェントな conversion value 管理
- SKAdNetwork アトリビューションを使用したキャンペーン最適化に不可欠
2. S2S 連携の更新 (任意):
- Singular の S2S イベントとセッションを SKAdNetwork メタデータでエンリッチします
- 実装の検証とトラブルシューティングを可能にします
- デバッグ用に conversion value のタイムスタンプを提供します
クライアントサイド実装
最適なキャンペーン測定のために、Singular のネイティブ iOS コードサンプルを使用して、 SKAdNetwork フレームワーク登録と conversion value 管理を実装します。
実装の責任範囲
中核となる機能
クライアントサイドコードは、SKAdNetwork 測定のための 2 つの重要な機能を処理します。
- SKAdNetwork 登録: アトリビューションを有効にするため、起動直後に アプリをフレームワークに登録します
-
conversion value 管理:
- 次の conversion value を受け取るために、Singular エンドポイントと 同期的に通信します
- セッション、イベント、収益を Singular に報告します
- 設定されたインストール後アクティビティを表す、エンコードされた conversion value を受け取ります
- 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 時間後にインストール通知を送信します
- より高い値で conversion value を更新すると、タイマーがリセットされ 新たな 24 時間の間隔が始まります
- Apple のリファレンス: updateConversionValue Documentation
conversion value メソッド
これらのメソッドは、インストール後のアクティビティと設定されたコンバージョンモデルに基づいて、 conversion value を計算および更新します。
対応するアクティビティ:
- セッション: リテンション測定に不可欠
- コンバージョンイベント: インストール後のイベント 測定に不可欠
- 収益イベント: 収益測定に不可欠
インターフェースの実装
完全な実装コード
完全な実装では、登録、コンバージョン管理、および 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 メタデータの保存を管理し、 conversion value の計算に不可欠です。
重要な実装: これらのユーティリティは、正確な conversion value 計算に 必要なメタデータを維持します。必要でない限り 変更しないでください。
+ (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 連携の更新
実装の検証とトラブルシューティングのために、任意で server-to-server 連携を SKAdNetwork メタデータで強化します。
必須の更新
conversion value のレスポンス
コンバージョンモデルが有効化されると、S2S エンドポイントは、クライアントサイドの
更新用の次の値を含む
conversion_value
整数フィールドを返します。
レスポンスの処理:
S2S エンドポイントのレスポンスから
conversion_value
を解析し、クライアントサイドコードを介して SKAdNetwork フレームワークに適用します。
任意の拡張
メタデータパラメータ
連携を検証し、実装の問題をトラブルシューティングするために、S2S セッションおよびイベントの リクエストを SKAdNetwork メタデータでエンリッチします。
| パラメータ | 説明 |
|---|---|
skan_conversion_value
|
リクエスト時点での最新の 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
連携フロー
クライアントサイドの conversion value 管理からポストバック処理まで、 S2S を利用する顧客向けの完全な SKAdNetwork フロー。
フローの各段階
エンドツーエンドのプロセス
- conversion value のリクエスト: アプリコードが Singular エンドポイントと同期的に通信し、セッション、イベント、収益に基づく 最新の conversion value を取得します
- フレームワークの更新: アプリが受け取った conversion value で SKAdNetwork フレームワークを更新します
- メタデータのエンリッチメント: アプリが検証のために S2S イベントおよび セッションを SKAdNetwork メタデータでエンリッチします
- タイマーの満了: 最後の更新から 24 時間以上経過後、 SKAdNetwork が広告ネットワークにポストバックを送信します
- ポストバックの転送: ネットワークがポストバックを Singular に転送します (セキュアセットアップまたは通常)
-
ポストバックの処理:
Singular は次の方法でポストバックを
処理します:
- 暗号署名を検証する
- 設定されたモデルを使用して conversion value をデコードする
- パートナー連携に基づいてマーケティングパラメータでエンリッチする
- デコードされたデータをポストバック経由で 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 キーは不要です
- conversion value を 0~2 の順に返します
- 3 回目の呼び出し以降は空文字列を返します
テスト手順
検証ステップ
-
エンドポイントの更新:
SINGULAR_API_URL定数をテストエンドポイント URL に置き換えます - conversion value の初期化: デフォルトの conversion value が 0 に初期化されていることを確認します
- イベントの生成: アプリ内で 3 つの異なるイベントを トリガーします
-
更新の確認:
各イベント後に
updateConversionValueAsyncが呼び出されることを確認します - 値のログ記録: 返された conversion value をログに記録し、 正しい進行 (0 → 1 → 2) を確認します
- 完了の確認: 3 回目の呼び出し後、空の レスポンスと最終値の保持を確認します
期待される挙動:
- 1 番目のイベント: conversion value 0 を受け取る
- 2 番目のイベント: conversion value 1 を受け取る
- 3 番目のイベント: conversion value 2 を受け取る
- 4 番目以降のイベント: 空のレスポンス、値 2 が維持される
コードの変更履歴
これまでに適用されたコードサンプルの更新と重要な修正を記録します。
バージョン履歴
| 日付 | 変更内容 |
|---|---|
| 2020年10月1日 |
|
| 2020年9月23日 |
|
| 2020年9月15日 |
|