콘텐츠로 이동

데이터 수집 커스텀 규칙

View

FTRUMConfig.enableTraceUserView = YES 설정을 활성화해야 합니다.

rumConfig.viewTrackingHandler = [CustomViewTracker new];

#import "FTDefaultUIKitViewTrackingHandler.h"

// 프로토콜 구현 방식 예시
@interface CustomViewTracker : NSObject <FTUIKitViewTrackingHandler>
// SDK 기본 view 수집 규칙이 필요한 경우에만 추가
@property (nonatomic, strong) FTDefaultUIKitViewTrackingHandler defaultHandler;
@end

@implementation CustomViewTracker

// SDK 기본 view 수집 규칙이 필요한 경우에만 추가
-(FTDefaultUIKitViewTrackingHandler *)defaultHandler{
    if (!_defaultHandler) {
        _defaultHandler = [FTDefaultUIKitViewTrackingHandler new];
    }
    return _defaultHandler;
}

- (FTRUMView *)rumViewForViewController:(UIViewController *)viewController {
    // 클래스명으로 정확히 매칭
    if ([viewController isKindOfClass:[HomeViewController class]]) {
        return [[FTRUMView alloc] initWithViewName:@"main_home" property:@{@"page_type": @"home"}];
    }
    // 프리픽스로 필터링
    else if ([NSStringFromClass([viewController class]) hasPrefix:@"FT"]) {
        return [[FTRUMView alloc] initWithViewName:[NSString stringWithFormat:@"ft_%@", NSStringFromClass([viewController class])] property:nil];
    }
    // accessibilityLabel로 설정
    else if (viewController.view.accessibilityLabel) {
        return [[FTRUMView alloc] initWithViewName:viewController.view.accessibilityLabel property:nil];
    }
    // 일부 페이지를 커스텀한 후, 나머지 페이지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)
    return [self.defaultHandler rumViewForViewController:viewController];

    // 추적을 건너뛰려면 nil을 반환
    return nil;
}
@end
rumConfig.viewTrackingHandler = CustomViewTracker()

class CustomViewTracker: NSObject, FTUIKitViewTrackingHandler {

    // SDK 기본 view 수집 규칙이 필요한 경우에만 해당 속성 유지
    lazy var defaultHandler: FTDefaultUIKitViewTrackingHandler = {
        FTDefaultUIKitViewTrackingHandler() 
    }()

    func rumView(for viewController: UIViewController) -> FTRUMView? {
        // 클래스명으로 정확히 매칭
        if viewController is HomeViewController {
            let properties: [String: Any] = ["page_type": "home"]
            return FTRUMView(viewName: "main_home", property: properties)
        }

        // 클래스명 프리픽스로 필터링
        let vcClassName = String(describing: type(of: viewController))
        if vcClassName.hasPrefix("FT") {
            let viewName = "ft_\(vcClassName)"
            return FTRUMView(viewName: viewName, property: nil)
        }

        // accessibilityLabel로 설정
        if let accessibilityLabel = viewController.view.accessibilityLabel, !accessibilityLabel.isEmpty {
            return FTRUMView(viewName: accessibilityLabel, property: nil)
        }

        // 일부 페이지를 커스텀한 후, 나머지 페이지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)
        return defaultHandler.rumView(for: viewController)

        // 추적을 건너뛰려면 nil을 반환
        return nil
    }
}

SwiftUI View 자동 수집 (실험적)

SwiftUI View 자동 수집은 먼저 SDK가 페이지 이름을 추출한 후, FTRumConfig.swiftUIViewTrackingHandler를 통해 RUM View를 생성할지 결정합니다. 기본 핸들러를 사용하여 직접 활성화하거나, FTSwiftUIViewTrackingHandler를 구현하여 추출된 SwiftUI View Name을 필터링하고 보고할 View Name과 속성을 커스텀할 수 있습니다.

특정 SwiftUI View에서 페이지 이름과 생명 주기를 명시적으로 제어하려면 SwiftUI View 수동 수집을 참조하세요.

