릴리스 빌드

Flutter 릴리스 빌드의 스택 트레이스를 읽을 수 있게 만드는 방법 — Android의 R8 매핑 파일, iOS의 dSYM, --obfuscate와 --split-debug-info를 쓸 때 Dart 스택 트레이스에 일어나는 일 — 을 설명합니다.

Flutter 릴리스 빌드에는 두 종류의 코드가 있고, 스택 트레이스가 복원되는 방식도 다릅니다. Java, Kotlin, Objective-C, Swift 코드는 업로드한 파일로 Sophonz가 복원합니다. Android는 R8 매핑 파일, iOS는 dSYM입니다. Dart 코드는 스택 트레이스를 텍스트로 직접 보고하고 Sophonz는 받은 그대로 보여 줍니다. 그 텍스트를 읽을 수 있는지는 빌드 플래그에 달려 있습니다.

무엇이 어디서 복원되나

스택 트레이스보내는 곳복원
Android JVM 크래시·예외(플러그인, MainApplication, Android SDK)Android SDK빌드의 mapping.txt로 Sophonz가 복원
iOS 크래시(Swift, Objective-C, 플러그인, Flutter 엔진)Apple SDK빌드의 dSYM으로 Sophonz가 복원
Dart 오류(error 로그의 exception.stacktrace)Dart 런타임Sophonz가 복원하지 않습니다. --split-debug-info나 --obfuscate 없이 빌드하면 그대로 읽을 수 있습니다

Sophonz는 Dart 심볼 파일과 Android 네이티브 .so 심볼을 처리하지 않습니다.

Dart 스택 트레이스

기본 릴리스 빌드

추가 플래그 없이 flutter build apk, flutter build appbundle, flutter build ipa로 빌드하면 AOT 스냅샷에 Dart 함수 이름이 남습니다. Dart 오류는 #0 CheckoutRepository.submit (package:shop/checkout/repository.dart:42)처럼 읽을 수 있는 프레임으로 도착하며, 업로드할 것은 없습니다.

--split-debug-info

이 플래그는 Dart 디버그 정보를 앱에서 빼내 빌드 머신의 디렉터리에 두어 앱 크기를 줄입니다. 이렇게 빌드한 앱의 스택 트레이스에는 이름 대신 주소가 담깁니다.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
pid: 12345, tid: 12400, name 1.ui
os: android arch: arm64 comp: yes sim: no
build_id: '4f5a2...'
isolate_dso_base: 7a1c2e3000, vm_dso_base: 7a1c2e3000
#00 abs 0000007a1c4f21b3 virt 00000000002b31b3 _kDartIsolateSnapshotInstructions+0x1c21b3

Sophonz는 이 텍스트를 그대로 저장하고 보여 줍니다. 읽으려면 배포한 릴리스마다 심볼 디렉터리를 보관해 두고 flutter symbolize로 로컬에서 복원합니다.

flutter build appbundle --release --split-debug-info=build/symbols/1.4.0+12
 
# 나중에 exception.stacktrace 값을 헤더 줄까지 포함해 trace.txt로 저장한 뒤
flutter symbolize -i trace.txt -d build/symbols/1.4.0+12/app.android-arm64.symbols

트레이스의 아키텍처에 맞는 심볼 파일(app.android-arm64.symbols, app.ios-arm64.symbols 등)을 쓰세요. build_id 줄이 트레이스를 한 빌드에 묶으며, 다른 빌드의 심볼로는 틀린 결과가 나옵니다.

--obfuscate

--obfuscate는 Dart 클래스와 함수 이름을 바꾸며 --split-debug-info와 함께 써야 합니다. 주소는 위와 같이 flutter symbolize로 복원합니다. exception.type처럼 메시지에 나타나는 난독화된 이름을 되돌리려면 빌드할 때 난독화 맵을 저장합니다.

flutter build ipa --release \
  --obfuscate \
  --split-debug-info=build/symbols/1.4.0+12 \
  --extra-gen-snapshot-options=--save-obfuscation-map=build/symbols/1.4.0+12/obfuscation.json

맵은 원래 이름과 난독화된 이름이 번갈아 나오는 평평한 JSON 배열입니다.

