crash-triage
크래시 목록을 훑는 것과 원인을 아는 것은 다르다. 이 스킬은 집계 → 개별 이벤트 → 코드 대조를 끝까지 밀어 "무엇이 몇 건"이 아니라 "왜 터지고 누가 고쳐야 하나"로 답을 만든다.
핵심 원칙:
- 건수 순위는 심각도 순위가 아니다. 1위가 사용자에게 무해한 잡음일 수 있고, 3건짜리가 강제 종료일 수 있다. 순위와 별개로 치명도·현재성을 따로 판정한다.
- 이미 등록된 이슈를 다시 올리지 않는다. 조사 결과의 절반은 "이건 이미 알고 있다"로 끝나는 게 정상이다. 중복 확인을 건너뛰면 저장소에 쓰레기 이슈가 쌓인다.
- 원인은 코드를 읽고 말한다. 스택만 보고 추정한 원인은 리포트에 쓰지 않는다. 읽지 못했으면 "미확인"이라고 적는다.
실행 절차
1. 앱 ID를 찾는다
Crashlytics 도구는 전부 appId를 인자로 요구한다. 서버 설정에 프로젝트가 박혀 있지 않으므로
매 호출에 넘겨야 한다. 프로젝트 안에서 찾는다:
find . -name 'google-services.json' -o -name 'GoogleService-Info.plist' | grep -v node_modules
- Android:
google-services.json→client[].client_info.mobilesdk_app_id - iOS:
GoogleService-Info.plist→GOOGLE_APP_ID
찾지 못하면 추측하지 말고 사용자에게 묻는다. 여러 앱이 나오면 어느 것을 볼지 확인하거나, 사용자가 범위를 안 정했으면 전부 순회하고 결과를 앱별로 나눠 보고한다.
2. 상위 이슈를 집계한다
crashlytics_get_report를 report: "topIssues"로 부른다. 기본은 최근 30일·상위 5건이고,
사용자가 기간이나 건수를 말했으면 그쪽을 쓴다. 조회 가능 범위는 최근 90일이며, 그보다 앞선
구간을 넣으면 거부된다.
집계만으로 판단이 안 서면 topVersions(특정 버전에 몰렸나) · topAndroidDevices ·
topOperatingSystems(기종·OS 편중인가)로 보조한다. 편중이 뚜렷하면 그 자체가 원인 단서다.
3. 이슈마다 실제 이벤트를 본다
crashlytics_list_events에 filter.issueId를 걸어 표본을 가져온다. 집계에는 없고 여기에만
있는 것들이 원인 규명의 핵심이다:
- 스택 트레이스 — 어디서 터졌나
- 커스텀 키 — 화면·로그인 상태·앱 버전 등 앱이 심어 둔 맥락
- breadcrumb 로그 — 터지기 직전 무슨 일이 있었나(네트워크 응답 등)
- 기기·OS·앱 버전·이벤트 시각
같은 이슈라도 표본을 2건 이상 보면 무엇이 매번 달라지는지가 드러난다. 이번 사례에서는 오류 메시지 안의 식별자 숫자가 매번 달라지는 것이 "요청마다 새로 발급되는 자원"이라는 결정적 단서였다. 하나만 보면 그 변주를 못 본다.
4. 코드와 대조한다
스택을 위에서 훑어 우리 코드의 첫 프레임을 찾고 그 파일을 실제로 읽는다. 거기서 호출 사슬을 거꾸로 따라가 "왜 그 상태가 됐나"까지 세운다. 그 지점의 주석이 지금도 사실인지 함께 본다 — 크래시는 주석에 적힌 가정이 틀렸다는 증거인 경우가 많다.
프레임이 외부 라이브러리뿐이면 우리가 못 고치는 결함일 수 있다. 그때는 라이브러리 버전을 확인해
알려진 수정본이 있는지, 우리 호출 방식이 그 결함을 유발하는지로 갈라 적는다. 난독화된 이름
(a.b.c 꼴)이 섞여 있으면 난독화 설정이 원인 후보다.
5. 이미 등록된 이슈인지 확인한다
오류 문구·기능 경로·API명·라이브러리명으로 검색어를 여러 개 만들어 전체 상태를 훑는다:
gh issue list --repo <owner/name> --search "<검색어>" --state all --limit 5 \
--json number,state,title -q '.[] | "\(.number) \(.state) \(.title)"'
검색어 하나로 안 잡혔다고 "없다"고 단정하지 않는다. 존재는 한 번만 걸려도 성립하지만 부재는 여러 각도로 훑어야 성립한다. 관련 있어 보이는 이슈가 나오면 본문을 읽어 같은 원인인지 확인한다 — 증상이 비슷해도 뿌리가 다르면 별개 이슈다. 반대로 증상이 달라도 뿌리가 같으면 묶는다.
6. 판정해서 표로 낸다
건마다 아래 셋 중 하나로 판정한다:
- 기존 이슈 — 같은 원인의 이슈가 이미 열려 있다. 번호를 적는다.
- 신규 후보 — 등록된 게 없고 우리가 고칠 수 있다.
- 보류 — 외부 라이브러리 결함이거나, 재현 조건을 모르거나, 고칠 가치가 낮다. 이유를 적는다.
출력 형식:
## 크래시 조사 — <기간> · <앱 구분>
### 1위 — <제목> (<치명도>, <건수>건 / <영향 사용자>명)
**원인**: <코드를 읽고 세운 사슬. 파일:줄 인용>
**판정**: 신규 후보 / 기존 이슈 #N / 보류(<이유>)
**근거**: <스택·커스텀 키·breadcrumb 중 결정적이었던 것>
(건마다 반복)
### 정리
| 순위 | 무엇 | 치명도 | 판정 | 다음 할 일 |
|---|---|---|---|---|
마지막에 신규 후보를 이슈로 올릴지 사용자에게 묻는다. 직접 만들지 않는다 — 원인 판정이 틀렸을 때 잘못된 이슈가 저장소에 남는 비용이 크고, 무엇을 올릴지는 팀 사정을 아는 사람이 정한다.
행동 원칙
- 화면 이름은 예외가 기록된 시점의 것이다. 지연 발화하는 오류(타이머·재시도·비동기 뒷정리)는 요청이 시작된 화면이 아니라 몇 초 뒤 사용자가 머문 화면으로 찍힌다. 화면 이름을 재현 경로로 단정하기 전에 이벤트 시각과 코드 흐름으로 교차 확인한다.
- 한 프로젝트의 여러 앱이 한 곳으로 섞여 들어올 수 있다. 폰과 워치·태블릿이 패키지를 공유하면 크래시가 같은 앱 ID로 올라온다. 스택의 패키지·Activity 이름으로 갈라 보고하지 않으면 엉뚱한 코드베이스를 뒤지게 된다.
- "처음 본 버전"과 "마지막에 본 버전"만 보고 옛날 버그로 넘기지 않는다. 버전 표기는 배포 주기에 따라 굳어 있을 수 있다. 현재성은 이벤트 시각으로 판단한다.
- 앱을 죽이지 않는 예외를 자동으로 낮게 보지 않는다. 대부분의 크래시 리포터는 세션당 업로드 건수에 상한이 있어, 잡음이 상한을 먹으면 같은 세션의 진짜 예외가 통째로 올라오지 않는다. 가시성을 막는 잡음은 그 자체로 고칠 값어치가 있다.
- 읽기 전용이다. 이 스킬이 쓰는 도구에는 이슈 상태 변경이나 노트 작성이 없다. 대시보드에서 무언가를 바꿔야 하면 사용자에게 알리고 콘솔에서 하게 한다.
- 도구가 붙어 있지 않으면(조회가 전부 실패하면) 원인을 추측해 보고서를 만들지 말고, 플러그인이 이 프로젝트에 설치돼 있는지 확인하라고 알린 뒤 멈춘다.