블로그로 돌아가기
2026년 8월 13일Sergei Solod20 분 읽기

모든 브라우저 오류에 알림을 보내던 내가, 오류 소음을 실제로 쓸 수 있는 프로덕션 모니터링으로 바꾼 방법

초기 프런트엔드 리포터는 광고 차단, GTM 실패, AbortError, 정보가 거의 없는 Script error, 실제 Next.js 청크 장애를 모두 같은 오류로 취급했습니다. 이후 소유 주체, 사용자 영향, 증거의 질, 사건 간 상관관계, 복구 여부를 기준으로 모니터링을 다시 설계했습니다.

브라우저 오류 모니터링프런트엔드 관측 가능성JavaScript 오류프로덕션 모니터링Next.js웹 성능

클라이언트 오류 수집을 만든 이유는 단순했습니다. 로컬에서는 재현하기 어려운 실제 사용자 문제를 보고 싶었기 때문입니다. 리포터는 제가 시킨 일을 정확히 했습니다. 실패를 잡아서 저에게 보냈습니다.

문제는 거의 모든 이벤트가 똑같이 중요해 보였다는 것입니다.

외부 분석 스크립트 로드 실패? 빨간 알림. 광고 스크립트 차단? 빨간 알림. 크롤러가 Google Analytics를 불러오지 못함? 빨간 알림. 영상 미리보기에서 play()를 호출한 뒤 Promise가 끝나기 전에 중지됨? 빨간 알림. 유용한 출처나 호출 스택이 없는 Script error.? 이것도 빨간 알림이었습니다.

그 같은 흐름 안에는 실제로 대응해야 할 장애도 섞여 있었습니다. 제 URL이 실수로 https://example.comhttps://example.com/... 형태가 된 경우와, 브라우저가 /_next/static/chunks/... 아래의 자체 Next.js 파일을 불러오지 못한 경우입니다.

수집은 동작했습니다. 모니터링은 동작하지 않았습니다.

이 차이를 이해한 뒤 프런트엔드 관측 가능성을 보는 방식이 달라졌습니다. 브라우저 오류 이벤트는 “무언가가 일어났다”는 관측값일 뿐입니다. 그 자체가 진단, 심각도, 인시던트를 의미하지는 않습니다.

첫 번째 설계 실수는 “오류”를 곧바로 “긴급”으로 해석한 것이었다

초기 모델은 거의 다음과 같았습니다.

브라우저가 error를 보고
        ↓
CLIENT ERROR 전송
        ↓
개발자가 대응해야 함

실제로는 서로 다른 질문이 한데 섞여 있었습니다. 제 코드가 실패한 것인가? 현재 페이지가 실제로 깨졌는가? 예상된 취소인가? 브라우저가 원인을 식별할 만큼 정보를 갖고 있는가? 애플리케이션은 복구됐는가? 메시지 10개가 인시던트 10개인가, 아니면 인시던트 하나가 증상 10개를 만든 것인가?

이 질문에 답하기 전에는 이벤트 하나를 자동으로 알림으로 올리면 안 됩니다.

초기 점검 때 약 18개의 메시지를 한꺼번에 본 적이 있습니다. 대부분은 외부 서비스나 정상적인 수명주기에서 나온 잡음이었습니다. 뚜렷하게 달랐던 것은 두 건이었습니다. 잘못 만들어진 자체 URL은 명백한 버그였고, 자체 JavaScript 청크 로드 실패는 페이지에 필요한 코드가 전달되지 않았을 가능성이 있었습니다. 그런데 당시 리포터는 광고 차단과 거의 같은 긴급도로 보여줬습니다.

그때부터 “브라우저 오류를 전부 수집하는 것”과 “프로덕션 모니터링을 만드는 것”을 같은 일로 보지 않았습니다. 수집은 증거를 보존하고, 모니터링은 그 증거를 판단으로 압축해야 합니다.

