サーバー間 - SKAdNetwork 4 導入ガイド

ドキュメント

SKAdNetwork 4.0実装ガイド

AppleのSKAdNetwork 4.0フレームワークを実装することで、サーバー間の統合を利用したプライバシー重視のiOSアトリビューションを実現し、ユーザーのプライバシーを保護しながら、強化されたポストバックウィンドウと粗いコンバージョン値でアプリインストールキャンペーンの測定を可能にします。


概要

SKAdNetwork 4.0とは?

SKAdNetwork(SKAN)はAppleのプライバシーを重視したアトリビューションフレームワークで、iOSアプリインストール広告キャンペーンの計測を可能にします。

このフレームワークは、Appleが規定する方法によってユーザーのプライバシーを維持しながら、アトリビューションの重要な側面をすべて処理するため、iOS 14.5以降の環境で活動するモバイルマーケターにとって不可欠なものとなっています。


主な機能

SKAdNetwork 4.0では、キャンペーン最適化のための測定機能と柔軟性が強化されています。

  • 堅牢な検証:すべてのネットワークからのポストバックを集約し、不正防止機能を内蔵
  • ダイナミックなコンバージョン管理:コンバージョン値エンコーディングのためのダッシュボード設定
  • レポートの強化:充実したマーケティングパラメータと詳細なデータインサイト
  • 安全なパートナーのポストバック:デコードされたコンバージョン値と収益追跡
  • 包括的な検証:実装検証のための充実したイベントおよびセッション追跡
  • 複数のポストバックウィンドウ:0~2日、3~7日、8~35日にわたるタイムスタンプの自動管理
  • 収益トラッキング:通貨指定による広告収益化と通常収益のサポート

前提条件

SKAN 4.0を導入する前に、SKAdNetworkのコンセプトと以前のバージョンについてよく理解してください。


SingularのSKAdNetworkソリューション

SingularのSKAdNetworkソリューションは、クライアントサイドの実装からポストバック処理、キャンペーンの最適化まで、エンドツーエンドのアトリビューション管理を提供します。

ソリューションコンポーネント

プラットフォーム機能

アトリビューションとアナリティクスのワークフロー全体でSKAdNetworkを包括的にサポートします。

コンポーネント 機能
クライアントサイドコード SKAdNetworkフレームワーク登録とコンバージョンバリュー管理用のネイティブiOSコードサンプル。Conversion Value APIエンドポイントを使用したサーバーサイドのアプローチも可能です。
ポストバック処理 すべての広告ネットワークからのポストバックを検証し、集計します。
不正防止 暗号化署名の検証、トランザクションIDの重複排除、未署名データのセキュアなパラメータ検証
コンバージョン管理 インストール後の活動をコンバージョン値にエンコードするためのダイナミックなダッシュボード設定
レポーティング きめ細かな分析のためのキャンペーンIDの変換とマーケティングパラメータによるエンリッチメント
パートナーのポストバック パートナーの最適化のために、デコードされたコンバージョン値がイベントおよび収益として送信されます。

実装アーキテクチャ

2つの実装

クライアントサイドSKAdNetworkの実装は、2つの主要コンポーネントで構成されています。

1.クライアント側の実装(必須)

  • アプリ起動時のSKAdNetworkフレームワーク登録
  • インストール後のアクティビティに基づくインテリジェントなコンバージョンバリュー管理
  • SKAdNetworkのアトリビューションを利用したキャンペーン最適化に必須
  • 関連するインストール後のアクティビティのトラッキングが可能

2.サーバーサイドインテグレーションアップデート(推奨)

  • クライアント側の実装の検証およびトラブルシューティング
  • セッションと イベントエンドポイント経由で送信されるイベントとセッションの強化
  • SKAdNetworkメタデータの検証を可能にします。
  • アプリ側の適切な実装の確認

クライアント側の実装

SKAN4.0の機能を使って最適なキャンペーン測定を行うために、SingularのネイティブiOSコードサンプルを使ってSKAdNetworkフレームワークの登録とコンバージョン値の管理を実装します。

実装の責任

コア機能

コードサンプルはSKAdNetwork登録とインテリジェントなコンバージョン値管理をサポートします。

  1. SKAdNetwork登録:ローンチ直後にフレームワークにアプリを登録し、アトリビューションを可能にします。
  2. コンバージョンバリュー管理
    • Singularエンドポイントと同期的に通信し、次のコンバージョンバリューを受け取ります。
    • セッション、イベント、収益をSingularに報告
    • 設定されたインストール後のアクティビティを表すエンコードされたコンバージョン値を受信
    • SKAdNetworkのメタデータを測定期間ごとに収集し、検証と計算を行います。

