ES /docs

CPElement::makeElementRecords | No phases found for Element bim_external_id: 49561a92-3ad4-4bfd-a1f6

RCA: CPElement::makeElementRecords | No phases found for Element

Overview#

What Happened#

2026-04-20 23:06~23:15 UTC 사이에 cupixworks-sitetrack-preprocessor-agent 서비스에서 647건의 에러가 발생했다. 두 개의 preprocessor job(1025542, 1025563)이 각각 sitetrack 16666, 16667을 처리하는 과정에서 element에 연결된 category에 phase가 하나도 없어 CPElement::makeElementRecords에서 에러를 로깅했다. 에러는 load_target_models 단계에서 발생했으며, job 자체는 중단되지 않고 후속 단계(download_files, create_request_json 등)까지 계속 진행되었다.

Quick Facts#

Field Value
exception.message No phases found for Element bim_external_id: ..., category_id: 1030864, workarea_ids: [...]
top_frame packages/models/src/cpelement.ts:84
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
tpc (team_id: 778) 647 sitetrack 16666, 16667의 element record 생성 누락. 해당 element들의 진행률 추적 불가.

Timeline#

  1. 23:04:55Z — Job 1025542 시작 (sitetrack 16666)
  2. 23:06:25Zload_target_models 단계에서 637건의 "No phases found" 에러 발생 (~200ms burst)
  3. 23:06:34Z — Job 1025542가 download_files 단계로 진행 (에러 무시하고 계속)
  4. 23:14:04Z — Job 1025563 시작 (sitetrack 16667)
  5. 23:15:56Z — Job 1025563에서 10건의 동일 에러 발생
  6. 23:17:52Z — Job 1025563 정상 완료

Error Log#

Datadog Logs

text
CPElement::makeElementRecords | No phases found for Element bim_external_id: 49561a92-3ad4-4bfd-a1f6-8506c828da70-014ca627, category_id: 1030864, workarea_ids: [ 108942 ]

Impact#

  • Service: cupixworks-sitetrack-preprocessor-agent
  • Team: tpc
  • 발생 횟수: 647
  • 최초 발생: 2026-04-20T23:06:25.098Z
  • 최근 발생: 2026-04-20T23:15:56.506Z

Root Cause Summary#

facility 내에서 특정 category(id: 1030864)에 대해 phase가 하나도 설정되어 있지 않아 발생한 데이터 구성 문제이다. loadAllPhases에서 API로 로드한 phase들을 cpPhase.srvCategoryId를 기준으로 category에 연결하는데, category 1030864에 매칭되는 phase가 없어 해당 category의 cpPhases 배열이 비어 있다. 이후 makeElementRecords()가 호출될 때 this.cpPhases.length === 0 조건에 걸려 error 로그를 남기고 빈 배열을 반환한다. 이 에러는 job을 중단시키지 않지만, 해당 element들에 대한 CPElementRecord가 생성되지 않아 SiteTrack 진행률 추적에서 누락된다.

Technical Analysis#

Code Path#

Entry point: SiteTrackPreprocessorRunner::createTargetModels — job의 load_target_models 단계에서 호출된다.

  1. Category 로드loadAllCategories (runner line 194)에서 facility의 모든 category를 API에서 가져온다.

  2. Element 생성createCPElements (runner line 130)에서 각 BIM × Category 조합에 대해 element를 로드하고, cpElement.cpCategory = cpCategory로 category를 할당한다.

sitetrack-preprocessor-runner.ts:138-143typescript
for (const cpBim of cpBims) {
    for (const cpCategory of cpCategories) {
        const cpElements = await createModels(CPElement, cpBim, this.cupixApi.element.getAll(cpSitetrack.facilityKey, cpBim.id, cpCategory.id, levelIds));
        cpElements.forEach(cpElement => cpElement.cpCategory = cpCategory);
    }
}
  1. Phase 로드 및 Category 연결loadAllPhases (runner line 206)에서 facility의 모든 phase를 로드한 후, cpPhase.srvCategoryId와 일치하는 category에 phase를 추가한다.
sitetrack-preprocessor-runner.ts:219-224typescript
for (const cpPhase of cpPhases) {
    const cpTasksInPhase = cpTasks.filter(cpTask => cpTask.phaseId === cpPhase.id);
    cpTasksInPhase.forEach(cpTask => cpPhase.addCPTask(cpTask));

    const cpCategory = cpCategories.find(cpCategory => cpCategory.id === cpPhase.srvCategoryId);
    if (cpCategory) cpCategory.addCPPhase(cpPhase);
}

여기서 category 1030864에 매칭되는 phase가 없으면 cpCategory.addCPPhase가 한 번도 호출되지 않아 _cpPhases가 빈 배열로 남는다.

  1. Element Record 생성loadAllElementRecords (runner line 295)에서 각 element에 대해 makeElementRecords()를 호출한다.
sitetrack-preprocessor-runner.ts:299-303typescript
// create element records for each cpElement
for (const cpElement of cpElements) {
    const cpElementRecords = cpElement.makeElementRecords();
    cpElementRecords.forEach(cpElementRecord => cpSitetrack.cpElementRecordMap.set(cpElementRecord.identifierKey, cpElementRecord));
}
  1. Failure point: CPElement::makeElementRecords (cpelement.ts:83-89) — this.cpPhases가 빈 배열이므로 error를 로깅하고 빈 배열을 반환한다.
