웹 게임 클라이언트 성능 모니터링을 Sentry Metrics와 Issue로 나눠 설계하기
기존에는 게임이 멈추거나 스크립트 오류가 날 때만 알림이 왔다. 화면이 끊기거나 서버 응답이 느린 경우는 잡히지 않았고, 알림이 와도 어느 운영사·기기에서 생겼는지 보이지 않아 원인을 찾는 데 오래 걸렸다. 평소 성능이 어느 수준인지 알 방법도 없었다.
측정값을 두 갈래로 나눴다.
| 1분 성능 기록 | 즉시 알림 | |
|---|---|---|
| 언제 | 1분마다. 문제가 없어도 보낸다. | 불편이 이어질 때, 접속 문제가 생길 때 |
| 대상 | 샘플링한 일부 세션(기본값은 전체) | 모든 세션 |
| 쓰임 | 평소 수준 파악, 알림 기준 조정 | 담당자 알림, 원인 진단 |
| Sentry 제품 | Application Metrics(용량 과금) | Errors(건수 과금) |
측정 경로는 PIXI ticker의 프레임 시간과 서버 어댑터의 요청·응답·단절 훅을 공통 텔레메트리 모듈(src/common/telemetry/)로 모으는 구조다. 요약은 Metrics로, 지속된 저하와 실패는 Issue로 나간다. 이슈는 Sentry 웹훅을 거쳐 웹 에러 진단 도구로 이어진다. DSN 환경변수가 비어 있으면 측정도 전송도 하지 않는다.
집계 창
섹션 제목: “집계 창”창은 60초 집계 구간이다. 창이 10초를 채우지 못하고 닫히면 그 창의 요약은 버리고 RTT 판정에도 쓰지 않는다.
- FPS는 게임 씬이 처음 뜬 뒤부터 잰다. 인트로와 에셋 로딩 중의 끊김은
session.load_time_ms로 본다. - 탭이 숨겨지거나 WebGL 컨텍스트가 손실된 동안에는 창을 끊는다. 그 구간의 RTT 요청은 샘플에도 timeout에도 넣지 않는다.
- 목업 모드에서는 RTT를 재지 않는다.
꼬리표
섹션 제목: “꼬리표”모든 기록과 알림에 누가 겪었는지 나눠 볼 꼬리표를 붙인다. 게임 이름은 꼬리표가 아니라 게임마다 분리한 Sentry 프로젝트로 구분한다.
| 꼬리표 | 뜻 |
|---|---|
operator |
접속 경로가 된 운영사 코드 |
user |
서버 기록과 맞출 사용자 번호. 이슈에만 붙인다. |
rendering_context |
그 판에서 PIXI가 고른 렌더러(webgpu / webgl2 / webgl1) |
screen_resolution |
화면 해상도 |
orientation |
portrait / landscape / desktop |
device_type |
mobile / tablet / desktop |
language · currency |
플레이어가 보는 언어와 통화 |
environment |
접속 호스트로 정한 서버 환경 |
release |
배포 커밋 |
telemetry.test |
테스트 패널을 연 세션 |
사용자 번호는 Sentry.setUser({ id })로 이슈에만 붙인다. 메트릭에는 SDK가 user.* 속성을 넣는데, beforeSendMetric에서 허용 목록 밖 속성을 지워 1분 기록에는 사용자 번호가 남지 않게 했다.
플레이어 이름, 로그인 아이디, 기기 모델명은 보내지 않는다. 잔액, 베팅 금액, 거래 ID, 서버 응답 본문도 어떤 이벤트에도 넣지 않는다. 결과를 알 수 없는 베팅 이슈에만 서버 로그를 찾을 수 있게 라운드 ID와 베팅 ID를 넣는다.
1분 성능 기록
섹션 제목: “1분 성능 기록”메트릭을 보낼지는 세션 시작 때 샘플 비율로 한 번 정한다. 샘플링에서 빠진 세션도 이슈 판정은 그대로 한다.
| 메트릭 | 설명 |
|---|---|
frame_time.p50_ms · p95_ms · p99_ms, fps.avg, fps.low_seconds, fps.low_streak_max_s, frame.dropped, heap.used_mb, texture.count |
창마다 보낸다. 속성으로 창 길이와 그 창 안에서 오토베팅이 돌았는지를 붙인다. |
rtt |
응답마다 보낸다. 속성은 이벤트, 결과(ok / error), 요청 변형(auto / manual). |
rtt.timeout, rtt.aborted |
10초 안에 응답이 없던 요청, 응답 대기 중 연결이 끊긴 요청 |
session.load_time_ms, session.time_to_login_ms |
세션당 1회 |
RTT는 요청 이벤트와 응답 이벤트를 짝지어 잰다. 값에는 서버 처리 시간이 포함된다. 응답 훅은 envelope를 풀기 전에 불리므로 오류 응답도 샘플에 들어간다. 레이트리밋으로 드롭된 요청은 집계하지 않고 대기열에서만 지운다. 로그인, 상태 조회, 베팅, 잔액 같은 공용 짝은 공통 모듈에 두고, 게임 전용 요청 짝은 게임 쪽 어댑터가 등록한다.
알림 기준
섹션 제목: “알림 기준”잠깐의 끊김은 무시하고 불편이 이어질 때만 알린다. 기본값은 운영 데이터가 쌓이기 전의 잠정치다.
| 이슈 | 조건 |
|---|---|
perf.low_fps |
1초 버킷의 프레임 수가 30 미만인 초가 7초 연속 |
perf.high_rtt |
샘플 5건 이상인 창의 p95가 100ms를 넘는 일이 3창 연속 |
net.uncertain_wager |
금전·선택 요청이 timeout, DTO 위반, 서버 오류로 세션을 동결함 |
net.protocol_error |
그 밖의 요청(부팅 시 설정 조회 등)이 세션을 동결함 |
net.connect_error / net.disconnect |
최초 접속 실패 / 서버나 네트워크 단절 |
net.result_code |
지정한 서버 오류 결과 코드. 이미 결과 불명 베팅으로 보낸 경우는 제외 |
각 이슈에는 판단 근거를 함께 담는다. perf.low_fps는 연속 길이, 진행 중인 창의 백분위·평균·드롭 수, 힙, 텍스처 수, 오토베팅 여부를, perf.high_rtt는 창별 p50·p95·max·샘플 수·timeout·aborted와 마지막 창의 이벤트별 분해를 담는다.
알림 폭주를 막는 규칙도 정했다.
perf.*는 두 종류를 합쳐 세션당 3건까지 보낸다.net.*는 종류별로 세션당 한 번,net.result_code는 이벤트와 코드 조합마다 한 번이다.- 레이트리밋, 킥, 중복 로그인은 보내지 않는다. 클라이언트가 직접 끊은 연결(
io client disconnect)과pagehide이후의 끊김도 이슈로 만들지 않는다. - 잔액 부족, 중복 베팅, 점검, 로그인 만료 같은 정상 거절과 사용자에게 띄운 공용 안내는 breadcrumb으로만 남긴다. 나중에 이슈가 생기면 그 맥락으로 함께 보인다.
perf.*와net.disconnect는 warning, 나머지net.*는 error 등급이다.
응답 시간에는 서버 처리 시간이 들어 있어 100ms는 모바일에서 자주 넘을 수 있다. 그래서 첫 배포 뒤에는 perf.high_rtt부터 확인한다. 응답 없음 기준은 게임의 요청 watchdog과 같은 10초로 코드에 고정했다.
환경변수로 기준 조정
섹션 제목: “환경변수로 기준 조정”기준값은 Vite 환경변수로 빌드 시점에 넣는다. 비어 있거나 허용 범위를 벗어난 값은 무시하고 기본값을 쓴다. CI 변수를 바꾸면 다시 빌드해야 반영된다.
| 변수 | 기본값 | 뜻 |
|---|---|---|
VITE_SENTRY_PERF |
켜짐 | 끄면 FPS·RTT 측정, 메트릭, perf.* 이슈를 끈다. 꼬리표와 net.* 이슈는 계속 동작한다. |
VITE_SENTRY_PERF_SAMPLE_RATE |
1 | 60초 요약 메트릭을 보낼 세션 비율 |
VITE_SENTRY_PERF_FPS_MIN |
30 | 프레임 수가 이 값 미만인 초를 저속 초로 센다. |
VITE_SENTRY_PERF_FPS_STREAK_S |
7 | 저속 초가 이만큼 이어지면 perf.low_fps |
VITE_SENTRY_PERF_RTT_P95_MS |
100 | 창의 RTT p95가 이 값을 넘으면 저하 창 |
VITE_SENTRY_PERF_RTT_WINDOWS |
3 | 저하 창이 이만큼 이어지면 perf.high_rtt |
VITE_SENTRY_PERF_ISSUE_CAP |
3 | 세션당 perf.* 이슈 상한 |
과금 구조 검토
섹션 제목: “과금 구조 검토”이 기능이 추가로 쓰는 Sentry 항목은 에러와 애플리케이션 메트릭뿐이다. 트레이싱(span), 세션 리플레이, 로그, 프로파일은 보내지 않는다. 과금은 조직 단위·월 단위라 같은 조직의 다른 게임과 포함량을 나눠 쓴다. 포함량과 PAYG 예산을 다 쓰면 그달의 추가 데이터는 버려지고 더 청구되지 않는다.
메트릭은 용량으로 과금되어 세션이 아주 많이 쌓여야 포함량에 가까워진다. 먼저 한도에 닿는 쪽은 건수로 과금되는 에러다. 저속 세션 수가 무료 플랜의 에러 포함량을 넘기 시작하면 상위 플랜으로 올리는 비용을 검토한다.
진행 순서
섹션 제목: “진행 순서”- 꼬리표, 성능 측정, 1분 기록, 알림을 함께 만든다. H5 미니게임 하나에 먼저 넣고, 게임 전용 값(요청 짝, 오토베팅, 세션 동결)은 게임에 둔 채 공통 모듈을 템플릿 저장소로 올린다.
- 테스트 서버에서 일부러 느리게 만들어 기록과 알림이 오는지 확인한다. 고주사율 화면과 탭 전환도 확인한다.
- 실제 서비스에는 기록 비율과 알림 기준을 기본값으로 시작한다.
- 2~4주 뒤 쌓인 1분 기록으로 비율과 기준을 확정한다.
검증용 테스트 패널
섹션 제목: “검증용 테스트 패널”테스트 서버에서는 URL 쿼리(sentry_test=1)로 테스트 패널을 띄워 알림을 직접 보내 본다. 운영 환경이나 DSN이 없는 빌드에서는 패널이 뜨지 않고, 패널 코드는 패널을 띄울 때만 동적으로 받는다. 테스트 이벤트에는 telemetry.test=true가 붙어 실제 알림과 구분된다.
| 버튼 | 이슈 | 동작 |
|---|---|---|
| 화면 끊김 | perf.low_fps |
실제 판정 경로를 탄다. FPS_STREAK_S + 3초 동안 프레임을 붙잡는다. |
| 응답 지연 | perf.high_rtt |
같은 전송 코드에 가짜 값을 넣어 바로 보낸다. |
| 결과 불명 베팅 | net.uncertain_wager |
가짜 값. 누를 때마다 세션당 1회 제한을 비운다. |
| 프로토콜 오류 | net.protocol_error |
가짜 값 |
| 서버 오류 코드 | net.result_code |
가짜 오류 코드 |
| 연결 끊김 | net.disconnect |
알림만 보내고 실제 연결은 끊지 않는다. |
| 접속 실패 | net.connect_error |
가짜 메시지 |
| 1분 요약 지금 보내기 | Metrics | 현재 창을 바로 닫아 보낸다. 10초 미만이면 보내지 않는다. |
| 스크립트 예외 | 일반 오류 | 기존 오류 수집 확인 |
Sentry SDK의 Dedupe 통합은 직전과 같은 이벤트를 버린다. 그래서 같은 버튼을 연달아 누르면 패널이 전송을 막고 다른 버튼을 먼저 누르라고 안내한다.
Sentry에서 읽는 법
섹션 제목: “Sentry에서 읽는 법”Issues 목록에서 프로젝트와 환경으로 좁히고 검색어로 거른다.
| 보고 싶은 것 | 검색어 |
|---|---|
| 테스트로 보낸 것만 | telemetry.test:true |
| 테스트를 뺀 실제 알림 | !telemetry.test:true |
| 특정 플레이어 | user.id:<사용자 번호> |
| 특정 운영사 | operator:<운영사 코드> |
| 특정 종류 | 이름 그대로. 예: perf.low_fps |
상세 화면에서는 Tags로 운영사·기기 비율을, User로 서버 기록을 찾을 사용자 번호를, Breadcrumbs로 알림 직전의 UI 클릭·요청/응답 콘솔 로그·결과 코드를, Contexts로 종류별 판단 값을 본다. Sentry는 깊이 들어간 응답 본문을 잘라 결과 코드까지만 남긴다.
1분 기록
섹션 제목: “1분 기록”Explore → Metrics에서 본다. 기본 집계가 sum이라 응답 시간·프레임 그래프가 비어 보일 수 있어 집계를 바꿔야 한다. Group by에 operator, device_type, orientation, rendering_context를 고르면 어디가 느린지 비교된다.
| 기록 | 추천 집계 | 읽는 법 |
|---|---|---|
fps.avg |
avg, min | 1분 평균 FPS. min은 가장 나빴던 1분 |
frame_time.p95_ms |
p95 | 오래 걸린 프레임의 시간. 클수록 끊김 |
fps.low_seconds |
avg | 1분 중 30 FPS 미만이던 시간(초) |
frame.dropped |
sum | 33ms를 넘긴 프레임 수 |
rtt |
p50, p95 | 서버 응답 시간. event로 나눈다. |
rtt.timeout |
sum | 10초 안에 응답이 오지 않은 요청 수 |
session.load_time_ms |
p50, p95 | 게임이 뜨기까지 걸린 시간 |
heap.used_mb, texture.count |
avg, max | 계속 오르기만 하면 누수를 의심한다. |
p50은 중간값, p95는 느린 쪽 5%의 경계를 나타내며 알림 기준과 같은 계산이다.
참고한 범위
섹션 제목: “참고한 범위”다른 게임사가 자기 게임에 넣은 수집 항목 중 서비스 상태를 보는 항목을 가져왔다. 1분 화면 성능, 시작 시 환경 정보, 플레이어에게 보여 준 오류 안내, 이슈의 사용자 번호다. 긴 멈춤과 고주사율은 프레임 시간으로 잰다. 서버 응답 본문, 기기 모델명, 버튼 클릭 추적, A/B 실험은 범위 밖으로 뒀다.