メタデータの収集

コードは、検証およびコンバージョン値計算のために必要不可欠なSKAdNetworkメタデータを収集します。

  • 基礎となるSKAdNetworkフレームワークへの最初の呼び出しタイムスタンプ
  • SKAdNetworkフレームワークへの最終呼び出しタイムスタンプ
  • 最終更新ポストバック値(粗と細の両方)
  • デバイスによって生成された総収入と総広告収益化収入

統合フロー

エンドツーエンドプロセス

クライアント側のコンバージョン管理からポストバック処理まで、S2S顧客向けの完全なSKAdNetworkフロー。

SKAdNetwork 4.0 S2S Integration Flow

  1. コンバージョンバリューのリクエストアプリコードはSingularエンドポイントと同期的に通信し、セッション、イベント、収益に基づいた最新のコンバージョン値を取得します。
  2. フレームワークの更新:アプリは受け取ったコンバージョン値でSKAdNetworkフレームワークを更新します。
  3. メタデータのエンリッチメント:アプリはS2SイベントとセッションをSKAdNetworkメタデータでエンリッチし、検証する。
  4. タイマー満了:タイマー満了後、SKAdNetworkは広告ネットワークにポストバックを送信。
  5. ポストバック転送:ネットワークがポストバックをSingularに転送(セキュアセットアップまたは通常
  6. ポストバック処理:Singularは以下の方法でポストバックを処理します:
    • 暗号署名の検証
    • 設定されたモデルを使った変換値のデコード
    • パートナーとの統合によるネットワーク情報のエンリッチ化
    • ポストバックを介して、デコードされたデータを BI およびパートナーに送信します。

データの分離:SKAdNetworkのデータ(インストールとデコードされたイベント)は、テストと検証中に既存のデータセットとの混合を防ぐために、別々のレポート、API、ETLテーブル、ポストバックを介してアクセス可能。


SKAdNetworkインターフェース

アトリビューショントラッキング、コンバージョン値の更新、収益管理のためのメソッドを提供するSKAdNetwork統合のための完全なインターフェース定義。

メソッド定義

アトリビューション登録

初回アプリ起動時にSKANアトリビューショントラッキングを初期化し、初期コンバージョン値を0に設定し、ベースラインタイムスタンプを確立します。

Objective-C
+ (void)registerAppForAdNetworkAttribution;

コンバージョン値管理

メソッドは、アプリによってキャプチャされたインストール後のアクティビティと動的に設定された選択されたコンバージョンモデルに基づいてコンバージョン値を更新します。

サポートされるアクティビティ

  • セッション:リテンション測定に不可欠
  • コンバージョンイベント:インストール後のイベント測定に不可欠
  • 収益イベント:収益測定に不可欠

セッショントラッキング

リテンションとコホート分析のためのセッションベースのトラッキングを管理し、更新後のアクションのための完了ハンドラをオプションで提供します。

Objective-C
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

イベントトラッキング

Singularにデータを送信する前にコンバージョンイベントのトラッキングを処理し、イベントコンテキストに基づいてコンバージョン値を更新します。

Objective-C
+ (void)updateConversionValuesAsync:(NSString *)eventName 
                withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

収益管理

収益イベントをトラッキングし、広告収益と通常の収益を別々に集計します。コンバージョン値を更新する前に呼び出す必要があります。

Objective-C
+ (void)updateRevenue:(double)amount 
          andCurrency:(NSString *)currency 
     isAdMonetization:(BOOL)admon;

データ検索

コンバージョン値、タイムスタンプ、収益トラッキングを含む包括的なSKANデータディクショナリを返します。

辞書には以下が含まれます:

  • 現在および以前の細かいコンバージョン値
  • 異なるポストバックウィンドウにわたる粗い値
  • ウィンドウロックのタイムスタンプ
  • 通貨別の収益トラッキング
  • 広告収益と通常の収益を別々にトラッキング
Objective-C
+ (NSDictionary *)getSkanDetails;

実装上の注意

  • メインスレッドのブロックを防ぐため、メソッドは非同期パターンを使用します。
  • 収益トラッキングはコンバージョン値の更新に先行する必要がある
  • きめ細かい(0-63)コンバージョン値と粗い(低/中/高)コンバージョン値の両方をサポートします。
  • 異なるポストバックウィンドウに対して別々のトラッキングを維持します。
  • 完了ハンドラによる包括的なエラー処理の実装

完全なインターフェースコード

SKANSnippet.h

Objective-C
//SKANSnippet.h

#import <Foundation/Foundation.h>

@interface SKANSnippet : NSObject

// Register for SKAdNetwork attribution.
// Call this method as soon as possible on first app launch.
// Sets conversion value to 0 and updates timestamp for additional processing.
+ (void)registerAppForAdNetworkAttribution;

// Track retention and cohorts by calling for each app open.
// Reports session details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

// Track conversion events by calling after each event and before sending to Singular.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(NSString *)eventName 
                withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

// Track revenue by calling before every revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValuesAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount 
          andCurrency:(NSString *)currency 
     isAdMonetization:(BOOL)admon;

// Gets current fine, coarse, window locked values saved in dictionary.
// Contains all relevant SKAN values including:
// - skan_current_conversion_value
// - prev_fine_value  
// - skan_first_call_to_skadnetwork_timestamp
// - skan_last_call_to_skadnetwork_timestamp
// - skan_total_revenue_by_currency
// - skan_total_admon_revenue_by_currency
// - p0_coarse, p1_coarse, p2_coarse
// - p0_window_lock, p1_window_lock, p2_window_lock
// - Previous coarse values and revenue per window
+ (NSDictionary *)getSkanDetails;

@end

SKAdNetworkの実装

AppleのSKAdNetwork 4.0インターフェースの完全な実装コードで、複数のポストバックウィンドウにわたってアトリビューショントラッキング、コンバージョン値、収益レポートを管理します。

実装の概要

定数とコンフィギュレーション

実装では、ユーザーのアクティビティとコンバージョンをトラッキングするための3つの異なるポストバックウィンドウを定義します。

Objective-C
static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2;  // 48 hours
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;  // 7 days
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35; // 35 days

主な機能

  • アトリビューション登録:アプリのアトリビューションの初期設定と初回のコンバージョン値のトラッキングを処理します。
  • コンバージョン管理:複数のポストバックウィンドウでコンバージョン値の更新とトラッキングを行います。
  • 収益トラッキング広告収益化と定期的な収益イベントの個別のトラッキングを維持します。
  • データの永続化:NSUserDefaultsを使用して、アプリのセッションをまたいでSKAN関連データを保存します。
  • スレッドの安全性:ネットワーク呼び出し中のスレッドセーフな操作のためのNSLockの実装

データ保存構造

  • 細かい変換値(0~63)
  • 粗い値(Low/Medium/Highを0-2にマッピング)
  • 各ポストバック・ウィンドウの通貨別収益トラッキング
  • ポストバック・ウィンドウとロック状態のタイムスタンプ管理
  • 細かい変換値と粗い変換値の両方に対する以前の値のトラッキング

プライバシーへの配慮

  • iOS 15.4+およびiOS 16.1+固有の機能を実装
  • Appleのプライバシーガイドラインに従ってポストバックコンバージョン値の更新を処理します。
  • 正確なアトリビューションを保証するために、異なる収益タイプに対する個別のトラッキングを維持します。

テクニカルノート

  • ネットワークコールと値の更新に非同期オペレーションを使用
  • コンバージョン値のエラー処理と検証を実装しています。
  • 従来のコンバージョン値と粗いコンバージョン値の両方のトラッキングをサポート
  • 異なる期間と要件を持つ複数のポストバックウィンドウを管理します。

完全な実装コード

SKANSnippet.m

重要本番使用前に、プレースホルダの値(YOUR API KEY、YOUR APP VERSIONなど)をアプリケーションの実際の値に置き換えてください。

Objective-C
//  SKANSnippet.m

#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#import <UIKit/UIKit.h>

#define SESSION_EVENT_NAME @"__SESSION__"
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v2/conversion_value"

// SKAN Keys for NSUserDefaults persistency and requests
#define CONVERSION_VALUE_KEY @"skan_current_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 @"skan_total_revenue_by_currency"
#define TOTAL_ADMON_REVENUE_BY_CURRENCY @"skan_total_admon_revenue_by_currency"
#define SKAN_UPDATED_CONVERSION_VALUE @"conversion_value"
#define SKAN_UPDATED_COARSE_VALUE @"skan_updated_coarse_value"
#define SKAN_UPDATED_LOCK_WINDOW_VALUE @"skan_updated_lock_window_value"

#define P0_COARSE @"p0_coarse"
#define P1_COARSE @"p1_coarse"
#define P2_COARSE @"p2_coarse"
#define P0_WINDOW_LOCK_TS @"p0_window_lock"
#define P1_WINDOW_LOCK_TS @"p1_window_lock"
#define P2_WINDOW_LOCK_TS @"p2_window_lock"

#define P0_PREV_FINE_VALUE @"prev_fine_value"
#define P0_PREV_COARSE_VALUE @"p0_prev_coarse_value"
#define P1_PREV_COARSE_VALUE @"p1_prev_coarse_value"
#define P2_PREV_COARSE_VALUE @"p2_prev_coarse_value"

#define TOTAL_REVENUE_P0 @"p0_total_iap_revenue"
#define TOTAL_REVENUE_P1 @"p1_total_iap_revenue"
#define TOTAL_REVENUE_P2 @"p2_total_iap_revenue"
#define TOTAL_ADMON_REVENUE_P0 @"p0_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P1 @"p1_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P2 @"p2_total_admon_revenue"

@implementation SKANSnippet

static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2; //48 hours in sec
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35;

static NSLock *lockObject;

+ (void)registerAppForAdNetworkAttribution {
    if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
        return;
    }
    
    if (@available(iOS 15.4, *)) {
        [SKAdNetwork updatePostbackConversionValue:0 completionHandler:nil];
        [SKANSnippet setFirstSkanCallTimestamp];
        [SKANSnippet setLastSkanCallTimestamp];
        [SKANSnippet valuesHasBeenUpdated:@(0) coarseValue:nil lockWindow:NO];
    } 
}

