[ベータ] Skadnetwork 4.0 S2S実装

SKAdNetworkは、iOS上のアプリインストール広告キャンペーンのプライバシーに配慮した計測を可能にするためにAppleが提供するフレームワークです。SKAdNetwork 4.0はフレームワークの最新バージョンで、SKAdNetwork 3.0とはいくつかの重要な点が異なります。

SKAdNetwork 4.0をSingular S2Sとの統合の一部として実装するには、このガイドをご利用ください。

SKAdNetwork 4.0 S2Sインプリメンテーション

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

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

サーバー間(S2S)の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フレームワークへの最後の呼び出しタイムスタンプ
      • 最後に更新されたポストバック値(CoarseとFineの両方)
      • ユーザーによって生成された総収入とアドモン収入

S2S統合アップデート

必須

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

推奨

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

  • P0,P1,P2の変換値 fine と coarse
  • P0,P1,P2以前の変換値(細かい値と粗い値
  • SKAdNetworkフレームワークへの呼び出しの最後のタイムスタンプ。
  • SKAdNetworkフレームワークへの呼び出しの最初のタイムスタンプ
  • ウィンドウ測定期間(P0,P1,P2)ごとの集計収益

統合フロー

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とNetworkのキャンペーンIDを結合することで、パートナーとの統合からデータを収集します。
    • デコードされたポストバックをBIとパートナーに送信します。

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

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

SKAdNetwork インターフェース

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

SKAdNetworkへの登録

  • このメソッドは SKAdNetwork を初めて呼び出します。
  • P0の変換値を0に設定する。
  • 基礎となる Apple API メソッドは、デバイスにそのアプリのアトリビューションデータがある場合、通知を生成します。

コンバージョン値の管理

  • コンバージョン値は、以下のメソッドによって取得されたデバイスのインストール後のアクティビティと、ユーザーが動的に設定できる選択されたコンバージョンモデルに基づいて計算されます。
  • このセクションのメソッドは、選択されたコンバージョンモデルと報告されたインストール後のアクティビティに従って、Singularのエンドポイントから次のコンバージョン値を取得します(上記のドキュメントを参照)。
  • 以下のメソッドは、以下のインストール後のアクティビティに基づいてコンバージョン値を更新します:
    • セッション:SKAdNetworkでのユーザーのリテンション測定に重要です。
    • コンバージョンイベント: SKAdNetworkでのインストール後のコンバージョンイベント測定に重要です。
    • 収益イベント:SKAdNetwork での収益測定に重要。
//SKANSnippet.h
  
  #import <Foundation/Foundation.h>
  
  @interface SKANSnippet : NSObject
  
  /* SkAdNetwork アトリビューションに登録します。 アプリを初めて起動したら、できるだけ早くこのメソッドを呼び出す必要があります。 この関数は、変換値を 0 に設定し、追加の処理のためにタイムスタンプを更新します。*/
  + (void)registerAppForAdNetworkAttribution;
  
  /* 保持率とコホートを追跡するには、開いているアプリごとにこのメソッドを呼び出す必要があります。 セッションの詳細をレポートし、必要に応じてこのセッションによるコンバージョン値を更新します。 メソッドに渡されるコールバックはオプションであり、変換値が更新された後にそれを使用してコードを実行できます。*/
  + (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
  
  /* SKAdNetwork でコンバージョン イベントを追跡するには、各イベントの後、このイベントが Singular に送信される前に、このメソッドを呼び出す必要があります。 イベントの詳細をレポートし、必要に応じてこのイベントによるコンバージョン値を更新します。 メソッドに渡されるコールバックはオプションであり、変換値が更新された後にそれを使用してコードを実行できます。*/
  + (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
  
  /* SKAdNetwork で収益を追跡するには、各収益イベントの前にこのメソッドを呼び出す必要があります。 収益の合計が更新されるため、「updateConversionValuesAsync」を呼び出すと、収益の合計額に応じて新しいコンバージョン値が決定されます。
注記:
1. 'updateConversionValuesAsync' を呼び出す前にこのメソッドを呼び出して、収益が更新されていることを確認します。
2. イベントを再試行する場合は、同じ収益が 2 回カウントされないように、このメソッドの呼び出しを避けてください。*/ + (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon; /* 現在の細かい、粗い、ウィンドウロックされた値を取得します。 「fineValue」、「coarseValue」、「windowLock」の下の辞書に保存されます。 さらに、SKAN の目的に関連する他のすべての値が含まれます。*/ // 例: // { // "skan_current_conversion_value": 3, // "prev_fine_value": 2, // "skan_first_call_to_skadnetwork_timestamp": 167890942, // "skan_last_call_to_skadnetwork_timestamp": 167831134, // "skan_total_revenue_by_currency": { "USD": 1.2 }, // "skan_total_admon_revenue_by_currency": { "USD": 0.8 }, // "p0_coarse": 0, // "p1_coarse": 1, // "p2_coarse": nil, // "p0_window_lock": 167890942, // "p1_window_lock": nil, // "p2_window_lock": nil, // "p0_prev_coarse_value": 0, // "p1_prev_coarse_value": 0, // "p2_prev_coarse_value": nil, // "p0_total_iap_revenue": nil, // "p1_total_iap_revenue": nil, // "p2_total_iap_revenue": nil, // "p0_total_admon_revenue": nil, // "p1_total_admon_revenue": nil, // "p2_total_admon_revenue": nil // } + (NSDictionary *)getSkanDetails; @end

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

//  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"
  
  // NSUserDefaults の永続性とリクエスト用の SKAN キー
  #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_CURRNECY @"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 16.1, *)) {
          [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_CURRNECY] forKey:TOTAL_ADMON_REVENUE_BY_CURRNECY];
      //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]) {
                      if (handler) {
                          handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                   code:0
                                                               userInfo:@{NSLocalizedDescriptionKey:@"Illegal values recieved"}]);
                      }
                      
                      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];
      [userDefaults synchronize];
  }
  
  + (void)setLastSkanCallTimestamp {
      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
      [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:LAST_SKAN_CALL_TIMESTAMP];
      [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];
  }
  
  + (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;
  }
  
  // アクティブなスキャンウィンドウに基づいて更新された変換値を保持します。
  + (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;
  }
  
  // 収益は、広告収益化イベントと広告収益化以外のイベントによって蓄積され、合計され、スキャン ウィンドウごとに内訳されます。
  + (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency isAdmon:(BOOL)isAdmon  {
      NSString *key = isAdmon ? TOTAL_ADMON_REVENUE_BY_CURRNECY : 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];
  }
  
  // 粗い値はリクエストと応答で Int として送信され、API の実行時にシステム定義の粗い値に変換されます。
  + (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];
      [userDefaults synchronize];
  }
  
  @end
  

