Skadnetwork 3.0 S2S実装ガイド

新しいバージョンのSKAdNetworkを実装するには、[BETA] SKAdNetwork 4.0 S2S実装を参照。

 

SKAdNetworkの概要

SKAdNetworkは、iOSのアプリインストール広告キャンペーンのプライバシーに配慮した計測を可能にするためにAppleが提供するフレームワークです詳細はこちら)。 このフレームワークは、ユーザーの識別情報を損なうことなく、アプリインストールキャンペーンのコンバージョン率を計測するのに役立ちます。

SKAdNetworkの仕組み

SKAdNetworkでは、アトリビューションプロセスはApp StoreがAppleのサーバーを通して行います。そして、アトリビューション情報はユーザー識別子や時間情報から切り離され、ネットワークに送られます。

Screen_Shot_2020-09-16_at_18.57.56.png

広告がクリックされ、ストアが開かれると、パブリッシングアプリとネットワークは、ネットワーク、パブリッシャー、キャンペーンIDなどの基本情報を提供する。広告主アプリが起動し、SKAdNetworkに登録されると、デバイスはコンバージョン成功の通知をネットワークに送信します。広告アプリが報告できるコンバージョン値と一緒に、添付された値が報告されます。

この通知は、最初の起動から少なくとも24時間後に送信され、デバイスやユーザーを特定する情報は一切含まれません。

さらに、App Storeは、広告されたアプリが元の広告やパブリッシャーについて知らないように処理を行います。このようにして、インストールするユーザーについて何も知ることなく、ネットワークにインストールが通知されます。

SKAdNetworkを利用する際の注意点

SKAdNetworkにはいくつかの大きな利点があります。それは以下の全ての情報を提供します:

  • 同意なしで機能するラストクリックのアトリビューション
  • ソース、キャンペーン、パブリッシャーの内訳
  • インストール後のコンバージョン値(最大64の離散値)
  • インストールを検証するための暗号署名

しかし、現在のSKAdNetworkは骨抜きであり、確実に機能させるには慎重な実装と複数のエンティティ間の調整が必要である。

以下は、SKAdNetworkの現在の制限事項である:

  • ユーザーレベルのデータがない
  • ビュースルーアトリビューションなし
  • コンバージョン値の範囲が限られている:
    • インストール/再インストールごとに1つの変換イベント
    • 最大64の変換値(6ビット)
  • 粒度の制限
    • 最大100キャンペーン値
    • 広告グループとクリエイティブレベルの表現なし
  • LTV/ロングコホートなし
  • 不正の可能性
    • コンバージョン値自体が署名されていない(操作可能)
    • ポストバックの複製が可能

これらの問題を克服するために、SingularはSKAdNetwork実装のためのパブリックスタンダードと、SKAdNEtworkをナビゲートするためのいくつかのブログ記事をリリースしました:

SKAdNetwork S2Sの実装

SKAdNetworkソリューションは以下のコンポーネントで構成されています:

  • SKAdNetworkを実装するクライアント側のコード
  • 全てのネットワークからのポストバック検証及びアグリゲーション
  • 不正防止
    • 署名検証とトランザクションIDのデデューピング
    • 署名されていないパラメータ(コンバージョン値とジオデータ)を検証するためのネットワークとの安全なセットアップ。
  • コンバージョン値の管理 :Singularのダッシュボードで、どのインストール後のアクティビティをSKAdNetworkのコンバージョン値にエンコードするかを動的に設定できます。
  • レポーティング:限られたSKAdNetworkキャンペーンIDを変換し、より多くのマーケティングパラメータと粒度でデータを充実させます。
  • パートナーのポストバック: SKAdNetworkのポストバックを、イベントと収益にデコードされたコンバージョン値で送信します。