브라우저에는 모든 실패를 같은 의미로 전달하는 하나의 오류 채널이 없다

클라이언트 쪽 실패는 서로 다른 의미를 가진 채 들어옵니다.

windowerror 이벤트는 동기 스크립트 오류에 사용되고 리소스 로드 실패에도 관련됩니다. 처리되지 않은 Promise 거부는 다른 경로를 통해 브라우저가 unhandledrejection을 발생시킵니다. 스크립트, 이미지, 미디어를 로드하는 요소는 자체 error 이벤트를 낼 수 있습니다. React와 Next.js는 그 위에 프레임워크 수준의 오류 경계를 추가합니다.

window.error
→ 동기 script error가 바깥으로 전파됐을 수 있음

unhandledrejection
→ rejected Promise가 그 시점에 처리되지 않음

element error
→ resource를 로드하거나 사용할 수 없음

framework boundary
→ rendering 또는 execution이 오류 경계에 도달함

전역 훅이 보는 것은 시스템 경계에 나타난 증상입니다. 전체 인과관계까지 알 수 있는 것은 아닙니다.

이 점을 받아들인 뒤부터는 모든 이벤트를 곧바로 하나의 일반 Error와 같은 심각도로 정규화하지 않았습니다.

가장 먼저 볼 것은 실패한 대상의 소유권이다

잡음을 줄이는 첫 번째 기준은 실패한 코드나 리소스가 누구의 것인지 확인하는 것이었습니다.

/_next/static/chunks/app/... 실패는 다른 오리진의 광고 SDK 실패와 다릅니다. 제 URL 생성 코드가 잘못된 URL을 만든 경우는 분석 요청이 차단된 경우와 다릅니다. 브라우저 확장 프로그램에서 발생한 오류도 별도 범주입니다.

  • 자체 애플리케이션: JavaScript, CSS, API, 미디어, 직접 생성한 URL;
  • 프레임워크/런타임: 애플리케이션 실행 경로에 포함된 Next.js 또는 React;
  • 외부 연동: 분석, 광고, 위젯, 외부 SDK;
  • 실행 환경: 브라우저 확장 프로그램, 크롤러, 네트워크 상태, 개인정보 보호 도구, 브라우저별 동작 차이.

외부 서비스라고 해서 무시해도 된다는 뜻은 아닙니다. 결제나 인증 제공업체는 중요할 수 있고, 광고 장애는 수익에 직접 영향을 줄 수도 있습니다. 하지만 “외부 연동이 비정상”과 “애플리케이션이 충돌”은 같은 사건이 아닙니다.

모든 문제를 같은 긴급 채널로 보내면 그 채널 자체가 무엇을 의미하는지 금방 흐려집니다.

두 개의 자체 장애가 “조치 가능한 신호”의 의미를 알려줬다

잘못된 URL은 단순했습니다.

https://example.comhttps://example.com/resource

여기서는 AdBlock, VPN, 브라우저 정책을 추측할 필요가 없었습니다. URL 자체가 잘못됐습니다. 이미 절대 URL인 값에 오리진을 한 번 더 붙이고 있었습니다.

이 이벤트는 증거가 구체적이고 리소스가 제 것이며, 문제 지점도 제가 통제하는 코드 경로를 가리켰기 때문에 바로 조치할 수 있었습니다.

Next.js 청크 로드 실패는 달랐습니다:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

이 역시 자체 리소스이고 페이지를 망가뜨릴 수 있습니다. 그러나 이벤트 하나만으로 원인을 증명할 수는 없습니다. 오래된 클라이언트가 이전 배포의 자산을 요청했을 수도 있고, 요청이 시간 초과됐을 수도 있습니다. 리버스 프록시나 CDN이 실패했거나 연결이 끊겼을 수도 있고, 파일이 실제로 없었을 수도 있습니다.

따라서 “원인을 찾았다”가 아니라 “중요도가 높으므로 더 많은 맥락을 모아야 한다”가 올바른 결론입니다.