참고: SwiftUI View 자동 수집은 현재 실험적 기능이며, 관련 API 및 수집 동작은 향후 버전에서 변경될 수 있습니다.

사용 전에 FTRumConfig.enableTraceUserView를 활성화하고 FTRumConfig.swiftUIViewTrackingHandler를 설정해야 합니다.

rumConfig.enableTraceUserView = YES;
rumConfig.swiftUIViewTrackingHandler = [FTDefaultSwiftUIViewTrackingHandler new];
rumConfig.enableTraceUserView = true
rumConfig.swiftUIViewTrackingHandler = FTDefaultSwiftUIViewTrackingHandler()

SwiftUI View 수집 규칙을 커스텀하려면 FTSwiftUIViewTrackingHandler를 구현합니다. FTRUMView를 반환하면 해당 SwiftUI View를 수집하고, nil을 반환하면 건너뜁니다.

rumConfig.swiftUIViewTrackingHandler = [CustomSwiftUIViewTracker new];

@interface CustomSwiftUIViewTracker : NSObject <FTSwiftUIViewTrackingHandler>
@end

@implementation CustomSwiftUIViewTracker
- (nullable FTRUMView *)rumViewForExtractedViewName:(NSString *)extractedViewName {
    if ([extractedViewName isEqualToString:@"HomeView"]) {
        return [[FTRUMView alloc] initWithViewName:@"main_home" property:@{@"page_type": @"home"}];
    }
    return nil;
}
@end
rumConfig.swiftUIViewTrackingHandler = CustomSwiftUIViewTracker()

class CustomSwiftUIViewTracker: NSObject, FTSwiftUIViewTrackingHandler {
    func rumView(forExtractedViewName extractedViewName: String) -> FTRUMView? {
        if extractedViewName == "HomeView" {
            return FTRUMView(
                viewName: "main_home",
                property: ["page_type": "home"]
            )
        }
        return nil
    }
}

Action

FTRUMConfig.enableTraceUserAction = YES 설정을 활성화해야 합니다.

rumConfig.actionTrackingHandler = [CustomActionTracker new];

#import "FTDefaultActionTrackingHandler.h"

// 프로토콜 구현 방식 예시
// iOS 환경에서는 `FTUIPressRUMActionsHandler` 프로토콜을 따라야 함
// tvOS 환경에서는 `FTUITouchRUMActionsHandler` 프로토콜을 따라야 함
@interface CustomActionTracker : NSObject <FTUIPressRUMActionsHandler,FTUITouchRUMActionsHandler>
// SDK 기본 Action 수집 규칙이 필요한 경우에만 추가
@property (nonatomic, strong) FTDefaultActionTrackingHandler defaultHandler;
@end
@implementation CustomActionTracker

// SDK 기본 Action 수집 규칙이 필요한 경우에만 추가
-(FTDefaultActionTrackingHandler *)defaultHandler{
    if (!_defaultHandler) {
        _defaultHandler = [FTDefaultActionTrackingHandler new];
    }
    return _defaultHandler;
}

// iOS와 tvOS 모두 구현해야 하는 프로토콜 메서드
- (nullable FTRUMAction *)rumLaunchActionWithLaunchType:(FTLaunchType)type {
    if(type == FTLaunchCold){
      return [[FTRUMAction alloc]initWithActionName:@"cold"];
    }
    // nil을 반환하여 추적 건너뜀
    return nil;
}