packages/models/src/cpelement.ts:83-89typescript
makeElementRecords = (): Array<CPElementRecord> => {
    if (this.cpPhases.length === 0) {
        logger.error('CPElement::makeElementRecords | No phases found for Element bim_external_id: %s, category_id: %d, workarea_ids: %s'
            , this.bimExternalId, this.categoryId, this.workareaIds);
        return this._cpElementRecords;
    }
    // ...
  1. Phase → Category 연결 경로: CPElement.cpPhases getter가 this.cpCategory?.cpPhases || []를 반환한다.
packages/models/src/cpelement.ts:60typescript
get cpPhases(): CPPhase[] { return this.cpCategory?.cpPhases || []; }

기대 동작: 모든 element가 속한 category에 최소 1개의 phase가 설정되어 있어야 한다. 실제 동작: category 1030864에는 phase가 설정되어 있지 않아 637개의 element가 element record를 생성하지 못했다.

Log Evidence#

Datadog에서 사용한 쿼리:

text
service:cupixworks-sitetrack-preprocessor-agent status:error @environment:production "CPElement::makeElementRecords"

두 job에서 동일한 패턴으로 에러가 발생했다:

text
[2026-04-20T23:06:25.098Z] job=1025542 sitetrack=16666
  CPElement::makeElementRecords | No phases found for Element bim_external_id: 49561a92-3ad4-4bfd-a1f6-8506c828da70-014ca627, category_id: 1030864, workarea_ids: [ 108942 ]

영향받은 category_id는 1030864와 1030869 두 개이며, workarea_ids는 108937108942 (job 1025542)와 108947108948 (job 1025563) 범위이다.

Job 타임라인에서 에러 위치 확인:

text
[23:04:55.910Z] [info] JobManager::updateJobProgress | begin - processingStatus: load_target_models
  ** 23:06:25.098-217Z — 637 errors burst (<200ms) **
[23:06:34.832Z] [info] JobManager::updateJobProgress | begin - processingStatus: download_files

에러가 load_target_modelsdownload_files 사이에 발생하여 loadAllElementRecords 실행 중임을 확인했다.

Job progress warn 로그도 함께 확인되었다:

text
[2026-04-20T23:17:52.081Z] sitetrack=16667 job=1025563
  JobManager::updateJobProgress | skipped - progress: 25, status: save_file_system

이 warn 로그는 진행률 업데이트 건너뜀을 나타내며, 에러와 직접적 관련은 없다. Job은 에러에도 불구하고 끝까지 실행되었다.

관련 사용자 정보:

json
{
  "team": { "domain": "tpc", "id": 778 },
  "sitetrack": { "id": 16666 },
  "job": { "id": 1025542 },
  "user": { "id": 42399, "email": "jesus.diaz@tutorperini.com" }
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Category에 phase가 설정되지 않은 데이터 구성 문제 에러 메시지에 category_id: 1030864가 반복적으로 나타남. loadAllPhases(runner:219-224)에서 phase를 category에 연결하는 로직상, 해당 category_id를 가진 phase가 없으면 빈 배열이 됨. 동일 facility의 다른 job(1025380/sitetrack 16661)에서는 findTask warn이 발생하지만 phase_id가 존재하여 phase는 정상 연결됨. Confirmed
H2 Phase API 호출 실패 또는 빈 응답 만약 phase API가 실패했다면 모든 element에서 에러가 발생해야 함 loadAllPhases(runner:214-216)에서 phase가 0개이면 throw new Error('No Phases found')로 job이 즉시 종료되는데, job은 정상 계속됨. 또한 다른 category의 element에서는 에러가 발생하지 않았을 것 (647건은 전체 element 수 대비 일부). Rejected
H3 Element의 cpCategory가 null이어서 cpPhases가 빈 배열 반환 cpelement.ts:60에서 this.cpCategory?.cpPhases || []이므로 cpCategory가 undefined이면 빈 배열이 됨 에러 메시지에 category_id: 1030864가 출력되고 있어 this.categoryId가 값이 있음. categoryId getter(cpelement.ts:56)는 this._serverModel?.category?.id를 반환하므로 서버 모델에서 category 정보가 있음. 하지만 cpCategory 할당은 runner:141에서 이루어지므로 할당 자체는 정상. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • packages/models/src/cpelement.ts:84-88 — 현재 logger.error 레벨을 logger.warn으로 변경하는 것을 권장한다. Phase가 없는 category에 속한 element는 원래 element record를 생성할 수 없는 상태이며, 이는 facility 설정에 따른 예상 가능한 상황이다. 647건의 error burst가 모니터링 노이즈를 만들고 있으므로 warn 레벨이 적절하다.

단기 개선 (1주 이내)#

  • SiteTrackPreprocessorRunner::loadAllElementRecords (runner:295-358) — makeElementRecords 호출 전에 element의 category에 phase가 있는지 사전 필터링하여, phase가 없는 element는 건너뛰고 요약 로그 1건(예: "Skipped N elements with no phases in category_ids: [...]")만 남기도록 변경한다. 이렇게 하면 element 수만큼 반복되는 개별 에러 대신 한 줄로 상황을 파악할 수 있다.

장기 개선 (재발 방지)#

  • Facility 설정 검증: category에 element가 존재하지만 phase가 없는 경우를 API 또는 UI 레벨에서 경고하는 validation을 추가한다. 이는 사용자가 SiteTrack 설정 시 phase 누락을 조기에 발견할 수 있게 해준다.

Monitoring#

  • 현재 에러를 warn으로 다운그레이드한 후, 아래 쿼리로 추이를 모니터링:
text
service:cupixworks-sitetrack-preprocessor-agent "No phases found for Element" @environment:production
  • Phase가 없는 category가 반복적으로 나타나는 facility를 추적하여 설정 문제를 사전 감지할 수 있다.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 로그 레벨 변경은 단순 수정이며, 사전 필터링도 기존 로직 흐름 내에서 간단히 구현 가능하다. Job 자체는 에러에도 불구하고 정상 완료되므로 서비스 안정성에는 영향 없다.