원인에 대한 확신이 낮아도 심각도는 높을 수 있습니다.

Script error.는 단서이지 호출 스택이 아니다

Error: Script error.
filename: unknown
line: 0
column: 0

심각해 보이지만 실제 정보는 거의 없습니다.

브라우저는 교차 출처 스크립트 오류의 세부 정보를 의도적으로 제한합니다. MDN은 적절한 CORS 설정이 없으면 window.onerror가 제한된 정보만 받으며, <script>crossorigin 동작이 노출 가능한 오류 정보에 직접 영향을 준다고 설명합니다.

그래서 정보가 거의 없는 Script error.를 자동으로 “제 애플리케이션이 충돌했다”라고 해석하지 않습니다. 자체 코드일 수도, 외부 코드일 수도, 주입된 코드일 수도 있고, 교차 출처 제한 때문에 브라우저가 자세한 정보를 공개하지 못한 오류일 수도 있습니다.

이벤트는 보관하고 페이지, 빌드, 브라우저, 인접 이벤트와 연관 지어 봅니다. 하지만 단발성 0:0만으로는 긴급 알림을 보내지 않습니다. 같은 특징이 특정 릴리스나 경로 주변에서 반복되기 시작하면 그때 우선순위를 올립니다.

알 수 없다는 것이 무해하다는 뜻은 아닙니다. 반대로 치명적이라는 뜻도 아닙니다.

AbortError는 실제 오류이면서도 정상 수명주기의 일부일 수 있다

영상 미리보기에서 그 차이가 가장 분명하게 드러났습니다:

AbortError:
The play() request was interrupted by a call to pause()

HTMLMediaElement.play()는 Promise를 반환하고, 그 Promise는 거부될 수 있습니다. 미디어 수명주기 작업은 대기 중인 재생을 의도적으로 중단할 수도 있습니다. MDN은 load()가 아직 끝나지 않은 play() Promise를 AbortError로 중단한다고 문서화합니다.

미리보기 그리드에서는 사용자에게 보이는 버그가 없어도 쉽게 생깁니다. 요소가 뷰포트에 들어오고 코드가 play()를 호출합니다. 사용자가 스크롤해서 요소가 화면 밖으로 나갑니다. 재생 시작이 끝나기 전에 코드가 미디어를 일시정지하거나 교체합니다.

Promise 거부는 실제입니다. 하지만 사용자에게 발생한 인시던트는 없을 수도 있습니다.

보통은 호출 지점 가까이에서 처리하면서 예상된 취소와 진짜 재생 실패를 구분해야 합니다. unhandledrejection은 안전망으로는 유용하지만, 정상적인 컴포넌트 수명주기의 의미를 처음 해석하는 장소가 되어서는 안 됩니다.

외부 서비스 장애에는 별도의 상태 모델이 필요하다

초기 로그에는 분석과 광고 도메인의 실패가 많았습니다. 개인정보 보호 기능이 강한 브라우저에서 온 것도 있었고 크롤러에서 온 것도 있었습니다. 크롤러가 Google Analytics를 불러오지 못한 사례는 긴급 알림으로서는 거의 가치가 없었습니다.

그 사실이 알려주는 것은 네트워크 요청 하나가 실패했다는 것뿐입니다. 실제 사용자가 애플리케이션을 쓸 수 있었는지와는 거의 관계가 없습니다.

이벤트를 저장한 것 자체가 문제는 아니었습니다. 자체 JavaScript 청크 로드 실패와 같은 인시던트 흐름에 섞어 보낸 것이 문제였습니다.

  • 사용자에게 애플리케이션이 실제로 깨졌는가?
  • 외부 연동은 정상인가?

차단된 광고는 광고 전달 지표로, 분석 스크립트 실패는 분석 수집 범위 지표로 볼 수 있습니다. 핵심 기능이 그것에 의존하지 않는다면 “프런트엔드 충돌”이라는 긴급 알림으로 보내면 안 됩니다.

