ES /docs

[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_iorescue => 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#

  1. 2026-07-13 15:57 KST — 프로덕션 배포 20260713T0657Z0-69260da5-cupixworks (ap-southeast-2) 완료.
  2. 2026-07-13 16:55 KST — 타 tenant 의 BE_MAIL_CAPTURE_PROCESSING_COMPLETE Customer.io track 정상 전송.
  3. 2026-07-13 16:58 KST — 타 tenant 의 BE_MAIL_CAPTURE_PROCESSING_COMPLETE 정상 전송.
  4. 2026-07-13 17:14 KST — nswgov tenant, capture 46890 processing_completed 후 Customer.io track 이 401 반환 → track_customer_iofalse 반환.
  5. 2026-07-13 17:57 KST — 후속 BE_MAIL_CAPTURE_PROCESSING_COMPLETE 정상 전송 (Customer.io 정상 동작 복귀).

Error Log#

Datadog Logs

text
[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.dispatch with event: '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)

호출 흐름:

lib/cupix/mailer/processing_status_mailer.rb:111-125ruby
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 기준):

lib/cupix/mailer/dispatcher.rb:65-79ruby
::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 내부):

lib/cupix/mailer/dispatcher.rb:130-151ruby
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:

config/initializers/customerio.rb:5-17ruby
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 반환. 상위 dispatchcustomer_io_failed = true 로 SendGrid fallback (send_sendgrid_fallback) 을 시도하거나, EMAIL_PROVIDER == 'BOTH' 이면 이미 SendGrid 병행 경로가 동작.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-worker "CustomerIo" "track failed"

실패 로그 원문 (14일 창 유일):

json
{
  "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"
}

같은 이벤트 종류의 성공/실패 비교:

text
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) — 이메일 이외의 처리 흐름은 정상 진행:

text
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):

text
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 사용자에게 이메일이 전달되지 않아 사용자 영향 발생 실패 로그 존재 dispatchEMAIL_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-150Cupix::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 sent info 로그 (dispatcher.rb:158) 로만 남는다. Customer.io 실패 시 최종 이메일 도달 여부를 대시보드에서 확인할 수 있도록 count 메트릭 (custom.mailer.customerio_failure, custom.mailer.sendgrid_fallback) 을 emit 하는 방향을 권장.
  • EMAIL_PROVIDER 값 확인: production EMAIL_PROVIDERBOTH 인지 CUSTOMERIO 인지에 따라 이번 실패의 사용자 영향이 다르다. Ops 채널에서 확인 필요 (uncertain — needs verification).

장기 개선 (재발 방지)#

  • Customer.io Track 호출에 재시도 도입: customerio gem 자체는 재시도를 하지 않는다. Cupix::Retryable 유틸이 있다면 401 을 포함한 특정 응답에 한해 exponential backoff 1-2회 재시도를 추가하는 방식을 검토. 다만 401 이 진짜 자격 증명 문제라면 재시도가 무의미하므로, 401 지속 시에는 알림을 발동하는 회로 차단기(circuit breaker) 형태가 바람직.
  • Customer.io 상태 페이지 연계: 외부 dep 성 실패를 status-board 에서 인식하도록, 향후 dep:customerio scope 를 추가하거나 vendor 상태 페이지 확인 절차를 RCA 플로우에 편입.

Monitoring#

Customer.io track 실패율 timeseries (Datadog logs-to-metrics 또는 log queries 위젯):

text
service:cupixworks-worker "[CustomerIo] track failed"

같은 이벤트 종류의 성공/실패 비교:

text
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 — 필요한 후속 조치는 로그 레벨 조정과 메트릭 계측 정도.