+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
    [SKANSnippet updateConversionValuesAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}

+ (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
        if ([SKANSnippet isSkanWindowOver]) {
            return;
        }
        
        [SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}

+ (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon {
    // Update total revenues
    if (amount == 0 || !currency ) {
        return;
    }
    
    [SKANSnippet addToTotalRevenue:@(amount) withCurrency:currency isAdmon:admon];
}

+ (NSDictionary *)getSkanDetails {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *res = [NSMutableDictionary dictionary];
    //current fine
    [res setValue:[[userDefaults valueForKey:CONVERSION_VALUE_KEY] stringValue] forKey:CONVERSION_VALUE_KEY];
    //prev fine
    [res setValue:[[userDefaults valueForKey:P0_PREV_FINE_VALUE] stringValue] forKey:P0_PREV_FINE_VALUE];
    //current coarse
    [res setValue:[[userDefaults valueForKey:P0_COARSE] stringValue] forKey:P0_COARSE];
    [res setValue:[[userDefaults valueForKey:P1_COARSE] stringValue] forKey:P1_COARSE];
    [res setValue:[[userDefaults valueForKey:P2_COARSE] stringValue] forKey:P2_COARSE];
    //prev coarse
    [res setValue:[[userDefaults valueForKey:P0_PREV_COARSE_VALUE] stringValue] forKey:P0_PREV_COARSE_VALUE];
    [res setValue:[[userDefaults valueForKey:P1_PREV_COARSE_VALUE] stringValue] forKey:P1_PREV_COARSE_VALUE];
    [res setValue:[[userDefaults valueForKey:P2_PREV_COARSE_VALUE] stringValue] forKey:P2_PREV_COARSE_VALUE];
    //lock windows ts
    [res setValue:[[userDefaults valueForKey:P0_WINDOW_LOCK_TS] stringValue] forKey:P0_WINDOW_LOCK_TS];
    [res setValue:[[userDefaults valueForKey:P1_WINDOW_LOCK_TS] stringValue] forKey:P1_WINDOW_LOCK_TS];
    [res setValue:[[userDefaults valueForKey:P2_WINDOW_LOCK_TS] stringValue] forKey:P2_WINDOW_LOCK_TS];
    //total revenues
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_BY_CURRENCY] forKey:TOTAL_REVENUE_BY_CURRENCY];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_BY_CURRENCY] forKey:TOTAL_ADMON_REVENUE_BY_CURRENCY];
    //revenue per window
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P0] forKey:TOTAL_REVENUE_P0];
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P1] forKey:TOTAL_REVENUE_P1];
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P2] forKey:TOTAL_REVENUE_P2];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P0] forKey:TOTAL_ADMON_REVENUE_P0];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P1] forKey:TOTAL_ADMON_REVENUE_P1];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P2] forKey:TOTAL_ADMON_REVENUE_P2];
    //skan TS
    [res setValue:[[userDefaults valueForKey:LAST_SKAN_CALL_TIMESTAMP] stringValue] forKey:LAST_SKAN_CALL_TIMESTAMP];
    [res setValue:[[userDefaults valueForKey:FIRST_SKAN_CALL_TIMESTAMP] stringValue] forKey:FIRST_SKAN_CALL_TIMESTAMP];
    
    return res;
}


