계측

코드를 더 쓰지 않아도 Flutter 앱이 보고하는 것 — Flutter 계층의 Dart 오류, 프레임, 아이솔레이트 멈춤, 시작과 수명 주기, 네이티브 SDK의 크래시, ANR, 세션 — 과 Android·iOS의 차이를 정리합니다.

Flutter 앱에서는 두 계층이 데이터를 수집합니다. Dart 계층은 오류, 프레임, UI 아이솔레이트, 라우트, 감싼 HTTP 클라이언트처럼 Dart에서 일어나는 일을 봅니다. 네이티브 SDK는 세션, 네이티브 크래시, ANR, 앱 시작, 네이티브 네트워크 호출처럼 프로세스 수준의 일을 봅니다. Dart 계층은 직접 업로드하지 않고 모든 기록을 메서드 채널로 네이티브 SDK에 넘기며, 저장과 전송은 네이티브 SDK가 맡습니다.

Dart 계층이 수집하는 것

아래 항목은 모두 Sophonz.instance.start()가 실행되면 시작됩니다.

신호기록 형태필요한 것
처리되지 않은 Flutter 프레임워크 오류error 로그, spz.exception_handling: unhandledstart(action: ...) 또는 installErrorHandlers
처리되지 않은 비동기·플랫폼 오류error 로그, spz.exception_handling: unhandledstart(action: ...) 또는 installErrorHandlers
첫 프레임까지의 시간spz-time-to-first-frame-flutter 스팬start(action: ...)
상호작용 가능까지의 시간spz-time-to-interactive-flutter 스팬, 프로세스당 한 번내비게이션 옵저버
느린 프레임info 로그 slow-frames, 묶어서 기록없음
멈춘 프레임warning 로그 frozen-frame없음
UI 아이솔레이트 멈춤spz-dart-isolate-hang 스팬없음
백그라운드 시간spz-app-background 스팬없음
라우트뷰 스팬, 화면 로드 스팬SophonzNavigationObserver 또는 SophonzGoRouterObserver
HTTP 요청네트워크 스팬SophonzHttpClient 또는 SophonzInterceptor

라우트는 내비게이션, HTTP는 네트워크에서 설명합니다.

오류

start(action: ...)을 쓰면 핸들러 두 개가 설치됩니다.

  • FlutterError.onError — 프레임워크가 보고하는 오류입니다. RenderFlex 오버플로 같은 빌드·레이아웃·페인트 오류가 여기에 해당합니다. Sophonz가 기록한 뒤 기존 핸들러도 실행되므로 디버그 콘솔 출력은 그대로입니다.
  • PlatformDispatcher.instance.onError — 아무도 await하지 않은 async 콜백에서 던진 예외처럼 어디에서도 잡히지 않은 오류입니다. 핸들러가 false를 돌려주므로 기본 처리도 그대로 이어집니다.

오류마다 다음 속성을 담은 error 수준 로그가 기록됩니다.

속성값
exception.typeStateError 같은 런타임 타입
exception.message오류의 toString() 또는 프레임워크가 준 요약
exception.stacktraceDart 스택 트레이스
exception.context, exception.library프레임워크 오류의 FlutterErrorDetails context와 library
spz.exception_handlingunhandled, logHandledDartError로 기록하면 handled

Android에서는 로그 본문이 Dart error이고 메시지는 exception.message에 들어갑니다. iOS에서는 본문이 메시지 자체입니다.

직접 잡은 오류는 기록하지 않으면 남지 않습니다.

try {
  await repository.sync();
} catch (error, stack) {
  Sophonz.instance.logHandledDartError(error, stack);
}

처리된 오류로 기록한 오류는 오류 없는 세션 비율 계산에 포함되지 않습니다.

CAUTION — Dart 오류는 크래시가 아닙니다

Flutter는 처리되지 않은 Dart 예외 뒤에도 계속 실행되므로 이 오류들은 크래시가 아니라 error 로그이며, getLastRunEndState()도 cleanExit로 남습니다. 크래시는 네이티브 SDK가 수집하는 처리되지 않은 JVM·Objective-C 예외나 네이티브 시그널입니다.

아이솔레이트