CAUTION — 난독화된 타입 이름이 데이터에 남습니다

--obfuscate를 쓰면 exception.type에 난독화된 클래스 이름이 들어가므로, 두 빌드에서 난 같은 타입의 오류가 하나로 묶이지 않습니다. 오류 타입별 묶음이 난독화보다 중요하다면 --obfuscate 없이 빌드하세요.

심볼 디렉터리와 맵은 이미 배포한 빌드에 대해 다시 만들 수 없으므로, CI의 빌드 아티팩트로 버전과 빌드 번호를 붙여 보관하세요.

Android: R8 매핑 파일

Flutter 릴리스 빌드는 MainApplication, 플러그인, Sophonz Android SDK 같은 Java·Kotlin 코드에 R8을 실행합니다. 이 코드의 매핑은 다음 위치에 생깁니다.

build/app/outputs/mapping/release/mapping.txt

Flutter는 Android 빌드 출력을 프로젝트 최상위 build/로 옮기므로 경로가 android/app/build/ 아래가 아닙니다.

Gradle 플러그인으로 업로드

설치에서 적용한 Sophonz Gradle 플러그인이 R8이 끝나면 매핑을 업로드합니다. 업로드 호스트를 지정합니다.

android/gradle.properties
sophonz.baseUrl=https://app.sophonz.ai

자격 증명은 서비스 키가 있는 같은 sophonz-config.json에 넣습니다.

android/app/src/main/sophonz-config.json
{
  "app_id": "sophz",
  "api_token": "sophonz_pat_...",
  "sdk_config": {
    "app_framework": "flutter",
    "ingest": { "service_key": "sk_replace_me", "service_namespace": "my-project" }
  }
}

CI에서는 파일에서 api_token을 빼고 SOPHONZ_API_TOKEN(과 SOPHONZ_APP_ID)을 flutter build의 환경 변수로 넘깁니다.

- name: Build Android release
  env:
    SOPHONZ_API_TOKEN: ${{ secrets.SOPHONZ_TOKEN }}
    SOPHONZ_APP_ID: sophz
  run: flutter build appbundle --release

플러그인은 빌드 ID를 만들어 앱에 넣고 같은 값으로 매핑을 올리므로, 스택 트레이스와 매핑이 빌드 단위로 연결됩니다. 토큰, app_id, 플러그인 속성은 Android 매핑 파일 업로드에 있습니다.

CLI로 업로드

플러그인 업로드를 쓰지 않으면 빌드 후에 파일을 보냅니다. Android 앱 버전은 versionName이며, Flutter는 pubspec.yaml의 version에서 + 앞부분을 씁니다.

npx @sophonz/cli upload-mapping \
  --file build/app/outputs/mapping/release/mapping.txt \
  --version 1.4.0

iOS: dSYM

flutter build ipa는 앱을 build/ios/archive/Runner.xcarchive로 아카이브합니다. 그 안의 dSYMs 폴더에 앱과 포함된 프레임워크의 dSYM이 있으므로 폴더 전체를 올립니다.

flutter build ipa --release
 
PLIST=build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Info.plist
npx @sophonz/cli upload-dsym \
  --file build/ios/archive/Runner.xcarchive/dSYMs \
  --version "$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$PLIST")" \
  --build-id "$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$PLIST")"

CFBundleShortVersionString과 CFBundleVersion은 pubspec.yaml의 version(1.4.0+12)의 앞뒤 부분입니다. dSYM은 이미지 UUID로 매칭되므로 버전은 목록 정리에만 쓰입니다. 토큰, Build Phase 업로드, API는 iOS dSYM 업로드에 있습니다.

릴리스 체크리스트

  1. --split-debug-info와 --obfuscate 사용 여부를 정합니다. 쓴다면 빌드마다 심볼 디렉터리와 난독화 맵을 보관합니다.
  2. Android: Gradle 플러그인 업로드를 설정해 빌드하거나, 빌드 후 upload-mapping을 실행합니다.
  3. iOS: 아카이브의 dSYMs 폴더로 upload-dsym을 실행합니다.
  4. 빌드가 텔레메트리를 보내면 Settings > project > app > Mapping files에서 새 버전을 확인합니다.

다음 단계