#pragma mark - internal
+ (BOOL)validateValues:(NSNumber *)conversionValue coarse:(NSNumber *)coarseValue{
    if ([conversionValue intValue] < 0 || 63 < [conversionValue intValue]) {
        return NO;
    }
    
    if (coarseValue) {
        if ([coarseValue intValue] > 2 || [coarseValue intValue] < 0) {
            return NO;
        }
    }
    
    return YES;
}

+ (NSURLComponents *)prepareQueryParams:(NSString *)bundleIdentifier eventName:(NSString *)eventName {
    NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
    
    NSString *API_KEY = @"YOUR API KEY";
    NSString *APP_VERSION = @"YOUR APP VERSION";
    NSString *IDFV = @"IDFV";
    NSString *IDFA = @"IDFA";
    
    NSMutableArray *queryItems = [@[
        [NSURLQueryItem queryItemWithName:@"a" value:API_KEY],
        [NSURLQueryItem queryItemWithName:@"v" value:[[UIDevice currentDevice] systemVersion]],
        [NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
        [NSURLQueryItem queryItemWithName:@"app_v" value:APP_VERSION],
        [NSURLQueryItem queryItemWithName:@"n" value:eventName],
        [NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
        [NSURLQueryItem queryItemWithName:@"idfv" value:IDFV],
        [NSURLQueryItem queryItemWithName:@"idfa" value:IDFA]
    ] mutableCopy];
    
    NSDictionary *skanValues = [SKANSnippet getSkanDetails];
    [skanValues enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[NSDictionary class]]) {
            [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:[SKANSnippet dictionaryToJsonString:obj]]];
        } else {
            [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:obj]];
        }
    }];
    
    components.queryItems = queryItems;
    
    return components;
}