サーバー間のSingularインテグレーションをご利用のお客様には、SKAdNetworkの実装は主に2つの部分で構成されています:

  1. SKAdNetworkクライアント側の実装: SKAdNetworkクライアント側の実装:この部分は、お客様のアプリをSKAdNetworkに登録し、SKAdNetworkのコンバージョン値をインテリジェントに管理するために重要です。つまり、これを実装することで、SKAdNetworkのアトリビューションと関連するインストール後のアクティビティに基づいて、キャンペーンを最適化できるようになります。
  2. S2S統合アップデート(オプション):このパートは、クライアント側の実装を検証し、トラブルシューティングを行うために重要です。SingularのS2Sセッションとイベントエンドポイントを経由して今日SIngularに送られるイベントとセッションをSKAdNetworkデータ(コンバージョン値と更新タイムスタンプ)でエンリッチすることで、Singularはアプリ側で実装が適切に行われたことを検証することができます。

SKAdNetworkクライアント側の実装

SingularはSKAdNetworkへの登録とコンバージョン値の管理をサポートするコードサンプルを提供しています。これらのコードサンプルは以下の部分を担当しています:

  1. SKAdNetworkのサポートと登録
  2. 変換値の管理:
    • このコードはSingularのエンドポイントと同期的に通信し、設定されたコンバージョンモデルに基づいて次のコンバージョン値を受け取ります。イベント/セッション/収益を報告し、その応答として次のコンバージョン値を取得します。この値は、Singularのダッシュボードで測定用に設定されたインストール後のアクティビティを表すエンコードされた数値です。
    • このコードは、SKAdnetworkのメタデータも収集し、検証と次のコンバージョン値の計算の両方に使用される:
      • 基礎となるSKAdNetworkフレームワークへの最初の呼び出しタイムスタンプ
      • 基礎となるSKAdNetworkフレームワークへの最初の呼び出しタイムスタンプ。
      • 最後に更新されたコンバージョン値

S2S統合アップデート

必須

コンバージョンモデルがアクティブになると、S2Sエンドポイントは "conversion_value "という新しいintフィールドを返すようになります。

オプション

統合を検証し、潜在的な実装上の問題をトラブルシュートするために、現在のS2S統合を拡張するコードサンプルを使用することをお勧めします。SingularのS2Sイベントとセッションエンドポイントは、既に以下のような追加SKAdNetworkパラメータの取得をサポートしています:

  • 最新のコンバージョン値
  • SKAdNetworkフレームワークへの呼び出しの最後のタイムスタンプ
  • 基礎となるSKAdNetworkフレームワークへの呼び出しの最初のタイムスタンプ

統合フロー

Screen_Shot_2020-09-16_at_18.59.13.png

上の図は、S2S顧客のSKAdNetworkフローを示しています:

  1. まず、アプリ内のコードがSingularサーバーと通信し(SKAdNetwork専用のエンドポイントを通して)、アプリ内で発生したイベント/セッション/収益イベントに基づいて最新のコンバージョン値を同期的に取得し、この値でSKAdNetworkフレームワークを更新します。
  2. 次に、アプリは既存のイベントやセッションをSKAdNetworkのデータでエンリッチします。
  3. アプリが新しい変換値でSKAdNetworkフレームワークを更新し終わり、SKAdNetworkタイマーが切れると、SKAdNetworkポストバックがネットワークに送信されます。
  4. ネットワークはこのポストバックをSingularに転送します(セキュアセットアップまたは通常セットアップ)。
  5. Singularはポストバックを以下のように処理します:
    • 署名の検証
    • 設定された変換モデルに基づいて変換値をデコードする。
    • パートナーとの統合やSKAdNetworkの内部キャンペーンIDに基づいて、より外向きのマーケティングパラメータでポストバックをエンリッチします。
    • デコードされたポストバックをBIとパートナーに送信します。

重要な注意点として、インストールやデコードされたイベントを含むSKAdNetworkの情報には、既存のデータセットとの混合を避けるために、別のレポート/API/ETLテーブルやポストバックのセットを介してアクセスできるようになります。これは、SKAdNetworkを既存のキャンペーンと並行して測定・テストするために、次の数週間は特に重要です。