핸들러는 루트 아이솔레이트에만 설치됩니다. 직접 띄운 아이솔레이트의 예외는 수집되지 않습니다. 단, compute는 오류를 호출한 아이솔레이트에서 다시 던지므로 거기서 잡지 않으면 수집됩니다.

Isolate.spawn은 onError로 오류를 루트 아이솔레이트에 넘기고 거기서 기록합니다.

final errors = ReceivePort();
errors.listen((message) {
  final pair = message as List<dynamic>;
  Sophonz.instance.logDartError(
    pair[0] as Object,
    StackTrace.fromString(pair[1] as String),
  );
});
 
await Isolate.spawn(parseLargeFile, path, onError: errors.sendPort);

start()는 루트 아이솔레이트에서만 실행되므로, 띄운 아이솔레이트 안에서 한 Sophonz.instance 호출은 버려집니다.

시작

스팬시작끝
spz-time-to-first-frame-flutterDart에서 start()가 시작될 때첫 프레임이 래스터화될 때
spz-time-to-interactive-flutter같은 시작 시각첫 라우트가 push된 다음 프레임. route 속성 포함

둘 다 Dart 기준으로 잽니다. 프로세스 시작이나 엔진 초기화처럼 Dart 이전의 시간은 네이티브 시작 트레이스에 있으며, 이 트레이스는 Android의 Application.onCreate()나 iOS의 AppDelegate 초기화 메서드에서 네이티브 SDK를 시작해야 기록됩니다.

프레임

프레임 타이밍은 SchedulerBinding.addTimingsCallback에서 받습니다. 빌드와 래스터 시간 중 긴 쪽으로 판단합니다.

경우기준기록
느린 프레임16ms 초과개수를 셉니다. 느린 프레임 60개마다 count, worst_build_ms, worst_raster_ms를 담은 info 로그 slow-frames 하나
멈춘 프레임700ms 이상프레임마다 build_ms, raster_ms를 담은 warning 로그 frozen-frame 하나

SophonzNavigationObserver를 쓰면 둘 다 현재 라우트를 route 속성으로 담습니다. 60개를 채우지 못한 느린 프레임 묶음은 앱이 끝날 때 기록되지 않습니다.

UI 아이솔레이트 멈춤

Flutter의 UI는 플랫폼 메인 스레드가 아니라 Dart 아이솔레이트에서 돌기 때문에, Android ANR 감지와 iOS 멈춤 감지는 Dart 이벤트 루프가 멈춘 것을 보지 못합니다. SDK는 UI 아이솔레이트에서 100ms마다 타이머를 돌리고, 이전 틱보다 700ms 이상 늦게 도착한 틱이 있으면 그 간격을 duration_ms 속성이 붙은 완료된 spz-dart-isolate-hang 스팬으로 기록합니다.

앱이 백그라운드에 있는 동안은 감시를 멈추고 다시 포그라운드로 오면 새로 시작하므로, 일시 정지된 프로세스가 멈춤으로 보고되지 않습니다.

수명 주기

앱이 백그라운드로 가면(AppLifecycleState.paused) spz-app-background 스팬이 시작되고, resumed나 detached에서 끝납니다.

네이티브 SDK가 수집하는 것

아래 항목은 Dart와 무관하며 네이티브 SDK에서 설정합니다.

신호AndroidiOS
세션예예
네이티브 크래시(JVM, NDK / Mach, 시그널)예예
ANR / 메인 스레드 멈춤ANR 기본 켜짐멈춤 수집 기본 꺼짐
앱 시작 트레이스Application.onCreate()에서 시작했을 때AppDelegate 초기화 메서드에서 시작했을 때
네이티브 코드의 네트워크 호출(OkHttp, HttpURLConnection / URLSession)예예
기기·앱 리소스예예
푸시 알림FCM 계측을 켰을 때PushNotificationCaptureService를 추가했을 때
네이티브 뷰의 탭과 화면예예

세부 항목과 스위치는 Android 계측과 iOS 계측에 있습니다.

NOTE — Dart HTTP는 네이티브 네트워크 수집에 보이지 않습니다