+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
    if (!lockObject) {
        lockObject = [NSLock new];
    }
    
    @try {
        // 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];
            NSString *bundleIdentifier = @"YOUR BUNDLE IDENTIFIER";
            NSURLComponents *components = [SKANSnippet prepareQueryParams:bundleIdentifier eventName:eventName];
            
            [[[NSURLSession sharedSession] dataTaskWithURL:components.URL
                                         completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                if (error) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil, nil, NO, error);
                    }
                    
                    return;
                }
                
                NSDictionary *parsedResponse = [SKANSnippet jsonDataToDictionary:data];
                
                if (!parsedResponse) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                 code:0
                                                             userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
                    }
                    
                    return;
                }
                
                NSNumber *conversionValue = [parsedResponse objectForKey:SKAN_UPDATED_CONVERSION_VALUE];
                NSNumber *coarseValue = [parsedResponse objectForKey:SKAN_UPDATED_COARSE_VALUE];
                BOOL lockWindow = [[parsedResponse objectForKey:SKAN_UPDATED_LOCK_WINDOW_VALUE] boolValue];
                
                
                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(nil, nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                      code:0
                                                                  userInfo:@{NSLocalizedDescriptionKey:reason}]);
                        }
                    }
                    
                    return;
                }
                
                
                if(![SKANSnippet validateValues:conversionValue coarse:coarseValue]) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                 code:0
                                                             userInfo:@{NSLocalizedDescriptionKey:@"Illegal values received"}]);
                    }
                    
                    return;
                }
                
                if (![SKANSnippet getFirstSkanCallTimestamp]) {
                    [SKANSnippet setFirstSkanCallTimestamp];
                }
                
                [SKANSnippet setConversionValues:conversionValue coarseValue:coarseValue lockWindow:lockWindow handler:handler];
                
                [lockObject unlock];
            }] resume];
        });
    } @catch (id exception) {
        NSLog(@"%@", exception);
    }
}

+ (void)setFirstSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:FIRST_SKAN_CALL_TIMESTAMP];
    
}

+ (void)setLastSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:LAST_SKAN_CALL_TIMESTAMP];
    
}

+ (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];
}

+ (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];
}

+ (NSInteger)getCurrentUnixTimestamp {
    return [[NSDate date] timeIntervalSince1970];
}