SKAdNetworkネイティブiOSコードサンプル

SKAdNetwork インターフェース

このインターフェースには以下のSKAdNetworkコンポーネントが含まれています:

SKAdNetworkへの登録

  • このメソッドは SKAdNetwork にアプリを登録します。基礎となるApple APIメソッドは、デバイスがそのアプリのアトリビューションデータを持っている場合、通知を生成し、24時間タイマーを開始します。
  • デバイスはタイマーが切れた後、0~24時間以内にインストール通知を広告ネットワークのポストバックエンドポイントに送信します。
  • コンバージョン値を前回よりも大きな値で更新すると、最初のタイマーがリセットされ、新しい24時間間隔になることに注意してください(詳しくはこちら)。

換算値の管理と更新

  • コンバージョン値は、以下のメソッドによって取得されたデバイスのインストール後のアクティビティと、ユーザーが動的に設定できる選択されたコンバージョンモデルに基づいて計算されます。
  • このセクションのメソッドは、選択されたコンバージョンモデルと報告されたインストール後のアクティビティに従って、Singularのエンドポイントから次のコンバージョン値を取得します(上記のドキュメントを参照)。
  • 以下のメソッドは、以下のインストール後のアクティビティに基づいてコンバージョン値を更新します:
      • セッション:SKAdNetworkでのユーザーのリテンション測定に重要です。
      • コンバージョンイベント: SKAdNetworkでのインストール後のコンバージョンイベント測定に重要です。
      • 収益イベント:SKAdNetwork での収益測定に重要。