분리하고 나면 외부 서비스 문제도 제공업체, 브라우저, 지역별로 집계하기 쉬워집니다.

navigator.onLine은 맥락이지 연결 가능성의 증명이 아니다

브라우저가 스스로 온라인이라고 판단하는지도 기록하기 시작했습니다. 유용한 정보지만 어디까지나 참고 신호입니다.

네트워크 실패로 분류된 일부 이벤트에도 동시에 다음 값이 기록돼 있었습니다:

Online: true

모순이 아닙니다. MDN은 navigator.onLine이 브라우저와 운영체제의 휴리스틱에 의존한다고 명시적으로 경고합니다. 장치가 LAN에 연결돼 있어도 제 오리진에는 접근하지 못할 수 있습니다. VPN, 방화벽, DNS, 부분적인 네트워크 장애가 상황을 더 복잡하게 만듭니다.

online === false
→ 환경 문제를 의심할 강한 힌트

online === true
→ origin이나 resource에 도달 가능했다는 증거가 아님

이 작은 구분만으로도 모니터링 시스템이 힌트를 확정 진단처럼 표현하는 일을 막을 수 있습니다.

하나의 근본 장애가 여러 브라우저 이벤트를 만들 수 있다

수집 정보를 늘리고 나니 다른 종류의 잡음도 보였습니다. 인시던트 하나가 여러 메시지를 만들 수 있었습니다.

청크가 먼저 resource.error를 만들고, 모듈 로더가 이어서 ChunkLoadError를 던진 뒤, React나 Next.js가 오류 경계에 들어가고, 복구 로직이 다시 로드를 예약할 수 있습니다. 각 계층이 별도의 알림을 보내면 사용자 행동 한 번이 여러 프로덕션 장애처럼 보입니다.

메시지 5개는 심리적으로 사용자 5명이 영향을 받은 것처럼 느껴지지만, 실제로는 한 세션과 한 리소스에서 나온 것일 수 있습니다.

메시지 텍스트만 비교해 중복 제거하는 것으로는 부족합니다. 인시던트 상관관계가 필요합니다:

session
+ 짧은 시간 구간
+ 정규화된 error class
+ first-party resource
+ client build
+ route

원시 이벤트는 보관하되, 사람에게는 가장 정보량이 큰 표현을 보여줍니다. 이미 오류 경계에 자체 코드 호출 스택과 정확한 청크 URL이 있다면, 그보다 앞선 일반 리소스 오류가 두 번째 긴급 알림을 만들 필요는 없습니다.

알림은 인시던트에, 저장은 이벤트에.

오류 문자열보다 오류 주변의 맥락이 더 중요해졌다

후기 텔레메트리는 훨씬 더 구조화됐습니다:

clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack

미디어에 대해서는 미디어 오류 코드, 별도 관측이 가능할 때의 HTTP 상태, 실제 Content-Type, 그리고 전달 실패가 HTTP 문제인지 네트워크 문제인지도 기록했습니다.

이 정보 덕분에 예외 문자열 하나로는 답할 수 없는 질문을 할 수 있었습니다. 특정 빌드부터 시작됐는가? 브라우저가 HTTP 응답을 받았는가? Service Worker가 페이지를 제어하고 있었는가? 복구 로직이 이미 실행됐는가? 여러 이벤트가 같은 리소스를 가리키는가? 현재 경로가 실제로 깨졌는가?

Resource Timing API는 리소스 소요 시간, 전송 정보, 그리고 환경이 지원하고 허용하는 경우 응답 상태를 제공할 수 있습니다. 다만 제한이 있습니다. 교차 출처 타이밍 정보는 제한되고, 캐시된 리소스는 transferSize: 0이 될 수 있으며, responseStatus도 모든 브라우저에서 지원되지는 않습니다. 그래서 null0은 억지로 의미를 채워 넣지 않고 실제 상태로 남겨야 합니다.