// iOS 환경에서 구현해야 하는 프로토콜 메서드
-(nullable FTRUMAction *)rumActionWithTargetView:(UIView *)targetView{
    if (view.accessibilityIdentifier){
       return [[FTRUMAction alloc] initWithActionName:view.accessibilityIdentifier];
    }

     // 일부 Action을 커스텀한 후, 나머지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)
    return [self.defaultHandler rumActionWithTargetView:targetView];

    // nil을 반환하여 추적 건너뜀
    return nil;
}
// tvOS 환경에서 구현해야 하는 프로토콜 메서드
- (nullable FTRUMAction *)rumActionWithPressType:(UIPressType)type targetView:(UIView *)targetView{
    if (type == UIPressTypeSelect && view.accessibilityIdentifier){
       return [[FTRUMAction alloc] initWithActionName:view.accessibilityIdentifier];
    }

    // 일부 Action을 커스텀한 후, 나머지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)
    return [self.defaultHandler rumActionWithPressType:type targetView:targetView];

        // nil을 반환하여 추적 건너뜀
    return nil;
}
@end
rumConfig.actionTrackingHandler = CustomActionTracker()

// 프로토콜 구현 방식 예시
// iOS 환경에서는 `FTUIPressRUMActionsHandler` 프로토콜을 따라야 함
// tvOS 환경에서는 `FTUITouchRUMActionsHandler` 프로토콜을 따라야 함
class CustomActionTracker: NSObject, FTUIPressRUMActionsHandler, FTUITouchRUMActionsHandler {

    // SDK 기본 Action 수집 규칙이 필요한 경우에만 추가
    lazy var defaultHandler: FTDefaultActionTrackingHandler = {
        FTDefaultActionTrackingHandler()
    }()

    // iOS와 tvOS 모두 구현해야 하는 프로토콜 메서드
    func rumLaunchAction(with type: FTLaunchType) -> FTRUMAction? {
        if type == .cold {
            return FTRUMAction(actionName: "cold")
        }
        // nil을 반환하여 추적 건너뜀
        return nil
    }

    // iOS 환경에서 구현해야 하는 프로토콜 메서드
    func rumAction(withTargetView targetView: UIView) -> FTRUMAction? {
        if let identifier = targetView.accessibilityIdentifier {
            return FTRUMAction(actionName: identifier)
        }

        // 일부 Action을 커스텀한 후, 나머지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)
        return defaultHandler.rumAction(withTargetView: targetView)

        // nil을 반환하여 추적 건너뜀
        return nil 
    }

    // tvOS 환경에서 구현해야 하는 프로토콜 메서드
    func rumAction(with pressType: UIPress.PressType, targetView: UIView) -> FTRUMAction? {
        if pressType == .select, let identifier = targetView.accessibilityIdentifier {
            return FTRUMAction(actionName: identifier)
        }

        // 일부 Action을 커스텀한 후, 나머지는 SDK 기본 수집 규칙 사용 (기본 핸들러의 처리 결과 반환)        
        return defaultHandler.rumAction(with: pressType, targetView: targetView)

        // nil을 반환하여 추적 건너뜀
        return nil
    }
}

Resource

FTRUMConfig.enableTraceUserResource = YES 설정을 활성화하거나 URLSession Delegate를 통한 Network 커스텀 수집이 필요합니다.

URL 기반 수집 필터링

rumConfig.resourceUrlHandler = ^(NSURL *url){
        // YES를 반환하면 수집하지 않음, NO를 반환하면 수집
        if ([url.host isEqualToString:@"example.com"]) {
            return YES;
        }
        return NO;
};
rumConfig.resourceUrlHandler = { url in 
     // true를 반환하면 수집하지 않음, false를 반환하면 수집
     return url.host == "example.com"
}

커스텀 속성 추가

속성 제공자 클로저를 설정하여 RUM Resource에 첨부할 추가 속성을 반환할 수 있습니다.

예를 들어, RUM Resource에 HTTP 요청 body를 추가하려면 다음과 같이 합니다.

rumConfig.resourcePropertyProvider = ^NSDictionary *_Nullable(NSURLRequest *request, NSURLResponse *response,NSData *data, NSError *error) {
     NSString *body = @"";
     if (request.HTTPBody) {
        body = [[NSString alloc] initWithData:httpBody encoding:NSUTF8StringEncoding] ?: @"";
     }
     return @{@"request_body": body};
 }
rumConfig.resourcePropertyProvider = { request, response, data, error in
   let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
   return ["request_body": body]
  }

네트워크 오류 필터링