//  SKANSnippet.h
  
  #import <Foundation/Foundation.h>
  
  @interface
  SKANSnippet : NSObject
  
  // SkAdNetwork アトリビューションに登録します。 アプリが起動したら、できるだけ早くこのメソッドを呼び出す必要があります。
  + (void)registerAppForAdNetworkAttribution;
  
  /* 保持率とコホートを追跡するには、各セッションの後にこのメソッドを呼び出す必要があります。 セッションの詳細をレポートし、必要に応じてこのセッションによるコンバージョン値を更新します。 変換値は、新しい値が前の値より大きい場合にのみ更新されます。 メソッドに渡されるコールバックはオプションであり、変換値が更新された後にそれを使用してコードを実行できます。*/
  + (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;
  
  /* SKAdNetwork でコンバージョン イベントを追跡するには、各イベントの後にこのメソッドを呼び出す必要があります。 イベントの詳細をレポートし、必要に応じてこのイベントによるコンバージョン値を更新します。 メソッドに渡されるコールバックはオプションであり、変換値が更新された後にそれを使用してコードを実行できます。*/
  + (void)updateConversionValueAsync:(NSString*)eventName
withCompletionHandler:(void(^)(int, NSError*))handler; /* SKAdNetwork で収益を追跡するには、各収益イベントの前にこのメソッドを呼び出す必要があります。 総収益が更新されるため、「updateConversionValueAsync」を呼び出すと、総収益額に応じて新しいコンバージョン値が決定されます。
注記:
1. 'updateConversionValueAsync' を呼び出す前にこのメソッドを呼び出して、収益が更新されていることを確認します。
2. イベントを再試行する場合は、同じ収益が 2 回カウントされないように、このメソッドの呼び出しを避けてください。*/ + (void)updateRevenue:(double)amount andCurrency:(NSString*)currency; // 現在の変換値を取得します (ない場合は nil) + (NSNumber *)getConversionValue; @end

SKAdNetworkインターフェースの実装

//  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"
  
  // UserDefaults ストレージのキー
  #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
  
  @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];
      }
      
      // 呼び出しスレッドがフリーズしないようにロックを非同期にする
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
          
          [lockObject lock];
          
          NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
          
          NSString* bundleIdentifier = @"";
          
          components.queryItems = @[
              [NSURLQueryItem queryItemWithName:@"a" value:@""],
              [NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
              [NSURLQueryItem queryItemWithName:@"app_v" value:@""],
              [NSURLQueryItem queryItemWithName:@"n" value:eventName],
              [NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
              [NSURLQueryItem queryItemWithName:@"idfv" value:@""],
              [NSURLQueryItem queryItemWithName:@"idfa" value:@""],
              [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; }

SKAdNetwork メタデータとユーティリティメソッド

このセクションでは、上記の実装で使用されるユーティリティの実装を担当する。これらのユーティリティは、次の変換値を計算するために必要なメタデータを維持・保存するために重要であることに注意

以下のユーティリティが実装されています:

UserDefaultsへの値の保存(および値の取得)

  • 基礎となる SKAdNetwork API への最初の呼び出しタイムスタンプ。
  • 基礎となる SKAdNetwork API への最終呼び出しタイムスタンプ
  • 通貨別収益合計

異なる表現間の値の変換

  • 辞書からJSON文字列
  • JSONから辞書へ
+ (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統合を更新してください(このメタデータは、SKAdNetwork実装検証のため、Singularに報告される全てのセッション&イベントで転送される必要があります):

  • skan_conversion_value - 最新のコンバージョン値。
  • skan_first_call_timestamp - 基礎となるSkAdNetwork APIへの最初のコールのUnixタイムスタンプ。
  • skan_last_call_timestamp - 基盤となる SkAdNetwork API の最新コールの Unix タイムスタンプ。

以下のコード・スニペットは、これらの値を抽出する方法を示しています:

NSDictionary *values = @{
      @"skan_conversion_value":
        [[SKANSnippet getConversionValue] stringValue]],
      @"skan_first_call_timestamp": 
[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]], @"skan_last_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]] };

これらのパラメータをサーバー側に送信すると、サーバー間 API エンドポイントを経由して転送することができます。詳細については、server-to-server APIリファレンスでこれらのパラメータを検索してください。

アプリのライフサイクルフローの例

// アプリ起動時
  [SKANSnippet registerAppForAdNetworkAttribution];
  
  // 各セッションが処理された後
  [SKANSnippet updateConversionValueAsync:handler];
  
  // 非収益イベントの処理後
  [SKANSnippet updateConversionValueAsync:@"event_name" 
withCompletionHandler:handler]; // 収益イベントの処理後 [SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"]; [SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];

コード変更履歴

  • 1 2020年10月
    • 修正 [IMPORTANT]:`getConversionValueFromServer` のロックメカニズムが正しく開始されていなかった。
    • 改善: サーバのレスポンスから失敗理由を返すようにした。
  • 23 Sep 2020
    • 修正[IMPORTANT]:`setConversionValue` が `updateConversionValue` を呼び出すようになりました。
    • 改善: サーバーから最新の変換値を取得する際のエラー処理
  • 2020 年 9 月 15 日
    • 重要]を変更しました: 変換値が設定されていない場合、`getConversionValue` が返すデフォルト値は null ではなく 0 になります。
    • 改善されました:収益の報告と処理
    • 改善されました:次のコンバージョン値の非同期取得

実装のテスト

コンバージョン管理

アプリのフローをシミュレートし、実装をテストするには、コンバージョン管理の本番エンドポイント(SINGULAR_API_URL)を次のテストエンドポイントに置き換えます。

    • https://skadnetwork-testing.singular.net/api/v1/conversion_value
    • APIキーは必要ありません。

推奨されるテストフロー

  • アプリ内で3つの異なるイベントを発生させます。
      • すべてが正しく実装されている場合
        • 各イベントの後、updateConversionValueAsyncが呼び出されるはずです(デフォルトの変換値が0に初期化されていることを確認してください)。
        • テスト・エンドポイントは、アプリから現在の変換値を受け取り、次の値を返します。
    • 返された変換値をログに記録して、すべてが期待通りに動作していることを確認します。
      • テストエンドポイントは0-2の間の値を返すように実装されています。
      • したがって、3回目の呼び出しの後は空の文字列を返し、最後の値が最終的なSKAdNetworkの変換値として使用されます。