+ (void)setConversionValues:(NSNumber *)conversionValue coarseValue:(NSNumber *)coarse lockWindow:(BOOL)lockWindow handler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
    @try {
        __block void(^skanResultHandler)(NSError * _Nullable error) = ^(NSError * _Nullable error) {
            if (handler) {
                if (error) {
                    handler(nil, nil, NO, error);
                } else {
                    handler(conversionValue, coarse, lockWindow, nil);
                }
            }
            
            [SKANSnippet valuesHasBeenUpdated:conversionValue coarseValue:coarse lockWindow:lockWindow];
        };
        
        if (@available(iOS 16.1, *)) {
            [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] coarseValue:[SKANSnippet resolveCoarseValueFrom:coarse] lockWindow:lockWindow completionHandler:^(NSError * _Nullable error) {
                skanResultHandler(error);
            }];
        } else {
            if (@available(iOS 15.4, *)) {
                [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] completionHandler:^(NSError * _Nullable error) {
                    skanResultHandler(error);
                }];
            }
        }
    } @catch (id exception) {
        NSLog(@"%@", exception);
    }
}

+ (NSNumber *)getConversionValue {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
        return @(0);
    }
    
    return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}

+ (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;
}

+ (NSInteger)getCurrentSkanWindow {
    NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
    if (timeDiff < firstSkan4WindowInSec) { return 0; }
    if (timeDiff < secondSkan4WindowInSec) { return 1; }
    if (timeDiff < thirdSkan4WindowInSec) { return 2; }
    
    return -1;
}

// persist updated conversion values based on the active skan window.
+ (void)valuesHasBeenUpdated:(NSNumber *)fineValue coarseValue:(NSNumber *)coarseValue lockWindow:(BOOL)lockWindow {
    NSNumber *currentPersistedFineValue;
    NSNumber *currentPersistedCoarseValue;
    NSInteger window = [SKANSnippet getCurrentSkanWindow];
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        
    switch (window) {
    case 0:
        currentPersistedFineValue = [userDefaults objectForKey:CONVERSION_VALUE_KEY];
        currentPersistedCoarseValue = [userDefaults objectForKey:P0_COARSE];
        [userDefaults setValue:fineValue forKey:CONVERSION_VALUE_KEY];
        [userDefaults setValue:currentPersistedFineValue forKey:P0_PREV_FINE_VALUE];
        [userDefaults setValue:coarseValue forKey:P0_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P0_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P0_WINDOW_LOCK_TS];
        }
            
        break;
    
    case 1:
        currentPersistedCoarseValue = [userDefaults objectForKey:P1_COARSE];
        [userDefaults setValue:coarseValue forKey:P1_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P1_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P1_WINDOW_LOCK_TS];
        }
            
        break;
    
    case 2:
        currentPersistedCoarseValue = [userDefaults objectForKey:P2_COARSE];
        [userDefaults setValue:coarseValue forKey:P2_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P2_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setValue:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P2_WINDOW_LOCK_TS];
        }
            
        break;
    }
    
    [SKANSnippet setLastSkanCallTimestamp];
}

+ (BOOL)isSkanWindowOver {
    NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
    return thirdSkan4WindowInSec <= timeDiff;
}