S2Sインテグレーションアップデート(推奨)

S2Sインテグレーションを以下のSKAdNetworkメタデータでアップデートしてください(このメタデータは、SKAdNetworkの実装検証のために、Singularに報告される全てのセッション&イベントで転送される必要があります):

  • SKAN_CURRENT_CONVERSION_VALUE:最新のファインコンバージョン値
  • prev_fine_value:以前のファインコンバージョン値
  • skan_first_call_to_skadnetwork_timestamp:基礎となる SkAdNetwork API への最初のコールの Unix タイムスタンプ
  • skan_last_call_to_skadnetwork_timestamp:基礎となる SkAdNetwork API への最新のコールの Unix タイムスタンプ。
  • p0/1/2_coarse:ウィンドウごとの最新の粗い値
  • p0/1/2_prev_coarse_value: ウィンドウごとの前の粗い値
  • p0/1/2_window_lock:ウィンドウごとのウィンドウロックによる最終更新のUnixタイムスタンプ
  • p0/1/2_total_iap_revenue:ウィンドウごとの非広告収益合計
  • p0/1/2_total_admon_revenue:ウィンドウごとの広告収益合計
  • skan_total_revenue_by_currency:広告収益以外の収益の合計
  • skan_total_admon_revenue_by_currency:広告収益合計

次の関数は、これらの値を抽出する方法を示しています:

NSDictionary *values = [SKANSnippet getSkanDetails];

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

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

// アプリの初回起動時のみ
  [SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //to Singular launch EP // 各セッションが処理された後 [SKANSnippet updateConversionValueAsync:handler]; NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //to Singular launch EP
// 非収益イベントの処理後 [SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler]; NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"] //to Singular evt EP
// 収益イベントの処理後 [SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"]; [SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"] //to Singular evt EP