[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#
- 2026-07-13 20:35 KST — 첫 실패 발생 (
first_seen, 11:35:34.362Z UTC) - 2026-07-13 20:36 KST — 마지막 실패 관측 (
last_seen, 11:36:42.370Z UTC), 총 11건 - 2026-07-13 20:36 KST 이후 — 동일한 로그 필터로 이후 발생 없음 (
now-24h재검색 시 11건이 전부)
Error Log#
[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH
대표 로그 항목 (Datadog):
{
"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-132—Cupix::Mailer::Dispatcher.dispatch_v1(email: email, event: 'BE_MAIL_ACCOUNT_MY_TEAM_SEARCH', ...) - Failure point:
lib/cupix/mailer/dispatcher.rbtrack_customer_io(productionmaster기준, commitc57517a65로 추가된 helper)
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
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 이후):
# 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 별로 라우팅되지 않는다:
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 파일에서 그대로):
service:cupixworks-api status:error @environment:production "[CustomerIo] track failed - event: BE_MAIL_ACCOUNT_MY_TEAM_SEARCH"
지난 7일간 실패한 이벤트를 넓게 조회:
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 로그 (비교용):
{
"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"
}
{
"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주 이내)#
Cupix::Mailer::Dispatcher.track_customer_io(lib/cupix/mailer/dispatcher.rb) 의rescue => e를 좁혀Customerio::InvalidResponse만 잡고, response body/status 를 로그에 포함해 401 vs 5xx 를 구분한다. 다른 예외는 계속 상위로 전파.- 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문구에 정확히 매칭해야 한다. dispatch_v1이 email 을 identifier 로 사용하기 전에, 최소한 Customer.io 에identify(email, { email: email })를 선행 호출해 프로필을 존재화하는 방안을 검토한다. Customer.io 정책과 라이선스 정책 (프로필 카운트 증가) 확인이 필요하므로 확정 이전에 마케팅/CustomerSuccess 팀과 조율. (uncertain — needs verification)
장기 개선 (재발 방지)#
request_my_teams처럼 사전에 등록되지 않은 사용자를 대상으로 하는 mail 이벤트는 Customer.io 캠페인이 아닌 transactional 채널 (SendGrid template) 만 사용하도록 dispatch 규칙을 분기한다. 현재dispatch_v1이 조건 없이 Customer.io + SendGrid 둘 다 호출하는 구조를 재검토.- Customer.io 실패에 대한 계측을 별도 metric (
custom.customerio.track.failure, tags:event,error_code) 으로 노출해, 이벤트별 인증 실패 비율을 추적한다.
Monitoring#
- 시계열: 이벤트별 Customer.io 실패 카운트
sum:logs.hits{service:cupixworks-api,status:error,@class:Cupix::Mailer::Dispatcher,@function:track_customer_io}.as_count()
- 시계열: 전체 Customer.io track 성공/실패 비율
sum:logs.hits{service:cupixworks-api,@class:Cupix::Mailer::Dispatcher,@function:track_customer_io,status:info}.as_count()
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 데이터만 누락)