Conversation
08-17·08-20 두 번 울린 "[운영] 푸시 발송 오류" 를 파고든 결과. 알람이 지목한
원인("설정/페이로드 확인 필요")이 **틀렸고**, 알람 본문은 정작 원인이 적힌
자리에서 잘려 있었다.
## 1) 분류가 iOS 를 못 가르고 있었다
violatesTokenField() 는 details[].fieldViolations[].field 만 본다. 그런데
**APNs 는 fieldViolations 를 쓰지 않는다** — 자기 사유를 ApnsError.reason 으로 준다:
{"@type":"…FcmError", "errorCode":"INVALID_ARGUMENT"}
{"@type":"…ApnsError", "statusCode":400, "reason":"BadDeviceToken"}
그래서 iOS 실패는 항상 false 로 떨어졌고 결과가 두 방향으로 틀렸다:
· tokenDead=false → 죽은 토큰을 영원히 안 지운다. 다음 푸시가 또 그 토큰을
때리고 또 알람이 온다(자기강화).
· needsAttention=true → "우리 페이로드 버그" 라고 틀린 곳을 지목한다.
페이로드 버그가 아니라는 근거는 데이터에 있었다 — 두 번의 실패가 **서로 다른
알림**(접종 알림·로그인 알림)이었고, 페이로드는 모든 푸시가 같은 틀을 쓴다.
버그였다면 매번 터졌어야 한다. 그리고 안드로이드·웹 토큰은 멀쩡했다.
실제 피해: 계정 하나에 활성 iOS 토큰이 17개 쌓여 있었다(전체 43). 실기기는 한두
대다. 다른 계정은 UNREGISTERED 경로로 정상 정리돼 1개씩만 남아 있었다.
#170 이 고친 것의 **반대편 오류**다. 그때는 INVALID_ARGUMENT 를 무조건 토큰
사망으로 봐서 멀쩡한 기기를 껐다. 어느 쪽이든 원인은 하나 — 근거 없이 단정한 것.
그래서 이번에도 허용목록으로 간다: APNs 가 토큰을 명시적으로 지목한 세 사유만
죽었다고 보고(BadDeviceToken · DeviceTokenNotForTopic · Unregistered), 모르는
사유는 살려 두고 사람에게 넘긴다.
허용목록에서 특히 조심한 것 — ExpiredProviderToken / InvalidProviderToken 은
우리 APNs 인증키 문제다. 여기 넣었으면 **키 만료 한 번에 모든 사용자의 기기가
전부 꺼진다.** 테스트로 못 박았다.
## 2) 알람이 원인 직전에서 잘리고 있었다
attention ??= `${v.code} — ${JSON.stringify(err?.error?.details ?? err).slice(0, 160)}`
details[0] 은 언제나 {"@type":"…FcmError","errorCode":"…"} 라는 상용구다. 160자가
그 상용구와 {"@type":"…ApnsError", 까지만 채우고 **reason 직전에서 끝났다.**
운영에 실제로 저장된 본문이 그렇게 잘려 있어 알람을 받고도 원인을 알 수 없었다.
summarizeFcmError() 를 만들어 **잘려도 정보가 남는 순서**로 바꿨다 — APNs 사유를
맨 앞에, 그다음 필드 위반, 그다음 메시지, 상용구는 버린다. 상한도 600 으로 올렸다
(notifications.body 는 text 라 길이 제약이 없다).
곁가지로 배치 내 건수도 붙인다. 첫 건만 보여주면 "한 대만 그런가" 로 읽힌다.
## 검증
deno test _shared/*_test.ts 22 passed (fcm 신규 9 + summarize 4 포함)
deno check functions/*/index.ts 타입 오류 1건 = 기존 auth.ts:65, 래칫 상한 그대로
push_error 는 자유 텍스트고(스키마 주석: '디버그/모니터링용') 파싱하는 곳이 없어
code 에 사유를 붙여도(INVALID_ARGUMENT/BadDeviceToken) 안전하다 — 전수 확인했다.
## 남는 것
실제 ApnsError.reason 값은 **아직 못 봤다.** 알람에서 잘렸고 push_report 는 일부
토큰이 성공하면 오류를 통째로 버린다(0031 유형). 이 변경으로 다음 발생 때 보인다.
BadDeviceToken 이라면 개발↔프로덕션 APNs 환경 혼재이므로 0031 §5.7
(aps-environment: development)과 이어진다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
08-17·08-20 두 번 울린
[운영] 푸시 발송 오류를 파고든 결과입니다. 알람이 지목한 원인이 틀렸고, 알람 본문은 정작 원인이 적힌 자리에서 잘려 있었습니다.1. 분류가 iOS를 못 가르고 있었다
violatesTokenField()는details[].fieldViolations[].field만 봅니다. 그런데 APNs는fieldViolations를 쓰지 않습니다 — 자기 사유를ApnsError.reason으로 줍니다:{"@type":"…FcmError", "errorCode":"INVALID_ARGUMENT"} {"@type":"…ApnsError", "statusCode":400, "reason":"BadDeviceToken"}그래서 iOS 실패는 항상
false로 떨어졌고, 결과가 두 방향으로 틀렸습니다:tokenDead=falseneedsAttention=true페이로드 버그가 아니라는 근거
두 번의 실패가 서로 다른 알림이었습니다:
vaccine_reminder"김첨지 접종일이 다가와요"security_login"새 기기에서 로그인되었어요"페이로드는 모든 푸시가 같은 틀을 씁니다(제목·본문·data만 다름). 버그였다면 매번 터졌어야 합니다. 그리고 안드로이드·웹 토큰은 멀쩡했고 오류는
ApnsError였습니다.실제 피해 — 토큰이 쌓여 있었습니다
실기기는 한두 대인데 활성 토큰이 17개입니다. 다른 계정은
UNREGISTERED경로로 정상 정리돼 1개씩만 남아 있습니다.#170의 반대편 오류입니다
그때는
INVALID_ARGUMENT를 무조건 토큰 사망으로 봐서 멀쩡한 기기를 껐습니다. 지금은 APNs 계열을 전부 페이로드 버그로 봐서 죽은 토큰을 못 지웠습니다. 어느 쪽이든 원인은 하나 — 근거 없이 단정한 것입니다.그래서 이번에도 허용목록으로 갑니다. APNs가 토큰을 명시적으로 지목한 세 사유만 죽었다고 보고, 모르는 사유는 살려 두고 사람에게 넘깁니다:
ExpiredProviderToken/InvalidProviderToken은 우리 APNs 인증키 문제입니다. 여기 넣었으면 키 만료 한 번에 모든 사용자의 기기가 전부 꺼집니다. 테스트로 못 박았습니다.2. 알람이 원인 직전에서 잘리고 있었다
details[0]은 언제나{"@type":"…FcmError","errorCode":"…"}라는 상용구입니다. 160자가 그 상용구와{"@type":"…ApnsError",까지만 채우고reason직전에서 끝났습니다. 운영에 저장된 본문이 실제로 그렇게 잘려 있어, 알람을 받고도 원인을 알 수 없었습니다.summarizeFcmError()를 만들어 잘려도 정보가 남는 순서로 바꿨습니다 — APNs 사유를 맨 앞, 그다음 필드 위반, 그다음 메시지. 상용구는 버립니다. 상한도 600으로 올렸습니다(notifications.body는text라 길이 제약 없음).곁가지로 배치 내 건수도 붙입니다 — 첫 건만 보여주면 "한 대만 그런가"로 읽힙니다.
검증
push_error는 자유 텍스트이고(스키마 주석: '디버그/모니터링용') 파싱하는 곳이 없어,code에 사유를 붙여도(INVALID_ARGUMENT/BadDeviceToken) 안전합니다 — 전수 확인했습니다.남는 것
실제
ApnsError.reason값은 아직 못 봤습니다. 알람에서 잘렸고,push_report는 일부 토큰이 성공하면 오류를 통째로 버립니다. 이 변경으로 다음 발생 때 보입니다.BadDeviceToken으로 나오면 개발↔프로덕션 APNs 환경 혼재이므로 0031 §5.7(aps-environment: development)과 이어집니다. 그때 §5.7을 함께 처리하는 게 맞습니다.별건으로 남긴 것(이번 PR 밖):
push_report가 토큰별 오류를 보존하지 않음 — 일부 성공 시 통째 유실record_auth_log는 성공 분기에만)app.rate_limits버킷 키에 IP가 평문으로 들어감(login:ip:139.178.129.10:…) —auth_logs는 방침대로 해싱하는데 여기만 평문🤖 Generated with Claude Code