계측
React Native 앱이 코드 없이 수집하는 것(네이티브 크래시, ANR, 네트워크, 생명주기), initialize가 JavaScript 에러와 Promise 거부에 대해 더하는 것, 추가 패키지별로 기록되는 것, Android와 iOS의 차이를 설명합니다.
React Native 앱의 텔레메트리는 세 계층에서 나옵니다. 네이티브 SDK는 운영체제가 볼 수 있는 것을 수집합니다. initialize는 JavaScript만 볼 수 있는 것, 즉 예외와 Promise 거부를 더합니다. 선택 패키지는 코드만 아는 것, 즉 화면, Redux 액션, 커스텀 작업의 스팬을 더합니다.
한눈에 보기
| 출처 | 기록하는 것 | 필요한 것 |
|---|---|---|
| 네이티브 SDK | 크래시, ANR, 네트워크 요청, 앱 시작, 세션, 네이티브 화면, 탭, WebView 로드, 저전력과 메모리 | 네이티브 설정만 |
initialize | 처리되지 않은 JS 예외, React 컴포넌트 스택, 처리되지 않은 Promise 거부(선택), React Native와 SDK 버전 | initialize 한 번 호출 |
| 직접 호출 | 로그, 처리한 에러, 브레드크럼, 세션 속성, 사용자 식별자, 수동 기록 요청 | API |
@sophonz/react-native-tracer-provider | OpenTelemetry API로 만든 스팬 | OpenTelemetry |
@sophonz/react-native-navigation | 화면마다 ux.view 스팬 하나 | 내비게이션 |
@sophonz/react-native-redux | 디스패치된 액션마다 sys.rn_action 스팬 하나 | Redux |
@sophonz/react-native-otlp | 새로 기록하는 것은 없고, 같은 텔레메트리를 다른 백엔드로도 보냅니다 | OTLP |
네이티브 SDK가 수집하는 것
SDK가 시작되면 JavaScript가 무엇을 호출하든 동작합니다. 각 항목이 붙이는 spz.type과 끄는 방법까지 담은 전체 목록은 Android 계측 항목과 iOS 계측 항목에 있습니다.
| 신호 | Android | iOS |
|---|---|---|
| 크래시 | JVM 크래시(sys.android.crash), Hermes를 포함한 네이티브 크래시(sys.android.native_crash), Android 11 이상의 앱 종료 정보 | KSCrash 기반 리포터(sys.ios.crash), MetricKit 크래시 보고서 |
| 앱 응답 없음 | ANR 감지(perf.thread_blockage) | 행 감지는 기본으로 꺼져 있고 네이티브 코드에서만 켤 수 있습니다 |
| 네트워크 | React Native의 fetch와 XMLHttpRequest가 쓰는 OkHttp를 빌드 시점에 계측 | React Native 네트워킹이 쓰는 URLSession |
| 앱 시작 | 앱 시작 트레이스 | sys.startup 스팬 |
| 화면 | Activity(ux.view) | UIViewController(ux.view, perf.ui_load) |
| 탭, WebView | 네이티브 앱과 같음. 플랫폼 문서 참고 | 화면 탭, WebView 로드 |
| 기기 상태 | 절전 모드, 연결 상태 변화 | 저전력 모드, 메모리 경고 |
네이티브 코드의 크래시는 JavaScript 로드 전이라도 수집되지만, SDK를 네이티브(MainApplication, AppDelegate)에서 시작했을 때만 그렇습니다. initialize로 시작하면 SDK는 JavaScript 번들이 실행된 뒤의 상황만 봅니다.
React Native 앱의 네이티브 화면
네이티브 화면 수집은 JavaScript 라우트가 아니라 운영체제가 아는 컨테이너를 기록합니다. Android에서 React Native 앱은 보통 MainActivity 하나뿐이라, 화면 타임라인에는 세션 내내 화면 하나만 보입니다. iOS에서는 내비게이션 라이브러리가 만든 뷰 컨트롤러가 보입니다. 라우트 이름을 보려면 @sophonz/react-native-navigation을 추가하고, 두 기록이 섞이지 않도록 네이티브 수집을 끄는 것도 고려하세요.
- Android:
sdk_config안에"view_config": {"enable_automatic_activity_capture": false} - iOS:
sdkConfig의ios.disableAutomaticViewCapture: true, 또는 네이티브 코드에서ViewCaptureService제거
세션과 생명주기
세션은 네이티브 SDK가 관리합니다. 앱이 백그라운드로 가면 세션이 끝나고, 돌아오면 새 세션이 시작되므로 JavaScript 앱도 네이티브 앱과 같은 세션 경계를 갖습니다. getCurrentSessionId()는 현재 세션 ID를 돌려주고, endSession()은 경계를 강제로 만듭니다.
iOS는 타이머가 아니라 세션 파트가 끝날 때(백그라운드 전환이나 다음 실행) 텔레메트리를 업로드합니다.
JavaScript 에러
initialize는 React Native의 전역 핸들러(ErrorUtils.setGlobalHandler)를 감쌉니다. 잡히지 않은 예외마다 다음을 수행합니다.
- 에러에 React 컴포넌트 스택이 있으면 별도의 ERROR 로그로 남깁니다
- 스택 트레이스의 앞 200줄만 남깁니다
- 예외를 네이티브 SDK로 보냅니다
- 150ms 뒤에 원래 핸들러를 호출하므로, 개발 중의 레드 박스나 릴리스에서 치명적 에러가 일으키는 크래시 같은 React Native 본래 동작은 그대로입니다
| Android | iOS | |
|---|---|---|
| 기록 형태 | React Native 크래시 타입의 로그 | ERROR 로그 |
| 속성 | JS 스택 트레이스, 예외 이름, 메시지, 타입 | exception.type, exception.message, exception.id, spz.exception_handling: unhandled, spz.ios.react_native_crash.js_exception의 JS 스택 |
iOS의 형태는 Apple SDK의 공개 API에서 비롯됩니다. SDK 밖에서는 타입이 지정된 크래시 로그를 기록할 수 없기 때문입니다. 두 플랫폼이 섞인 대시보드에서는 client.platform으로 거르세요.
치명적 에러로 릴리스 빌드가 크래시하면 네이티브 크래시 리포터가 그 크래시도 기록합니다. iOS에서는 크래시 보고서의 크래시 정보 키 spz-js에 로그의 exception.id와 같은 값이 들어 있어 둘을 짝지을 수 있습니다.
CAUTION — Error 객체를 던지세요
instanceof Error인 값만 처리합니다. throw "text" 같은 다른 값이면 핸들러는 [Sophonz] error must be of type Error를 출력하고 기록 없이 돌아가며, 원래 핸들러도 호출하지 않습니다. 그래서 React Native의 레드 박스나 크래시도 일어나지 않습니다.
처리되지 않은 Promise 거부
기본으로 꺼져 있습니다. trackUnhandledRejections로 켭니다.
await initialize({sdkConfig: {trackUnhandledRejections: true}});Hermes가 자체 Promise 구현을 제공하면 그 구현에, 아니면 React Native의 promise polyfill에 추적기를 붙입니다. 처리되지 않은 거부는 Unhandled promise rejection: <message> 메시지의 ERROR 로그가 되고, 거부 값이 Error면 스택 트레이스도 붙습니다. 나중에 Promise가 처리되더라도 이미 보낸 로그는 남습니다.
이 옵션은 SDK를 네이티브에서 시작했을 때도 적용됩니다.
에러 경계
React는 에러 경계가 잡은 에러를 전역 핸들러로 보내지 않습니다. 직접 기록하세요. logIfComponentError는 컴포넌트 스택을, logHandledError는 JS 스택을 기록합니다.
import React from "react";
import {logHandledError, logIfComponentError} from "@sophonz/react-native";
class ErrorBoundary extends React.Component<{children: React.ReactNode}, {failed: boolean}> {
state = {failed: false};
static getDerivedStateFromError() {
return {failed: true};
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
logHandledError(error, {boundary: "root"});
logIfComponentError(Object.assign(error, {componentStack: info.componentStack}));
}
render() {
return this.state.failed ? null : this.props.children;
}
}처리한 에러와 로그
나머지는 명시적으로 기록합니다. 잡은 예외는 logHandledError, 로그는 logInfo, logWarning, logError, logMessage, 가벼운 타임라인 이벤트는 addBreadcrumb입니다. 경고와 에러 로그에는 includeStacktrace = false를 넘기지 않는 한 호출 위치의 JavaScript 스택이 spz.stacktrace.rn으로 붙습니다. 자세한 내용은 API 레퍼런스에 있습니다.
버전과 번들
initialize는 React Native 버전, @sophonz/react-native 버전, 그리고 넘겼다면 patch를 기록합니다. iOS 릴리스 빌드에서는 main.jsbundle을 해시해 번들 식별자도 기록하므로, OTA 업데이트 뒤에도 에러가 어느 번들에서 났는지 알 수 있습니다.
Android에서는 이 값들이 SDK의 React Native 인터페이스로 전달되어 리소스 속성으로 보고됩니다. iOS에서는 hosted_platform_version, hosted_sdk_version, javascript_patch_number, react_native_bundle_id라는 프로세스 수명 속성이므로 세션 속성과 함께 보입니다.
화면 방향
useOrientationListener는 시작 방향과, 세로와 가로 사이의 변경마다 브레드크럼을 남깁니다.
import {useOrientationListener, useSophonz} from "@sophonz/react-native";
const {isStarted} = useSophonz(SOPHONZ_CONFIG);
useOrientationListener(isStarted);WebView
WebView 안에서 Sophonz 브라우저 SDK를 실행하는 웹 페이지는 앱의 세션에 합류할 수 있습니다. getCurrentSessionId()는 전송되는 형태 그대로인 소문자 16진수 32자를 돌려주므로 페이지에 그대로 넘기면 됩니다. 페이지가 이 값을 읽는 방법은 WebView 연동에 있습니다.
플랫폼 차이
| 동작 | Android | iOS |
|---|---|---|
| JavaScript에서의 설정 | 없음. sophonz-config.json만 | sdkConfig.ios |
| 처리되지 않은 JS 예외 | React Native 크래시 타입의 로그 | exception.*가 붙은 ERROR 로그 |
| 행 또는 ANR 감지 | 켜짐 | 꺼짐. 네이티브에서만 켤 수 있음 |
수집한 요청의 traceparent | 꺼짐. enable_traceparent_injection으로 켬 | disableNetworkSpanForwarding이 아니면 모든 호스트에 켜짐 |
| URL 제외 | networking.disabled_url_patterns, 정규식 | disabledUrlPatterns, 부분 문자열 |
| React Native 버전, JS 패치, 번들 ID | 리소스 속성 | 프로세스 수명 속성 |