dart:io는 자체 소켓을 쓰므로 http나 Dio의 요청은 OkHttp나 URLSession을 거치지 않고, Dart 래퍼로만 기록됩니다. cupertino_http처럼 URLSession에 위임하는 클라이언트는 Apple SDK에도 보여 같은 요청이 두 번 기록될 수 있습니다.

네이티브의 탭과 화면은 호스트 액티비티나 뷰 컨트롤러를 기준으로 하며, Flutter 앱에서는 FlutterActivity나 FlutterViewController 하나입니다. Flutter 안의 화면 이름은 내비게이션 옵저버가 기록합니다.

직접 호출해야 하는 것

원하는 것호출
잡은 오류logHandledDartError(error, stack)
띄운 아이솔레이트의 오류루트로 넘겨 logDartError
직접 작성한 작업의 시간startSpan / stop 또는 recordCompletedSpan
타임라인의 메시지나 이벤트logInfo, logWarning, logError, addBreadcrumb
사용자 정보setUserIdentifier, addUserPersona
세션 맥락addSessionProperty
라우트가 아닌 화면(탭, 바텀 시트)startView / endView
래퍼 밖에서 보낸 요청recordNetworkRequest
Dart에서 처리한 푸시 알림logPushNotification

모두 API 레퍼런스에 있습니다.

플랫폼별 차이

동작AndroidiOS
플러그인의 네이티브 시작앱이 시작하지 않았으면 플러그인이 시작(시작 시간 측정 없음)하지 않습니다. AppDelegate에서 시작하지 않으면 모든 호출이 무시됩니다
업로드 시점Android SDK의 전송 방식을 따름세션 구간이 끝날 때(백그라운드 전환, 다음 실행)
setUserName, setUserEmail기록받기만 하고 무시
페르소나Android SDK가 유지프로세스 수명. 다시 실행하면 다시 설정해야 합니다
endSession즉시5초에 한 번까지
addSpanExporter, addLogRecordExporterstart() 때 전달무시. 시작 옵션의 otel: 사용
Flutter 라우트와 app.screen.name—바뀌지 않음. 다른 스팬에는 Flutter 뷰 컨트롤러 이름이 남습니다
네트워크 스팬 URL받은 그대로쿼리 문자열과 프래그먼트 제거
GET, POST, PUT, DELETE, PATCH 외 HTTP 메서드기록되지 않음OTHER로 기록
푸시 알림 필드from, messageId, priority, hasNotification, hasDatatitle, body(둘 다 필수), subtitle, badge, category
Flutter SDK·Dart 버전연결할 때 Android SDK에 전달프로세스 속성 spz_flutter_sdk_version, spz_dart_runtime_version

CAUTION — iOS에서 포그라운드 디버그 실행 중에는 아직 아무것도 보이지 않습니다

Apple SDK는 스팬과 로그를 디스크에 쓰고 세션 구간이 끝날 때 업로드합니다. 데이터를 찾기 전에 앱을 백그라운드로 보내거나 다시 실행하세요.

WebView

네이티브 SDK는 브리지로 WebView 안의 페이지와 세션을 공유할 수 있습니다(WebView 연동). 0.1.0에서는 이 브리지가 webview_flutter에 연결되어 있지 않습니다. 플러그인이 플랫폼 WebView 인스턴스를 받지 못하기 때문이며, Flutter WebView 안의 페이지는 Browser SDK에서 별도 세션으로 시작합니다.

수집 확인

package:sophonz/sophonz_samples.dart에는 빌드를 시험하는 트리거가 있습니다. 디버그나 내부 빌드에서만 사용하세요.

import 'package:sophonz/sophonz_samples.dart';
 
SophonzSamples.triggerUncaughtException();   // Dart 오류 로그
SophonzSamples.triggerUncaughtExceptionAsync();
SophonzSamples.triggerNativeSdkError();      // 두 플랫폼 모두 네이티브 크래시
SophonzSamples.triggerAnr();                 // Android 전용: 메인 스레드를 10초 막음
SophonzSamples.triggerRaisedSignal();        // iOS 전용: SIGABRT

네이티브 크래시 뒤에는 앱을 다시 실행하세요. 크래시는 다음 실행에서 업로드됩니다.

다음 단계