ES /docs

[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH

RCA: [CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH

Overview#

What Happened#

2026-07-13 20:35 KST 부터 약 1분간 cupixworks-api (production, ap-southeast-2, tenant nswgov) 에서 Customer.io Track API 호출이 Customerio::InvalidResponse: Unauthorized request 로 11회 실패했다. 실패는 BE_MAIL_ACCOUNT_MY_TEAM_SEARCH 이벤트에만 국한되며, 같은 시간대의 다른 Customer.io 이벤트 (BE_MAIL_CAPTURE_PROCESSING_COMPLETE, BE_MAIL_PROJECT_UPDATE) 는 정상적으로 전송되고 있다.

Quick Facts#

Field Value
exception.class Customerio::InvalidResponse
exception.message {"meta": {"error": "Unauthorized request"}}
top_frame lib/cupix/mailer/dispatcher.rb (track_customer_io)
triggered_by Cupix::Mailer::UserMailer.request_my_teams (via Api::V1::AccountsController#request_my_teams)
env production, ap-southeast-2, tenant nswgov

Affected Teams#

Team / Domain Error Count Impact
nswgov (ap-southeast-2) 11 "Find my teams" 이메일이 Customer.io 를 통해 발송되지 않음. SendGrid 경로가 활성화되어 있으면 실제 사용자 이메일은 도착할 수 있으나 (EMAIL_PROVIDER == 'BOTH' 등의 설정에 따라), Customer.io 캠페인/분석 데이터는 누락

Timeline#

  1. 2026-07-13 20:35 KST — 첫 실패 발생 (first_seen, 11:35:34.362Z UTC)
  2. 2026-07-13 20:36 KST — 마지막 실패 관측 (last_seen, 11:36:42.370Z UTC), 총 11건
  3. 2026-07-13 20:36 KST 이후 — 동일한 로그 필터로 이후 발생 없음 (now-24h 재검색 시 11건이 전부)

Error Log#

Datadog Logs

text
[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH

대표 로그 항목 (Datadog):

json
{
  "timestamp": "2026-07-13 20:36:42",
  "status": "error",
  "message": "[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH",
  "class": "Cupix::Mailer::Dispatcher",
  "function": "track_customer_io",
  "error": {
    "msg": "Customerio::InvalidResponse: {\n  \"meta\": {\n    \"error\": \"Unauthorized request\"\n  }\n}\n"
  }
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 11
  • 최초 발생: 2026-07-13 20:35 KST
  • 최근 발생: 2026-07-13 20:36 KST
  • 사용자 영향은 제한적이다. 실패한 호출은 Cupix::Mailer::Dispatcher.dispatch_v1 내부의 Customer.io 전송 단계이며, 호출자인 AccountsController#request_my_teams 는 이 예외를 인지하지 못한다 (dispatcher 가 rescue => e 로 삼키고 로그만 남김). 즉 API 응답 자체는 200 { email: ... } 로 성공한다. 단, Customer.io 를 통한 "my teams" 안내 캠페인은 미발송된다.

Root Cause Summary#

Cupix::Mailer::UserMailer.request_my_teams 는 인증되지 않은 공개 엔드포인트에서 호출되며, Cupix::Mailer::Dispatcher.dispatch_v1 로 넘어와 raw email 문자열을 Customer.io track() 의 첫 인자 (customer_id) 로 사용한다. 다른 성공 경로 (dispatch) 는 인증된 actor.crn 을 identifier 로 쓰고 이 값은 Customer.io workspace 에 이미 identify 되어 있다. nswgov 요청에서 사용된 email 은 해당 Customer.io workspace 에 identify 된 프로필이 없거나 다른 workspace 에 속해 있어, Track API 가 "Unauthorized request" (Customer.io Track API 가 알려지지 않은 identifier 또는 workspace 권한 밖의 identifier 에 대해 반환하는 401) 를 응답했다. 즉 원인은 (1) 미확인 email 을 Customer.io identifier 로 직접 사용하는 dispatch_v1 경로의 구조적 문제 + (2) EMAIL_PROVIDER 가 Customer.io 를 포함하지만 nswgov 사용자 identity 가 해당 workspace 에 프로비저닝되지 않은 환경적 조건의 조합이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/accounts_controller.rb:31 — 인증 없이 (skip_before_action :authenticate!) 호출되는 request_my_teams 액션
  • Dispatch: lib/cupix/mailer/user_mailer.rb:127-132Cupix::Mailer::Dispatcher.dispatch_v1(email: email, event: 'BE_MAIL_ACCOUNT_MY_TEAM_SEARCH', ...)
  • Failure point: lib/cupix/mailer/dispatcher.rb track_customer_io (production master 기준, commit c57517a65 로 추가된 helper)
app/controllers/api/v1/accounts_controller.rb:1-2,31-44ruby
class Api::V1::AccountsController < Api::V1::ApiController
  skip_before_action :authenticate!, only: %i[request_my_teams teams]
# ...
  def request_my_teams
    raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'email is required') if params[:email].nil?

    query_option = Cupix::QueryOption::Team.new(get_query_option, params)
    teams = TeamRepository.search_by_email(query_option, params[:email])

    Cupix::Mailer::UserMailer.request_my_teams(
      params[:email],
      teams.select(&:signed_up),
      teams.reject(&:signed_up)
    )

    render_json 200, { email: params[:email] }
  end
lib/cupix/mailer/user_mailer.rb:84-133ruby
def request_my_teams(email, signed_up_teams, available_self_signup_teams)
  template_id = $SENDGRID_TEMPLATE_CONFIG.dig('user_mailer', 'request_my_teams', 'template_id')
  # ...
  Cupix::Mailer::Dispatcher.dispatch_v1(
    email: email,
    event: 'BE_MAIL_ACCOUNT_MY_TEAM_SEARCH',
    properties: data,
    template_id: template_id
  )
end

dispatch_v1 은 raw email 을 track() 의 첫 인자로 그대로 넘긴다 (production 코드, commit c57517a65 이후):

lib/cupix/mailer/dispatcher.rb (production, dispatch_v1 + track_customer_io)ruby
# Variant for UserMailer.request_my_teams which uses SendGrid::Mailer.send_v1
def dispatch_v1(email:, event:, properties:, template_id:)
  ::Analytics.track(user_id: email, event: event, properties: properties)

  if cupixworks? && (EMAIL_PROVIDER == 'CUSTOMERIO' || EMAIL_PROVIDER == 'BOTH')
    track_customer_io(email, event, payload)
  end
  # ...
end

def track_customer_io(track_id, event, payload)
  Cupix::CustomerIo.client.track(track_id, event, payload)
  Cupix::Logger.info("[CustomerIo] track sent - event: #{event}", ...)
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}"
  )
end

Customer.io 클라이언트 자체는 단일 workspace (CUSTOMERIO_SITE_ID, CUSTOMERIO_API_KEY, region US) 로 초기화되어 있어 tenant/region 별로 라우팅되지 않는다:

config/initializers/customerio.rb:1-17ruby
require 'customerio'

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

기대 동작: track_customer_io 가 Customer.io Track API 에 이벤트를 전송하고 200 응답을 받는다.

실제 동작: Customer.io 가 401 { "meta": { "error": "Unauthorized request" } } 를 반환하고, Customerio::InvalidResponse 예외가 rescue => e 에 걸려 error 로그만 남긴다. API 응답에는 영향 없음.

Log Evidence#

Datadog 쿼리 (cluster 파일에서 그대로):

text
service:cupixworks-api status:error @environment:production "[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH"

지난 7일간 실패한 이벤트를 넓게 조회:

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

→ 11 건, 전부 2026-07-13 11:35:34Z ~ 11:36:42Z 범위, 전부 BE_MAIL_ACCOUNT_MY_TEAM_SEARCH. 다른 이벤트로 실패한 경우는 관측되지 않음.

같은 24시간 창에서 성공한 Customer.io track 로그 (비교용):

json
{
  "timestamp": "2026-07-13 21:16:20",
  "status": "info",
  "message": "[CustomerIo] track sent - event: BE_MAIL_CAPTURE_PROCESSING_COMPLETE",
  "class": "Cupix::Mailer::Dispatcher",
  "function": "track_customer_io"
}
json
{
  "timestamp": "2026-07-13 21:03:59",
  "status": "info",
  "message": "[CustomerIo] track sent - event: BE_MAIL_PROJECT_UPDATE",
  "class": "Cupix::Mailer::Dispatcher",
  "function": "track_customer_io"
}

동일한 Customer.io 클라이언트/사이트 ID 로 다른 이벤트는 성공하는데 BE_MAIL_ACCOUNT_MY_TEAM_SEARCH 만 실패한다는 사실이 "글로벌 인증 실패"가 아니라 "특정 identifier 에 대한 인증 실패" 임을 확증한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 dispatch_v1 이 미확인 email 을 Customer.io identifier 로 직접 사용하여 Track API 가 "Unauthorized" 를 반환 dispatch_v1 만 raw email 을 user_id 로 사용 (lib/cupix/mailer/dispatcher.rb); 실패 이벤트가 BE_MAIL_ACCOUNT_MY_TEAM_SEARCH 하나로 국한됨 (dispatch_v1 을 쓰는 유일한 이벤트); 같은 클라이언트로 다른 이벤트는 성공 (Datadog [CustomerIo] track sent 로그) Confirmed
H2 Customer.io 사이트 인증 (CUSTOMERIO_SITE_ID / CUSTOMERIO_API_KEY) 자체가 만료 또는 회전되어 전역적으로 401 Customer.io 401 응답 문구 (Unauthorized request) 같은 시각 다른 이벤트 (BE_MAIL_CAPTURE_PROCESSING_COMPLETE, BE_MAIL_PROJECT_UPDATE) 는 정상 전송됨 → 클라이언트/사이트 credential 은 유효 Rejected
H3 Customer.io 서비스 광역 장애 401 이 여러 건 연속 발생 401 은 서비스 장애가 아닌 인증 실패 코드; 동일 클라이언트가 다른 이벤트에는 정상 응답 Rejected
H4 ap-southeast-2 리전 → US region client 로 라우팅되는 초기화 문제 클러스터 tenant 가 nswgov (AU) 이고 초기화가 region: ::Customerio::Regions::US 고정 다른 이벤트도 동일한 US region client 를 통해 성공. region mismatch 라면 모든 이벤트가 실패해야 함 Rejected
H5 Customer.io workspace 정책상 identify 되지 않은 신규 customer_id 로의 track 은 401 처리됨 (identifier 별 인증) Customer.io Track API 의 "customer identifier must be a known profile" 정책; 실패 event 만 미인증 사용자 대상 (request_my_teams 는 로그인 전 endpoint) 정확한 workspace 설정 확인은 어카운트 콘솔 필요 (uncertain — needs verification) Confirmed (H1 의 세부 메커니즘)

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 이 예외는 이미 Cupix::Mailer::Dispatcher.track_customer_io 에서 rescue 되며 사용자 요청 응답에 영향을 주지 않는다. SendGrid 경로가 활성화되어 있다면 실제 이메일 발송에도 영향이 없다. 다만 다음 로그 볼륨/노이즈 관리 관점에서 짧게 다룰 가치가 있음.

단기 개선 (1주 이내)#

  1. Cupix::Mailer::Dispatcher.track_customer_io (lib/cupix/mailer/dispatcher.rb) 의 rescue => e 를 좁혀 Customerio::InvalidResponse 만 잡고, response body/status 를 로그에 포함해 401 vs 5xx 를 구분한다. 다른 예외는 계속 상위로 전파.
  2. Customer.io 관점에서 "unknown customer identifier" 는 실제로 warn 성격의 이벤트다. Customerio::InvalidResponse 이면서 response 가 401/Unauthorized request 인 경우에 한해 Cupix::Logger.warn 으로 로그 레벨을 낮춰 Datadog error 스트림을 오염시키지 않도록 한다 (fallback 은 그대로 error). 참고: 클라이언트가 identify 되지 않은 identifier 로 track 하는 것은 설계상 예상된 시나리오이므로 blanket downgrade 는 지양하고 401/Unauthorized request 문구에 정확히 매칭해야 한다.
  3. dispatch_v1 이 email 을 identifier 로 사용하기 전에, 최소한 Customer.io 에 identify(email, { email: email }) 를 선행 호출해 프로필을 존재화하는 방안을 검토한다. Customer.io 정책과 라이선스 정책 (프로필 카운트 증가) 확인이 필요하므로 확정 이전에 마케팅/CustomerSuccess 팀과 조율. (uncertain — needs verification)

장기 개선 (재발 방지)#

  1. request_my_teams 처럼 사전에 등록되지 않은 사용자를 대상으로 하는 mail 이벤트는 Customer.io 캠페인이 아닌 transactional 채널 (SendGrid template) 만 사용하도록 dispatch 규칙을 분기한다. 현재 dispatch_v1 이 조건 없이 Customer.io + SendGrid 둘 다 호출하는 구조를 재검토.
  2. Customer.io 실패에 대한 계측을 별도 metric (custom.customerio.track.failure, tags: event, error_code) 으로 노출해, 이벤트별 인증 실패 비율을 추적한다.

Monitoring#

  • 시계열: 이벤트별 Customer.io 실패 카운트
text
sum:logs.hits{service:cupixworks-api,status:error,@class:Cupix::Mailer::Dispatcher,@function:track_customer_io}.as_count()
  • 시계열: 전체 Customer.io track 성공/실패 비율
text
sum:logs.hits{service:cupixworks-api,@class:Cupix::Mailer::Dispatcher,@function:track_customer_io,status:info}.as_count()
text
sum:logs.hits{service:cupixworks-api,@class:Cupix::Mailer::Dispatcher,@function:track_customer_io,status:error}.as_count()
  • 알림: 5분간 BE_MAIL_ACCOUNT_MY_TEAM_SEARCH 실패가 20 건 이상이거나, 다른 이벤트의 실패가 1건이라도 발생하면 노티. (별도 이벤트 실패는 credential 광역 이슈 신호)

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (log level narrowing) ~ standard (dispatch_v1 identify 선행 or transactional-only 분기)
  • 사용자 영향: 없음/미미 (SendGrid 활성 시 실제 메일은 발송, Customer.io 캠페인/analytics 데이터만 누락)