네트워크 요청에 오류가 발생하면 RUM에 network_error 유형의 Error 데이터가 생성됩니다. task.cancel과 같은 일부 URLSession의 로컬 오류는 사용자 프로그램의 정상적인 로직이지 오류 데이터가 아니므로, sessionTaskErrorFilter 콜백을 통해 차단 및 필터링할 수 있습니다. 차단하려면 YES를 반환하고, 차단하지 않으려면 NO를 반환합니다. 차단하면 RUM-Error가 해당 오류를 수집하지 않습니다.

rumConfig.sessionTaskErrorFilter = ^BOOL(NSError * _Nonnull error){
    return error.code == NSURLErrorCancelled;
}; 
rumConfig.sessionTaskErrorFilter = { error in
   return (error as? URLError)?.code == .cancelled
}

Error

FTRumConfig.issueDataProvider를 통해 SDK가 Crash 또는 ANR을 자동 수집할 때, 해당 예외 정보를 기반으로 RUM Error에 비즈니스 필드(예: 비즈니스 시나리오, 기능 모듈, 실험 그룹)를 추가할 수 있습니다. 이 기능은 SDK 1.6.7 이상에서 지원됩니다.

issueDataProvider는 자동 수집된 Crash 및 ANR에만 필드를 추가하며, Crash 또는 ANR 모니터링을 활성화하지는 않습니다. 사용 전에 수집 대상에 따라 enableTrackAppCrash 또는 enableTrackAppANR을 활성화해야 합니다.

구성

RUM을 시작하기 전에 issueDataProvider를 설정하세요. RUM 시작 후 원래 FTRumConfig를 수정해도 실행 중인 Provider는 변경되지 않습니다.

FTRumConfig *rumConfig = [[FTRumConfig alloc] initWithAppid:appid];
rumConfig.enableTrackAppCrash = YES;
rumConfig.enableTrackAppANR = YES;

// RUM 시작 전에 읽기 전용 비즈니스 스냅샷을 준비하여 콜백에서 시간이 오래 걸리는 작업을 피함
NSDictionary<NSString *, id> *businessContext = @{
    @"business_scene": @"checkout",
    @"release_channel": @"app_store"
};

rumConfig.issueDataProvider = ^NSDictionary<NSString *, id> * _Nullable(FTIssueInfo *issue) {
    NSMutableDictionary<NSString *, id> *fields = [businessContext mutableCopy];
    fields[@"issue_category"] =
        issue.category == FTIssueCategoryCrash ? @"crash" : @"anr";
    fields[@"historical_issue"] = @(issue.isHistorical);

    if (issue.threadName.length > 0) {
        fields[@"issue_thread_name"] = issue.threadName;
    }
    return fields;
};

[[FTMobileAgent sharedInstance] startRumWithConfigOptions:rumConfig];
let rumConfig = FTRumConfig(appid: appid)
rumConfig.enableTrackAppCrash = true
rumConfig.enableTrackAppANR = true

// RUM 시작 전에 읽기 전용 비즈니스 스냅샷을 준비하여 콜백에서 시간이 오래 걸리는 작업을 피함
let businessContext: [String: Any] = [
    "business_scene": "checkout",
    "release_channel": "app_store"
]

rumConfig.issueDataProvider = { issue in
    var fields = businessContext
    fields["issue_category"] =
        issue.category == .crash ? "crash" : "anr"
    fields["historical_issue"] = issue.isHistorical

    if let threadName = issue.threadName, !threadName.isEmpty {
        fields["issue_thread_name"] = threadName
    }
    return fields
}

FTMobileAgent.sharedInstance().startRum(withConfigOptions: rumConfig)

Provider는 조건에 맞는 각 자동 수집 Error에 대해 동기적으로 한 번씩 호출됩니다. nil 또는 빈 딕셔너리를 반환하면 이번에 커스텀 필드를 추가하지 않습니다.

FTIssueInfo

콜백 매개변수 FTIssueInfo는 현재 처리 중인 Crash 또는 ANR을 설명하는 읽기 전용 객체입니다.

