[CustomerIo] track failed - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE
RCA: [CustomerIo] track failed - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE
Overview#
What Happened#
2026-07-13 17:14 KST, cupixworks-worker (ap-southeast-2, tenant nswgov) 에서 capture 46890 처리 완료 후 BE_MAIL_CAPTURE_PROCESSING_COMPLETE 이벤트를 Customer.io Track API 로 전송하는 도중 Customerio::InvalidResponse: "Unauthorized request" (HTTP 401) 이 반환되어 이벤트 전송이 실패했다. 실패는 14일 조회 범위에서 단 1건 발생했고, 같은 이벤트 종류 (BE_MAIL_CAPTURE_PROCESSING_COMPLETE) 는 실패 직전 16:55·16:58 KST, 실패 직후 17:57 KST 에 정상 전송됐다. track_customer_io 는 rescue => e 로 감싸져 있어 예외가 전파되지 않고 false 를 반환하며, 상위 dispatch 로직이 SendGrid fallback 을 트리거하도록 설계돼 있다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Customerio::InvalidResponse |
| exception.message | {"meta": {"error": "Unauthorized request"}} |
| top_frame | lib/cupix/mailer/dispatcher.rb:132 (Cupix::CustomerIo.client.track) |
| deploy | production-ap-southeast-2-20260713T0657Z0-69260da5-cupixworks |
| env | production, ap-southeast-2 |
| tenant | nswgov |
| track_id | crn:nswgov:apse2:cupixworks:4:user/98 |
| request_id | bda8816198676fa499961497 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| nswgov (ap-southeast-2) | 1 | Customer.io Track API 로 1건의 이벤트 미전송. EMAIL_PROVIDER=BOTH 인 경우 SendGrid 로도 병행 전송되므로 이메일 자체는 정상 도달했을 가능성이 큼 (아래 Hypotheses 참조). CUSTOMERIO-only 였다면 send_sendgrid_fallback 이 SendGrid 로 대체 전송. |
Timeline#
- 2026-07-13 15:57 KST — 프로덕션 배포
20260713T0657Z0-69260da5-cupixworks(ap-southeast-2) 완료. - 2026-07-13 16:55 KST — 타 tenant 의
BE_MAIL_CAPTURE_PROCESSING_COMPLETECustomer.io track 정상 전송. - 2026-07-13 16:58 KST — 타 tenant 의
BE_MAIL_CAPTURE_PROCESSING_COMPLETE정상 전송. - 2026-07-13 17:14 KST — nswgov tenant, capture 46890 processing_completed 후 Customer.io track 이 401 반환 →
track_customer_io가false반환. - 2026-07-13 17:57 KST — 후속
BE_MAIL_CAPTURE_PROCESSING_COMPLETE정상 전송 (Customer.io 정상 동작 복귀).
Error Log#
[CustomerIo] track failed - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE
Impact#
- Service:
cupixworks-worker - 발생 횟수: 1
- 최초 발생: 2026-07-13 17:14 KST
- 최근 발생: 2026-07-13 17:14 KST
Root Cause Summary#
Customer.io Track API 가 단발성으로 HTTP 401 Unauthorized request 를 반환한 것이 직접 원인이다. Cupix::CustomerIo.client 는 전역 프로세스에서 한 번만 초기화된 클라이언트 (config/initializers/customerio.rb:9 의 @client ||=) 로, ENV['CUSTOMERIO_SITE_ID'] / ENV['CUSTOMERIO_API_KEY'] 를 계속 사용한다. 즉 tenant 별로 다른 자격 증명을 쓰지 않으며, 동일 프로세스가 실패 직전·직후 다른 tenant 이벤트를 성공적으로 track 했다는 사실이 자격 증명 자체는 유효함을 방증한다. 따라서 root cause 는 tesla 애플리케이션 코드 결함이 아니라 Customer.io API 측의 일시적 401 이다 — 요청 하나가 인증 미들웨어에서 spurious 하게 거부됐다. track_customer_io 는 이미 rescue 로 감싸져 있어 이 실패는 이벤트 전달을 실질적으로 차단하지 않고 SendGrid fallback 경로로 흘러간다.
Technical Analysis#
Code Path#
- Entry point:
lib/cupix/mailer/processing_status_mailer.rb:113(Cupix::Mailer::Dispatcher.dispatchwithevent: 'BE_MAIL_CAPTURE_PROCESSING_COMPLETE') - Dispatcher orchestration:
lib/cupix/mailer/dispatcher.rb:43-79(origin/develop,dispatch) - Failure point:
lib/cupix/mailer/dispatcher.rb:132(Cupix::CustomerIo.client.track호출) - Client factory:
config/initializers/customerio.rb:8-14(전역 memoized 클라이언트, ENV 기반 credentials)
호출 흐름:
Cupix::Logger.info("[ProcessingStatusMailer] - Record:#{record.id} - processing_completed sent by #{reason}".red)
Cupix::Mailer::Dispatcher.dispatch(
actor: record.user,
recipients: [record.user],
team: team,
admins: {
super: ::TeamRepository.new(model: team).super_admin,
team: team.admins,
workspace: workspace.try(:admins),
facility: facility.admins.active
},
event: 'BE_MAIL_CAPTURE_PROCESSING_COMPLETE',
properties: data
)
Dispatcher 내부 (origin/develop 기준):
::Analytics.track(user_id: track_id, event: event, properties: payload)
customer_io_failed = false
if cupixworks? && (EMAIL_PROVIDER == 'CUSTOMERIO' || EMAIL_PROVIDER == 'BOTH')
customer_io_failed = !track_customer_io(track_id, event, payload)
end
if EMAIL_PROVIDER == 'SENDGRID' || EMAIL_PROVIDER == 'BOTH'
Cupix::SendGrid::Mailer.send(mailer_parameters) if mailer_parameters && !(cupixworks? && CUSTOMERIO_ONLY_EVENTS.include?(event))
elsif customer_io_failed && mailer_parameters
send_sendgrid_fallback(event) { Cupix::SendGrid::Mailer.send(mailer_parameters) }
end
실패 지점 (track_customer_io 내부):
def track_customer_io(track_id, event, payload)
Cupix::CustomerIo.client.track(track_id, event, payload)
Cupix::Logger.info(
"[CustomerIo] track sent - event: #{event}",
class: 'Cupix::Mailer::Dispatcher',
function: __method__,
track_id: track_id,
activity: payload[:activity],
role: payload[:role]
)
true
rescue => e
Cupix::Logger.error(
"[CustomerIo] track failed - event: #{event}",
class: 'Cupix::Mailer::Dispatcher',
function: __method__,
track_id: track_id,
error: "#{e.class}: #{e.message}"
)
false
end
전역 client:
module Cupix
module CustomerIo
class << self
def client
@client ||= ::Customerio::Client.new(
ENV['CUSTOMERIO_SITE_ID'],
ENV['CUSTOMERIO_API_KEY'],
region: ::Customerio::Regions::US
)
end
end
end
end
기대 동작: Customerio::Client#track 가 2xx 를 반환하고 Cupix::Logger.info("[CustomerIo] track sent ...") 로그 방출. 이후 EMAIL_PROVIDER 값에 따라 SendGrid 병행 전송 여부 결정.
실제 동작: Customer.io API 가 401 을 반환 → customerio gem 이 Customerio::InvalidResponse 를 raise (sorbet/rbi/gems/customerio@5.5.0.rbi:224) → rescue => e 에 잡혀 error 로그 방출, 메서드는 false 반환. 상위 dispatch 는 customer_io_failed = true 로 SendGrid fallback (send_sendgrid_fallback) 을 시도하거나, EMAIL_PROVIDER == 'BOTH' 이면 이미 SendGrid 병행 경로가 동작.
Log Evidence#
Datadog 쿼리:
service:cupixworks-worker "CustomerIo" "track failed"
실패 로그 원문 (14일 창 유일):
{
"timestamp": "2026-07-13T08:14:38.254Z",
"status": "error",
"message": "[CustomerIo] track failed - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE",
"class": "Cupix::Mailer::Dispatcher",
"function": "track_customer_io",
"track_id": "crn:nswgov:apse2:cupixworks:4:user/98",
"tenant": "nswgov",
"environment": "production",
"error": {
"msg": "Customerio::InvalidResponse: {\n \"meta\": {\n \"error\": \"Unauthorized request\"\n }\n}\n"
},
"request_id": "bda8816198676fa499961497",
"host": "ip-10-1-145-250.ap-southeast-2.compute.internal",
"version": "production-ap-southeast-2-20260713T0657Z0-69260da5-cupixworks"
}
같은 이벤트 종류의 성공/실패 비교:
service:cupixworks-worker "BE_MAIL_CAPTURE_PROCESSING_COMPLETE"
| 시각 KST | 상태 | 결과 |
|---|---|---|
| 2026-07-13 16:55:47 | info | [CustomerIo] track sent |
| 2026-07-13 16:58:03 | info | [CustomerIo] track sent |
| 2026-07-13 17:14:38 | error | [CustomerIo] track failed — Unauthorized (본 인시던트) |
| 2026-07-13 17:57:43 | info | [CustomerIo] track sent |
동일 프로세스/설정에서 이벤트가 실패 전후로 성공했음을 확인.
같은 request_id 트레이스 (@request_id:bda8816198676fa499961497) — 이메일 이외의 처리 흐름은 정상 진행:
17:14:38 info [ProcessingStatusMailer] 흐름 이후 CreateCapture3dReconstructionJob 110944 status_code(202)
17:14:38 info reconstruction_state has transitioned from none to queued on Capture 46890
17:14:38 error [CustomerIo] track failed - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE
17:14:38 info start to push processing_completed notification for record:3586
Customerio::InvalidResponse 클래스 정의 (401 등 non-2xx 응답 시 raise):
sorbet/rbi/gems/customerio@5.5.0.rbi:224
class Customerio::InvalidResponse < ::RuntimeError
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Customer.io API 가 단발성으로 401 을 반환한 transient 서버측 오류 | 실패 로그 error body 가 Customer.io 서비스가 반환한 {"meta": {"error": "Unauthorized request"}} 그대로. 같은 프로세스가 실패 직전 (16:55, 16:58) 과 직후 (17:57) 에 동일 credential 로 정상 track 성공. 14일 창에서 유일한 실패 (service:cupixworks-worker "CustomerIo" "track failed" → 1건). |
재현 근거 없음 — Customer.io 상태 페이지 확인 미실시 (uncertain). | Confirmed |
| H2 | CUSTOMERIO_SITE_ID / CUSTOMERIO_API_KEY 자격 증명 만료 또는 rotate 로 인한 실패 |
원인이라면 401 이 발생 (일치) | Cupix::CustomerIo.client 는 `@client |
|
| H3 | tenant nswgov 전용 Customer.io 계정 자격 증명 문제 |
tenant 필드가 nswgov 하나로 고정 |
코드 grep 결과 tesla 전체에서 CUSTOMERIO_SITE_ID/CUSTOMERIO_API_KEY 참조는 config/initializers/customerio.rb 1곳뿐. tenant 별 client 스위치 로직 없음. BE_MAIL_CAPTURE_PROCESSING_COMPLETE 를 포함해 이 tenant 관련 이후 이벤트 (17:57 성공) 도 정상. |
Rejected |
| H4 | payload 크기/형식 문제로 특정 요청만 거부됨 | 이벤트 payload 에 role.admin.facility 배열이 nswgov 시설 admin 수만큼 확장됨 |
Customer.io 는 payload 오류에는 401 이 아닌 400/422 를 반환 (Customer.io API 규약). Unauthorized request 는 인증 층에서 거부됐음을 의미하므로 payload 원인 아님. |
Rejected |
| H5 | 최근 배포 (20260713T0657Z0-69260da5-cupixworks, 15:57 KST) 로 인한 회귀 |
시간적 근접성 | 배포 후 동일 프로세스에서 다수의 track 이벤트가 정상 성공 (16:55, 16:58, 17:57 및 다양한 다른 이벤트). 실패는 단발. | Rejected |
| H6 | 사용자에게 이메일이 전달되지 않아 사용자 영향 발생 | 실패 로그 존재 | dispatch 는 EMAIL_PROVIDER == 'BOTH' 조건 하에 SendGrid 병행 발송을 수행하고 (dispatcher.rb:73-74), CUSTOMERIO-only 경로에서도 send_sendgrid_fallback 로 SendGrid 대체 발송. 현재 프로덕션 EMAIL_PROVIDER 값을 이번 조사에서 직접 확인하지 못했으므로 uncertain — needs verification. |
Inconclusive |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 코드 결함이 아니고 이미 rescue 처리로 예외가 전파되지 않으며 SendGrid fallback 이 존재한다. 재발이 확인되지 않는 한 즉시 조치는 불필요.
단기 개선 (1주 이내)#
- 로그 레벨 재검토:
lib/cupix/mailer/dispatcher.rb:143-150의Cupix::Logger.error는 재시도 없는 단발 실패 시 error 로 남는데, SendGrid fallback 이 성공하면 실제 사용자 영향은 없다. 반복 실패 (예: 최근 10분 내 동일 tenant 3회 이상) 만 error 로 남기고 나머지는warn으로 낮추는 방향을 검토. 근거: 14일 창 error 1건이 alert 노이즈로 흘러들어옴. (Memory 노트의 "true bug vs expected operational scenario" 원칙과 부합.) send_sendgrid_fallback성공 여부 추적: 현재는[CustomerIo] SendGrid fallback sentinfo 로그 (dispatcher.rb:158) 로만 남는다. Customer.io 실패 시 최종 이메일 도달 여부를 대시보드에서 확인할 수 있도록 count 메트릭 (custom.mailer.customerio_failure,custom.mailer.sendgrid_fallback) 을 emit 하는 방향을 권장.EMAIL_PROVIDER값 확인: productionEMAIL_PROVIDER가BOTH인지CUSTOMERIO인지에 따라 이번 실패의 사용자 영향이 다르다. Ops 채널에서 확인 필요 (uncertain — needs verification).
장기 개선 (재발 방지)#
- Customer.io Track 호출에 재시도 도입:
customeriogem 자체는 재시도를 하지 않는다.Cupix::Retryable유틸이 있다면 401 을 포함한 특정 응답에 한해 exponential backoff 1-2회 재시도를 추가하는 방식을 검토. 다만 401 이 진짜 자격 증명 문제라면 재시도가 무의미하므로, 401 지속 시에는 알림을 발동하는 회로 차단기(circuit breaker) 형태가 바람직. - Customer.io 상태 페이지 연계: 외부 dep 성 실패를 status-board 에서 인식하도록, 향후
dep:customerioscope 를 추가하거나 vendor 상태 페이지 확인 절차를 RCA 플로우에 편입.
Monitoring#
Customer.io track 실패율 timeseries (Datadog logs-to-metrics 또는 log queries 위젯):
service:cupixworks-worker "[CustomerIo] track failed"
같은 이벤트 종류의 성공/실패 비교:
service:cupixworks-worker @function:track_customer_io @message:"BE_MAIL_CAPTURE_PROCESSING_COMPLETE"
Alert 후보 (모니터에서만 사용, 대시보드 위젯 아님): 최근 5분 동안 [CustomerIo] track failed 가 임계치 이상이면 페이지.
Risk Assessment#
- Risk level: low — 코드 결함 없음, 이미 rescue + fallback 존재, 14일 창 유일 발생, 동일 이벤트 종류가 실패 전후 정상 처리됨.
- 예상 복잡도: trivial — 필요한 후속 조치는 로그 레벨 조정과 메트릭 계측 정도.