// Revenues are being accumulated and saved by Ad monetization and non ad monetization events, total sum and break down by skan windows.
+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency isAdmon:(BOOL)isAdmon  {
    
    NSString *key = isAdmon ? TOTAL_ADMON_REVENUE_BY_CURRENCY : TOTAL_REVENUE_BY_CURRENCY;
    [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
        
    NSInteger window = [SKANSnippet getCurrentSkanWindow];
    switch (window) {
        case 0:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P0 : TOTAL_REVENUE_P0 ;
            break;
        case 1:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P1 : TOTAL_REVENUE_P1 ;
            break;
            
        case 2:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P2 : TOTAL_REVENUE_P2 ;
            break;
        case -1:
            key = nil;
            return;
    }
    
    [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
}

// Coarse value is being sent on requests and responses as an Int and being translated into the system defined coarse value upon API execution.
+ (NSString *)resolveCoarseValueFrom:(NSNumber *)value {
    if(@available(iOS 16.1, *)) {
        if (!value) {
            return nil;
        }
        
        switch ([value integerValue]) {
            case 0:
                return SKAdNetworkCoarseConversionValueLow;
            case 1:
                return SKAdNetworkCoarseConversionValueMedium;
            case 2:
                return SKAdNetworkCoarseConversionValueHigh;
            default:
                return nil;
        }
    }
    
    return nil;
}

+ (NSDictionary*)getTotalRevenue:(NSString *)revenueKey {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    if (![userDefaults objectForKey:revenueKey]){
        [userDefaults setObject:[NSDictionary dictionary] forKey:revenueKey];
    }
    
    return [userDefaults objectForKey:revenueKey];
}

+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency forKey:(NSString *)revenueKey {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *revenues = [[SKANSnippet getTotalRevenue:revenueKey] mutableCopy];
    
    NSNumber *currentRevenue = @(0);

    if ([revenues objectForKey:currency]) {
        currentRevenue = [revenues objectForKey:currency];
    }

    currentRevenue = @([currentRevenue floatValue] + [newRevenue floatValue]);
    [revenues setObject:currentRevenue forKey:currency];
    [userDefaults setObject:revenues forKey:revenueKey];
    
}

@end

S2S統合の更新

実装の検証やトラブルシューティングのために、SKAdNetworkメタデータとのサーバ間統合を強化しました(すべての実装で推奨)。

メタデータ構造

データ検索

getSkanDetails メソッドを使用してメタデータ辞書を抽出し、Session および Event エンドポイント API リクエストのクエリパラメータとして追加するためにサーバーに転送します。

クリティカル:メタデータは、セッション通知エンドポイントとイベント通知エンドポイントを経由してSingularに報告されるすべてのセッションとすべてのイベントで転送される必要があります。

Objective-C
NSDictionary *skanMetadata = [SKANSnippet getSkanDetails];

// Forward skanMetadata to your server for S2S API enrichment

アプリライフサイクルの実装

SKAN 4.0の機能でアトリビューションを完全にカバーするために、適切なアプリのライフサイクルポイントでSKAdNetworkのメソッドを統合してください。

実装例

実装上の注意事項

  • メインスレッドのブロックを防ぐため、変換値の更新に非同期メソッドを使用。
  • すべてのSKAN関連データは、サーバー送信前に辞書形式で収集されます。
  • Appleのプライバシー優先のアプローチに従いつつ、本質的なアトリビューショントラッキングを実現
  • 収益追跡には、正確な報告のための金額と通貨指定が含まれます。

アプリファーストローンチ

アトリビューショントラッキングのためにアプリをSKAdNetworkに登録し、最初のセッションデータをSingularのエンドポイントに送信します。アトリビューショントラッキングを確立するために、最初のアプリ起動時にのみ実行されます。

Objective-C
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues]; // to Singular launch EP

セッション管理

各セッション後にコンバージョン値を更新し、ユーザーエンゲージメントを追跡するために更新されたSKAN詳細を送信します。

Objective-C
[SKANSnippet updateConversionValuesAsync:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Conversion value update failed: %@", error);
    } else {
        NSLog(@"Values updated - Fine: %@, Coarse: %@, Lock: %d", fine, coarse, lock);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues];

イベントトラッキング

コンバージョン値を更新し、イベントデータをSingularのイベントエンドポイントに送信することで、収益以外のイベントを処理します。

Objective-C
[SKANSnippet updateConversionValuesAsync:@"event_name" 
                   withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Event conversion update failed: %@", error);
    } else {
        NSLog(@"Event values updated - Fine: %@, Coarse: %@", fine, coarse);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"];

収益トラッキング

通貨と関連するコンバージョン値で収益額を更新し、購入関連のアクティビティ用にSingularのイベントエンドポイントに送信することで、収益イベントを管理します。

Objective-C
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD" isAdMonetization:NO];
[SKANSnippet updateConversionValuesAsync:@"revenue_event_name" 
                   withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Revenue conversion update failed: %@", error);
    } else {
        NSLog(@"Revenue values updated - Fine: %@, Coarse: %@", fine, coarse);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"];

コンバージョン値API

クライアント側のインターフェイス実装の代わりにREST APIエンドポイントを使用した、SKAdNetworkのコンバージョン値をレポートするためのサーバー側の代替アプローチ。

APIの概要

実装方法

SKAdNetworkのコンバージョン値は、同一のデータフローとレポート整合性を持つ2つのメソッドでレポートすることができます。

  1. ダイレクトSKAdNetworkインターフェースクライアント側の実装(上記参照
  2. サーバーサイド統合:コンバージョン値APIエンドポイントの使用

コンバージョンバリューAPIエンドポイントは、クライアント側のインターフェースと同じパラメータを受け入れ、技術的なアーキテクチャに最適な実装を柔軟に選択することで、一貫したアトリビューショントラッキングを保証します。


APIエンドポイント

HTTPメソッドとURL

GET https://sdk-api-v1.singular.net/api/v2/conversion_value

必須パラメータ

APIキー

パラメータ 説明
a Developer Toolsの単一SDKキー。 報告用APIキーは使用しないでください。

sdkKey_afdadsf7asf56

デバイス識別子

パラメータ 説明
idfa 広告トラッキングとアトリビューション用の広告主識別子(IDFA)。 iOS 14.5以降、ATTフレームワークのオプトインが必要。利用できない場合は省略(NULLまたは空文字列を渡さない)。

DFC5A647-9043-4699-B2A5-76F03A97064B
idfv Identifier for Vendors (IDFV) - ATTステータスに関係なく、すべてのリクエストで必要。アプリのエコシステム全体でベンダー/開発者ごとに一意。

21DB6612-09B3-4ECC-84AC-B353B0AF1334

デバイスパラメータ

パラメータ 説明
p アプリのプラットフォーム(このAPIでは "iOS "でなければならない)。

iOS
v セッション時のデバイスのOSバージョン。

16.1

アプリケーション・パラメーター

パラメータ 説明
i App Identifier(iOSアプリケーションのバンドルID、大文字と小文字を区別)。

com.singular.app
app_v アプリケーションのバージョン。

1.2.3

イベントパラメータ

パラメータ イベント名
n 追跡するイベント名(最大32 ASCII文字)。セッションには__SESSION__ を使用。イベントの場合は、Event API経由でSingularに送信されたものと同じ名前とケーシングを使用します。

sng_add_to_cart

変換値パラメータ

パラメータ 説明
skan_current_conversion_value
iOS 15.4+
前回のセッション/イベント時の最新のSKAdNetwork変換値(0~63)。

7
p1_coarse
iOS 16.1+
postback_sequence 1の最新のSKAdNetwork粗変換値(0-2)。

0
p2_coarse
iOS 16.1+
postback_sequence 2(0-2)の最新のSKAdNetwork粗変換値。

1

収益トラッキングパラメータ

パラメータ 説明
skan_total_revenue_by_currency
iOS 15.4 以上
IAPまたはAll Revenueモデルで必須。現在のIAP収益の集計(広告収益化を除く)、JSON URLエンコード文字列。

%7B%22USD%22%3A9.99%7D
skan_total_admon_revenue_by_currency
iOS 15.4+
AdmonまたはAll Revenueモデルの場合は必須。JSON URLエンコードされた文字列。

%7B%22USD%22%3A1.2%7D

タイムスタンプパラメータ

パラメータ 説明
skan_first_call_to_skadnetwork_timestamp
iOS 15.4 以上
基礎となるSKAdNetwork APIを最初に呼び出したUnixタイムスタンプ。

1483228800
skan_last_call_to_skadnetwork_timestamp
iOS 15.4+
このセッション通知の時点で、基礎となるSKAdNetwork APIへの最新の呼び出しのUnixタイムスタンプ。

1483228800

リクエスト例

サンプル実装

コードサンプルは、コアとなる必須パラメータを示す。実装する場合は、すべての必須パラメータを含め、本番使用前に正しい値を検証してください。

PythoncURLHTTP
import requests

params = {
    'a': 'sdk_key_here',
    'p': 'iOS',
    'i': 'com.singular.app',
    'v': '16.1',
    'idfa': 'DFC5A647-9043-4699-B2A5-76F03A97064B',
    'idfv': '21DB6612-09B3-4ECC-84AC-B353B0AF1334',
    'n': '__SESSION__',
    'app_v': '1.2.3',
    'skan_current_conversion_value': 7,
    'p1_coarse': 0,
    'p2_coarse': 1,
    'skan_total_revenue_by_currency': {"USD":9.99},
    'skan_total_admon_revenue_by_currency': {"USD":1.2},
    'skan_first_call_to_skadnetwork_timestamp': 1510090877,
    'skan_last_call_to_skadnetwork_timestamp': 1510090877
}

response = requests.get('https://sdk-api-v1.singular.net/api/v2/conversion_value', params=params)
print(response.json())

レスポンスの形式

成功レスポンス

HTTP 200 - エラーや理由のない OK レスポンスは、リクエストが処理のためにキューに送られたことを示します。

{
   "conversion_value":1,
   "skan_updated_coarse_value":0,
   "postback_sequence_index":0,
   "status":"ok"
}

レスポンス・パラメータ

キー 説明
conversion_value 新しい細かい変換値 0-63
skan_updated_coarse_value 新しい粗い変換値 0-2
postback_sequence_index SKANポストバック測定期間(0=ポストバック1、1=ポストバック2、2=ポストバック3)。更新する粗い値のキーを示す 0-2
status 処理状況 ok

考えられるエラー

  • 最後の変換更新から24時間以上経過(28032時間)、更新ウィンドウが閉じられた。
  • 不明なプラットフォームエラー - iOS以外のプラットフォーム
  • 変換管理:無効なパラメータが指定されました
  • コンバージョン管理:アプリのコンバージョンモデルが見つかりません
  • 無効な測定期間
  • コンバージョン管理:所有者の通貨が見つかりません