속성 타입 설명
category FTIssueCategory 예외 카테고리: FTIssueCategoryCrash / Swift .crash는 Crash, FTIssueCategoryANR / Swift .anr은 ANR
errorType NSString 해당 RUM Error의 유형. Crash는 ios_crash, ANR은 anr_error
message NSString Error 메시지, 가져올 수 없으면 nil
stack NSString 예외 스택
occurredAtNanoseconds long long 예외 발생 시간, Unix 타임스탬프(나노초)
appState NSString 해당 RUM Error에서 사용하는 앱 상태
threadName NSString 예외 스레드 이름, 가져올 수 없으면 nil
historical BOOL 지속화된 데이터로 복원된 예외인지 여부; Objective-C getter는 isHistorical, Swift는 isHistorical 사용

자동 수집 시나리오에서 category, errorType, historical의 대응 관계는 다음과 같습니다.

시나리오 category errorType historical
Crash 보고서가 앱 다음 시작 시 복원됨 Crash ios_crash YES
현재 프로세스 복원 후 ANR ANR anr_error NO
이전 프로세스의 Watchdog ANR이 앱 다음 시작 시 복원됨 ANR anr_error YES

historicalYES인 경우, FTIssueInfo의 예외 시간, 스택 및 앱 상태는 지속화된 예외 데이터에서 가져오지만, Provider는 앱이 복원되어 보고서를 처리할 때 실행됩니다. 따라서 Provider 실행 시 직접 읽는 비즈니스 상태는 현재 프로세스에 속하며, 예외 발생 시의 상태와 일치하지 않을 수 있습니다. 예외 발생 시의 비즈니스 정보를 연결하려면 미리 지속화하고 RUM 시작 전에 스레드 안전한 메모리 스냅샷으로 로드하세요. Provider 내에서 디스크 읽기를 수행하지 마세요.

커스텀 필드 규칙

Provider가 반환하는 필드는 해당 RUM Error의 fields에 기록되며, tags에는 기록되지 않습니다. 필드는 다음 규칙을 충족해야 합니다.

  • Key는 비어 있지 않은 문자열이어야 하며, UTF-8 인코딩 길이가 100바이트를 초과할 수 없습니다.
  • Value는 문자열, 부울, 정수 및 유한 부동 소수점 숫자만 지원합니다. 배열, 딕셔너리, NSNull 또는 커스텀 객체는 지원되지 않습니다.
  • 문자열 Value의 UTF-8 인코딩 길이는 4096바이트를 초과할 수 없습니다.
  • SDK는 반환된 딕셔너리의 처음 50개 항목까지만 스캔하며, 유효하지 않은 필드도 스캔 수에 포함됩니다. 딕셔너리 순회 순서는 고정되지 않으므로 50개 항목을 초과하지 않는 것이 좋습니다.
  • 허용된 모든 필드의 예상 총 크기는 25KiB를 초과할 수 없습니다.
  • error. 또는 error_로 시작하는 Key는 무시됩니다.
  • 커스텀 필드가 SDK의 기존 tag 또는 field와 이름이 같은 경우 SDK 필드가 우선합니다.

규칙에 맞지 않는 필드는 무시되며, 원래 Crash 또는 ANR Error의 수집 및 보고를 방해하지 않습니다. 커스텀 필드는 RUM 데이터와 함께 업로드되므로 비밀번호, 토큰 등 민감한 정보를 포함하지 마세요.

콜백 실행 요구 사항

issueDataProvider는 메인 스레드가 아닌 곳에서 동시에 또는 재진입하여 실행될 수 있습니다. 콜백은 다음 요구 사항을 충족해야 합니다.

  • 스레드 안전성을 보장하며, 준비된 불변 데이터 또는 메모리 스냅샷을 우선적으로 읽어야 합니다.
  • 10ms 이내에 반환하는 것이 좋습니다.
  • UI를 조작하지 말고, 네트워크 또는 디스크 I/O를 실행하지 마세요.
  • 스레드 전환, 동기 디스패치, 오래 기다리는 잠금 등 차단 작업을 수행하지 마세요.