ChunkLoadError는 증상이지 404 탐지기가 아니다

나중에 본 한 이벤트는 청크 실패를 해석하는 방식을 크게 바꿨습니다.

브라우저는 Next.js 레이아웃 청크에 ChunkLoadError를 보고했지만, 보강된 텔레메트리에는 다음 정보도 있었습니다:

resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true

측정된 시간은 약 170초였습니다. 정확한 근본 원인이 무엇이든, 이 데이터만으로도 다음과 같은 단순한 등식은 틀렸다고 말할 수 있습니다:

ChunkLoadError === server가 404를 반환했다

다른 청크 실패에서는 관측 가능한 상태 자체가 없었습니다. 시간 초과로 나타난 것도 있었고 전송 정보가 남은 것도 있었습니다. 오류 종류는 같았지만 주변 증거는 달랐습니다.

Next.js에서는 특히 중요합니다. /_next/static/ 아래 파일은 일반적으로 콘텐츠 해시를 포함하고 불변 자산으로 다뤄집니다. 현재 Next.js 셀프 호스팅 문서도 이런 자산에 긴 캐시 헤더를 적용하는 동작을 설명합니다. 따라서 ChunkLoadError에는 배포 버전 불일치, 오래된 클라이언트, 네트워크, 리버스 프록시, CDN, 캐시, Service Worker, 실제 산출물 누락 등 여러 가설이 가능합니다.

알림 계층이 원인을 만들어내면 안 됩니다. 조사에 필요한 증거를 보존하는 것이 역할입니다.

미디어 오류에서도 같은 교훈을 다른 계층에서 얻었다

어떤 사례에서는 미디어 요소가 다음처럼 보고했습니다:

MEDIA_ELEMENT_ERROR: Format error

이 문장만 보면 코덱 호환성 문제처럼 보입니다.

하지만 일부 이벤트에 대해 전달 경로를 추가로 확인하니 다음과 같았습니다:

HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http

브라우저는 영상을 요청했지만 HTTP 오류가 담긴 HTML을 받았습니다. 미디어 요소는 HTML을 영상으로 디코딩할 수 없으므로 겉으로는 “Format error”가 됩니다. 유용한 진단은 전달 계층에 있었습니다.

실패를 처음 관측한 계층과 실제로 실패를 만든 계층은 같지 않을 수 있습니다.

“코덱 장애”, “네트워크 중단”, “캐시 버그”, “청크 누락”은 모두 결론입니다. 텔레메트리는 먼저 관측된 사실을 기록해야 합니다.

브라우저 실패를 다섯 가지 축으로 평가한다

1. 소유권

자체 애플리케이션, 프레임워크/런타임, 외부 연동, 실행 환경 중 어디에 속하는가?

2. 사용자 영향

현재 경로, 렌더링, 인증, 채팅, 결제 등 핵심 흐름이 실제로 깨졌는가? 아니면 선택적인 광고, 분석, 프리로드, 미리보기만 실패하고 페이지는 계속 사용할 수 있었는가?

3. 증거 품질

자체 코드 호출 스택, 리소스 URL, HTTP 상태, 빌드 ID, 컴포넌트 스택이 있는가? 아니면 0:0Script error.만 있는가?

4. 반복성과 확산

한 세션에서 한 번 나온 것인가, 아니면 같은 릴리스 이후 여러 사용자, 경로, 브라우저에서 같은 특징이 반복되는가?

5. 복구

애플리케이션이 자동으로 복구됐는가? 청크 다시 로드가 예약됐는가? 대체 처리가 성공했는가? 사용자는 여전히 막혀 있는가?

명확한 first-party
+ 큰 사용자 영향
+ 강한 증거
+ 여러 session
+ recovery 없음
= 긴급 incident

third-party
+ optional 기능
+ 약한 증거
+ 단발성
+ 사용자 영향 없음
= metric 또는 낮은 우선순위

