SKAdNetwork는 iOS에서 앱 인스톨 광고 캠페인을 개인정보 보호 친화적으로 측정할 수 있도록 Apple에서 제공하는 프레임워크입니다.SKAdNetwork 4.0은 프레임워크의 최신 버전으로, 몇 가지 중요한 세부 사항에서 SKAdNetwork 3.0과 다릅니다.
이 가이드를 통해 Singular S2S 연동의 일부로 SKAdNetwork 4.0을 구현할수 있습니다.
- SK애드네트워크 4.0에 대한 자세한 내용은 SKAN 4.0 FAQ에서 확인하세요.
- SKAdNetwork에 대한 자세한 배경 지식은 SKAdNetwork 3.0 S2S 구현 가이드를 참조하세요.
SKAdNetwork 4.0 S2S 구현하기
Singular의 SKAdNetwork 솔루션은 다음과 같은 구성 요소로 이루어져 있습니다:
- SKAdNetwork를 구현하기 위한 클라이언트 측 코드
- 모든 네트워크에서 포스트백 유효성 검사 및 집계
-
사기 방지:
- 서명 검증 및 트랜잭션 ID 중복 제거.
- 서명되지 않은 파라미터(전환 값 및 지오데이터)를 검증하기 위한 네트워크 보안 설정.
- 전환 가치 관리: Singular의 대시보드에서 동적으로 구성할 수 있는 기능을 제공하여 설치 후 활동을 SKAdNetwork 전환 가치로 인코딩할 수 있습니다.
- 리포팅: 제한된 SKAdNetwork 캠페인 ID를 변환하고 더 많은 마케팅 파라미터와 세분화로 데이터를 보강합니다.
- 파트너 포스트백: 이벤트 및 구매으로 디코딩된 전환 가치를 이벤트 및 구매으로 변환하여 SKAdNetwork 포스트백을 전송하는 기능으로, 최적화에 매우 중요합니다.
서버 대 서버(S2S) Singular 연동을 사용하는 고객의 경우, SKAdNetwork 구현은 크게 두 가지 부분으로 구성됩니다:
- SKAdNetwork 클라이언트 측 구현: 이 부분은 앱을 SKAdNetwork에 등록하고 SKAdNetwork 전환 가치를 지능적으로 관리하는 데 매우 중요합니다. 즉, 이를 구현하면 SKAdNetwork 어트리뷰션 및 관련 인스톨 후 활동을 기반으로 캠페인을 최적화할 수 있습니다.
- S2S 연동 업데이트: 이 부분은 클라이언트 측 구현의 유효성을 검사하고 문제를 해결하는 데 중요합니다. 현재 Singular의 S2S 세션과 이벤트 엔드포인트를 통해 Singular로 전송되는 이벤트와 세션을 SKAdNetwork 데이터(전환 값 및 업데이트 타임스탬프)로 보강함으로써, Singular는 앱 측에서 구현이 제대로 이루어졌는지 검증할 수 있습니다.
SKAdNetwork 클라이언트 측 구현
Singular는 SKAdNetwork 등록 및 전환 값 관리를 지원하는 코드 샘플을 제공합니다. 이 코드 샘플은 다음과 같은 부분을 담당합니다:
- SKAdnetwork 지원 및 등록
- 전환 가치 관리:
- 이 코드는 Singular의 엔드포인트와 동기식으로 통신하여 구성된 전환 모델에 따라 다음 전환 값을 수신합니다. 이벤트/세션/구매을 보고하고, 이에 대한 응답으로 Singular의 대시보드에서 측정하도록 구성된 설치 후 활동을 나타내는 인코딩된 숫자, 즉 다음 전환 값을 가져옵니다.
- 이 코드는 또한 측정 기간별로 SKAdnetwork 메타데이터를 수집합니다. 이 메타데이터는 유효성 검사 및 다음 전환 값 계산에 모두 사용됩니다:
- 기본 SKAdNetwork 프레임워크에 대한 첫 번째 호출 타임스탬프
- 기본 SKAdNetwork 프레임워크에 대한 마지막 호출 타임스탬프
- 마지막으로 업데이트된 포스트백 값(코러스 및 파인 모두)
- 사용자가 생성한 총 구매 및 애드몬 구매
S2S 연동 업데이트
필수
활성화된 전환 모델이 있으면 S2S 엔드포인트는 클라이언트 측 코드에서 업데이트할 다음 값을 포함하는 "conversion_value"라는 새 int 필드를 반환하기 시작합니다.
권장 사항
연동을 검증하고 추가 인사이트를 지원하려면 코드 샘플을 사용하여 현재 S2S 연동을 확장하는 것이 좋습니다. Singular의 S2S 이벤트 및 세션 엔드포인트는 이미 다음과 같은 추가 SKAdNetwork 파라미터 가져오기를 지원합니다:
- P0,P1,P2 전환 값 세분화 및 거시화
- P0,P1,P2 이전 전환 값 세분화 및 거시화
- 기본 SKAdNetwork 프레임워크에 대한 호출의 마지막 타임스탬프
- 기본 SKAdNetwork 프레임워크에 대한 호출의 첫 번째 타임스탬프
- 윈도우 측정 기간당 집계된 구매(P0, P1, P2)
연동 흐름
위의 다이어그램은 S2S 고객에 대한 SKAdNetwork 흐름을 보여줍니다:
- 첫째, 앱의 코드가 SKAdNetwork 전용 엔드포인트를 통해 Singular 서버와 통신하여 앱에서 발생하는 이벤트/세션/매출 이벤트에 따라 최신 전환 값을 동기식으로 가져오고, 이 값으로 SKAdNetwork 프레임워크를 업데이트합니다.
- 둘째, 앱은 나중에 유효성 검사에 사용될 SKAdNetwork 데이터로 기존 이벤트와 세션을 보강합니다.
- 앱이 새로운 전환 값으로 SKAdNetwork 프레임워크 업데이트를 완료하고 SKAdNetwork 타이머가 만료되면, SKAdNetwork 포스트백이 네트워크로 전송됩니다.
- 네트워크는 보안 설정 또는 일반 설정을 통해 이를 Singular로 전달합니다.
- Singular는 포스트백을 처리합니다:
- 서명 유효성 검사
- 구성된 전환 모델을 기반으로 전환 값을 디코딩합니다.
- 네트워크 정보로 포스트백을 보강합니다. 데이터는 파트너와의 연동을 통해 SKAdNetwork 및 네트워크 캠페인 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. 이벤트를 재시도할 경우 동일한 구매이 중복 계산되지 않도록 이 메서드를 호출하지 마세요.*/
+ (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 연동 업데이트(권장)
다음 SKAdNetwork 메타데이터로 S2S 연동을 업데이트하세요(이 메타데이터는 SKAdNetwork 구현 유효성 검사를 위해 Singular에 보고되는 모든 세션 및 이벤트에서 전달되어야 합니다):
- SKAN_CURRENT_CONVERSION_VALUE: 최신 미세 전환 값
- PREV_FINE_VALUE: 이전 미세 전환 값
- SKAN_FIRST_CALL_TO_SKADNETWORK_TIMESTAMP: 기본 SkAdNetwork API에 대한 첫 번째 호출의 유닉스 타임스탬프입니다.
- SKAN_LAST_CALL_TO_SKADNETWORK_TIMESTAMP: 기본 SkAdNetwork API에 대한 마지막 호출의 유닉스 타임스탬프입니다.
- p0/1/2_coarse: 창별 최신 거친 값
- p0/1/2_prev_coarse_value: 창별 이전 거친 값
- p0/1/2_window_lock: 창별 창 잠금이 있는 마지막 업데이트의 유닉스 타임스탬프
- p0/1/2_total_iap_revenue: 창당 비광고 구매화 구매의 합계
- P0/1/2_총_애드몬_구매: 창당 광고 구매화 구매의 합계
- SKAN_총_구매_통화별: 비광고 구매화 구매의 합계
- SKAN_총_애드몬_구매_통화별: 광고 구매화 구매의 합계
다음 함수는 이러한 값을 추출하는 방법을 보여줍니다:
NSDictionary *values = [SKANSnippet getSkanDetails];
이제 이러한 매개변수를 서버 측으로 전송한 후에는 서버 간 API 엔드포인트를 통해 전달할 수 있습니다. 자세한 내용은 서버 간 API 참조에서 이러한 파라미터를 검색하세요.
앱 라이프사이클 흐름 예시
// 앱 최초 실행 시에만
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //Singular "LAUNCH" 엔드포인트로
// 각 세션이 처리된 후
[SKANSnippet updateConversionValueAsync:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues] //Singular "LAUNCH" 엔드포인트로
// 비구매 이벤트 처리 후
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"] //Singular "EVT" 엔드포인트로
// 구매 이벤트 처리 후
[SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"] //Singular "EVT" 엔드포인트로