실행 시간이 50ms를 초과하면 SDK에서 느린 콜백 디버그 로그를 출력할 수 있지만, 원래 RUM Error 처리는 계속 진행됩니다.

적용 범위

issueDataProvider는 SDK가 자동 수집한 Crash, 현재 프로세스 복원 후 ANR, 이전 프로세스의 Watchdog ANR에 적용됩니다. 다음 데이터는 이 Provider를 트리거하지 않습니다.

  • addError 등 인터페이스를 통해 수동으로 추가한 Error
  • Resource / Network Error
  • WebView Error
  • Long Task
  • 데이터 업로드 또는 재시도 과정

커스텀 TraceHeader

FTTraceConfig.traceInterceptor를 통해 전역 설정하거나, URLSession 레벨 커스텀 Trace를 할 수 있습니다. 아래는 w3c-traceContext 예시입니다.

FTTraceConfig *traceConfig = [[FTTraceConfig alloc]init];
   traceConfig.traceInterceptor = ^FTTraceContext * _Nullable(NSURLRequest *request) {
    // 1. 비즈니스 커스텀 traceId 가져오기
    NSString *replaceTrace = [request.allHTTPHeaderFields valueForKey:CUSTOM_TRACE_HEADER];

    // 2. SDK 표준 W3C traceparent 요청 헤더 가져오기
    NSDictionary *traceHeaders = [[FTExternalDataManager sharedManager] getTraceHeaderWithUrl:request.URL];
    NSString *traceParentStr = traceHeaders[FT_NETWORK_TRACEPARENT_KEY];

    // 3. W3C traceparent 형식 파싱 후 인덱스 1의 traceId 교체
    NSArray *traceComponents = [traceParentStr componentsSeparatedByString:@"-"];
    if (traceComponents.count != 4) {
        return nil;
    }
    NSMutableArray *newComponents = [traceComponents mutableCopy];
    newComponents[1] = replaceTrace;
    NSString *newTraceParent = [newComponents componentsJoinedByString:@"-"];

    // 4. 커스텀 추적 컨텍스트 조립 및 반환
    FTTraceContext *context = [FTTraceContext new];
    context.traceHeader = @{FT_NETWORK_TRACEPARENT_KEY:newTraceParent};
    context.traceId = replaceTrace;
    // SDK가 생성한 spanId 유지 (인덱스 2 고정 위치)
    context.spanId = newComponents[2];
    return context;
};
let traceConfig = FTTraceConfig()
traceConfig.traceInterceptor = { (request: URLRequest) -> FTTraceContext? in
        // 1. 비즈니스 커스텀 traceId 가져오기
        guard let replaceTrace = request.allHTTPHeaderFields?[CUSTOM_TRACE_HEADER] else {
            return nil
        }

        // 2. SDK 표준 W3C traceparent 요청 헤더 가져오기
        guard let traceHeaders = FTExternalDataManager.shared().getTraceHeader(with: request.url!), let traceParentStr = traceHeaders[FT_NETWORK_TRACEPARENT_KEY] as? String else {
            return nil
        }

        // 3. W3C traceparent 형식 파싱 후 인덱스 1의 traceId 교체
        let traceComponents = traceParentStr.components(separatedBy: "-")
        guard traceComponents.count == 4 else {
            return nil
        }
        var newComponents = traceComponents
        newComponents[1] = replaceTrace
        let newTraceParent = newComponents.joined(separator: "-")

        // 4. 커스텀 추적 컨텍스트 조립 및 반환
        let context = FTTraceContext()
        context.traceHeader = [FT_NETWORK_TRACEPARENT_KEY: newTraceParent]
        context.traceId = replaceTrace
        // SDK가 생성한 spanId 유지 (인덱스 2 고정 위치)
        context.spanId = newComponents[2]
        return context
    }

문서 평가

이 페이지가 도움이 되었나요?