잡음을 줄일수록 무작정 버리지 않아야 한다

잡음이 많아지면 정규식 블랙리스트를 잔뜩 만들어 성가신 것을 전부 버리고 싶어집니다. 하지만 그렇게 하면 실제 문제도 숨길 수 있습니다.

모든 AbortError를 억제하면 실제로 중단된 API 요청을 놓칠 수 있습니다. 모든 Script error.를 지우면 특정 브라우저에서만 커지는 오류 군집을 놓칠 수 있습니다. 외부 서비스 실패를 전부 무시하면 결제, 인증, 동의 관리 제공업체의 심각한 장애를 가릴 수 있습니다.

ALERT
→ 사람이 대응해야 하는 강한 incident

RETAIN / AGGREGATE
→ 저장하고 집계한 뒤 cluster가 되면 alert

METRIC / SAMPLE
→ 예상되거나 영향이 작은 noise. 추세와 샘플은 보존

목표는 조용한 모니터링이지, 눈먼 모니터링이 아닙니다.

좋은 분류기의 핵심은 영리한 코드보다 정책에 있다

아래 코드는 제 프로덕션 코드에서 복사한 것이 아닙니다. 처음부터 갖고 있었으면 좋았을 정책을 짧게 표현한 예입니다:

function classifyClientEvent(event) {
  const owner = classifyOwner(event);

  if (isExpectedMediaCancellation(event)) {
    return { severity: "metric", reason: "expected-cancellation" };
  }

  if (owner === "first-party" && breaksActiveRoute(event)) {
    return { severity: "alert", reason: "first-party-user-impact" };
  }

  if (isActiveFirstPartyChunkFailure(event)) {
    return { severity: "alert", reason: "application-chunk" };
  }

  if (owner === "third-party") {
    return { severity: "aggregate", reason: "integration-health" };
  }

  if (isOpaqueScriptError(event)) {
    return { severity: "aggregate", reason: "insufficient-evidence" };
  }

  return { severity: "aggregate", reason: "needs-correlation" };
}

어려운 부분은 breaksActiveRoute() 같은 함수 안에 있습니다. 예외 이름만으로는 충분하지 않고, 경로 맥락, 리소스 소유 주체, 오류 경계 데이터, 때로는 제품별 지식까지 필요합니다.

지문은 메시지가 아니라 인시던트를 따라야 한다

문자열 완전 일치는 좋은 중복 제거 기준이 아닙니다. 압축된 스택 오프셋은 빌드마다 달라지고 청크 해시도 바뀝니다. URL에는 동적 식별자가 들어가고 브라우저마다 문구도 다릅니다.

{
  errorClass,
  normalizedFirstPartyResource,
  routeFamily,
  clientBuild,
  sessionId,
  shortTimeBucket
}

애플리케이션 전체 충돌에서는 가장 위의 자체 코드 스택 프레임이 유용할 수 있습니다. 청크 실패에서는 정규화된 청크 리소스가, 미디어 전달 장애에서는 HTTP 실패 분류와 미디어 경로가 브라우저의 겉보기 오류 메시지보다 더 유용할 수 있습니다.

resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled

이렇게 하면 관련된 네 개의 관측을 네 번의 긴급 장애가 아니라 하나의 인시던트로 묶을 수 있습니다.

긴급 알림 채널의 역할은 훨씬 좁아야 한다

즉시 알림은 현재 경로를 깨뜨리는 자체 런타임 오류, 실제 사용자 상호작용에 영향을 주는 React/Next.js 오류 경계, 현재 필요한 자체 JavaScript/CSS 로드 실패, 여러 세션이나 빌드에서 반복되는 ChunkLoadError, 사용자를 막아 버리는 핵심 API/데이터 실패, 잘못 생성된 URL처럼 명확한 자체 불변 조건을 어긴 경우에 집중합니다.

단발성이고 정보가 부족한 Script error., 복구에 성공한 자체 리소스 실패, 원인 계층이 아직 불명확한 미디어 오류, 먼저 묶어서 봐야 하는 브라우저별 이상 현상은 바로 알리지 않고 보관합니다.

알려진 광고·분석 리소스 실패, 예상된 미디어 AbortError, 크롤러에서만 나타나는 외부 서비스 실패, 강한 오프라인 신호가 있는 실패, 현재 경로에 영향을 주지 않는 선택적 추측성 리소스는 보통 지표나 샘플 진단으로 보냅니다.

긴급 알림은 브라우저가 뱉은 메시지 수가 아니라, 조치 가능한 사용자 영향을 뜻해야 합니다.

하나의 “오류 개수”보다 인시던트를 측정한다

  • 1,000개 세션당 자체 인시던트 수;
  • 빌드 ID별 영향받은 세션 수;
  • 경로별 오류 경계 인시던트;
  • 리소스와 배포별 청크 실패;
  • 제공업체별 외부 연동 실패율;
  • 예상된 취소 횟수. 갑작스러운 변화가 보이도록 따로 기록;
  • 자동 복구 성공률;
  • 고유 인시던트 수와 원시 이벤트 수를 분리.

“이벤트 하나가 발생했다”는 프로덕션 알림 기준으로 거의 쓸모가 없습니다. “새 빌드에서 같은 자체 인시던트가 여러 독립 세션에 영향을 주고 있고, 복구도 실패하고 있다”는 훨씬 의미 있는 운영 신호입니다.

클라이언트 모니터링만으로 근본 원인을 증명할 수는 없다

브라우저 텔레메트리에는 분명한 한계가 있습니다.

HTTP 상태가 없다는 것은 API 미지원, 교차 출처 제한, 요청 취소, 또는 다른 관측 공백 때문일 수 있습니다. Service Worker가 활성 상태라고 해서 그것이 오래된 리소스를 반환했다는 증거는 아닙니다. 배포 뒤 ChunkLoadError가 발생했다고 해서 배포 버전 불일치가 증명되는 것도 아닙니다. online: true 역시 오리진에 접근할 수 있었다는 증거가 아닙니다.

클라이언트 텔레메트리의 역할은 가설을 좁히는 것입니다. 서버 로그, 리버스 프록시 로그, 배포 매니페스트, 캐시 상태, 재현 과정이 여전히 필요할 수 있습니다.

관측 가능성을 무제한 사용자 데이터 수집으로 만들고 싶지도 않습니다. 각 필드는 실패 유형을 구분하는 데 도움이 되기 때문에 존재해야 합니다.

더 나은 텔레메트리는 더 많은 텔레메트리가 아닙니다. 실패 유형을 더 잘 구분하는 텔레메트리입니다.

지금의 원칙: 이벤트를 수집하고, 인시던트를 조사하고, 영향에 알림을 보낸다

처음에는 리포터가 “무언가 고장 났는가?”에 답하면 충분하다고 생각했습니다. 프로덕션에서는 너무 넓은 질문입니다. 크롤러가 분석 스크립트에 접근하지 못하고, 개인정보 보호 도구가 광고를 막고, 미디어 Promise가 의도적으로 취소되고, 사용자의 네트워크가 끊기고, 외부 SDK가 오작동하는 일은 어디선가 늘 생깁니다.

우리 것인가?
사용자가 기능을 잃었는가?
증거는 얼마나 강한가?
반복되는가?
애플리케이션은 recovery했는가?
여러 event인가, 하나의 incident인가?

모니터링을 이런 질문을 중심으로 다시 설계한 뒤에야 빨간 메시지의 홍수가 엔지니어링 도구가 됐습니다.

브라우저 오류는 하나의 관측입니다. 인시던트는 여러 증거를 연결해 사용자 영향을 설명한 것입니다. 알림은 “이제 사람이 행동해야 한다”는 결정입니다.

이 세 가지를 다시 같은 의미로 취급하고 싶지 않습니다.