brandonwie.dev
EN / KR
On this page
backend backendamplitudeapidata-format

Amplitude Export API 응답 형식

Amplitude Export API는 오해하기 쉬운 중첩 압축 형식으로 데이터를 반환해요

Updated March 22, 2026 3 min read

Amplitude export 파일에 gzip.decompress()를 호출했더니 알 수 없는 에러가 났어요. 파일 이름이 *.json.gz여서 당연히 gzip 압축된 JSON이라고 생각했는데, 아니었어요. 30분 디버깅 후에 첫 바이트를 hex-dump 해보니 PK가 나왔어요 — GZIP이 아니라 ZIP 파일의 magic bytes였어요.

Amplitude Export API는 .json.gz 확장자를 가진 파일을 반환하는데, gzip 파일이 아니에요. 안에 gzip 파일을 포함하는 ZIP 아카이브예요. 이 이중 레이어 중첩은 문서에서 명확하지 않고, 오해를 유발하는 파일 확장자가 잘못된 디버깅 경로로 이끌어요.

오해를 유발하는 확장자

파일 이름은 *.json.gz이지만 실제 구조는 중첩되어 있어요:

{PROJECT_ID}_{DATE}_{HOUR}#0.json.gz
└── Actually a ZIP file (magic: PK / 0x504B)
    └── Contains: {PROJECT_ID}/{PROJECT_ID}_{DATE}_{HOUR}#0.json.gz
        └── This inner file IS gzip (magic: 0x1F8B)
            └── Contains: Newline-delimited JSON events

바깥 레이어는 ZIP 아카이브예요. 그 ZIP 안에 gzip 압축된 파일이 있어요. 그 gzip 파일 안에 newline-delimited JSON이 있어요. 세 겹의 래핑인데, 파일 확장자는 중간 것만 설명해요.

형식 식별 방법

ZIP과 GZIP을 구분하는 확실한 방법은 파일 시작 부분의 magic bytes를 확인하는 거예요:

형식Magic BytesHex
ZIPPK0x504B
GZIP\x1f\x8b0x1F8B

표준 gzip 도구와 Python의 gzip.decompress()는 ZIP 파일을 도움이 안 되는 에러 메시지와 함께 거부해요. Amplitude Export API 문서는 바깥 ZIP 래퍼를 명시하지 않고 “gzipped JSON”이라고만 언급해서 놓치기 쉬워요.

Amplitude Export 파일 읽기

올바른 접근 방식은 이거예요 — 바깥 레이어를 unzip하고, 안쪽 gzip을 decompress하고, newline-delimited JSON을 파싱해요:

import zipfile
import gzip
import json
import io

def read_amplitude_export(raw_data: bytes) -> list[dict]:
    # Outer layer: ZIP
    with zipfile.ZipFile(io.BytesIO(raw_data)) as zf:
        inner_file = zf.namelist()[0]

        # Inner layer: GZIP
        with zf.open(inner_file) as f:
            json_data = gzip.decompress(f.read()).decode()

    # Content: Newline-delimited JSON
    events = [json.loads(line) for line in json_data.strip().split('
')]
    return events

validation 고려사항도 있어요: 일부 hourly export는 빈 ZIP 파일이나 빈 namelist를 가진 파일을 반환해요. production 코드에서는 인덱싱 전에 zf.namelist()를 확인하고, extraction을 BadZipFile 에러에 대한 try/except로 감싸야 해요.

이벤트 구조

압축 해제된 출력의 각 줄은 하나의 이벤트를 나타내는 JSON 객체예요:

{
  "event_type": "session_end",
  "event_time": "2026-01-26 04:23:35.379000",
  "user_id": "user@example.com",
  "device_id": "6fd6899d-2b08-40e3-b723-e4ca1f848a43",
  "platform": "Web",
  "country": "South Korea",
  "city": "Suwon",
  "event_properties": {},
  "user_properties": {
    "utm_source": "longblack"
  }
}

event_time 필드는 사용자 기기에서 이벤트가 발생한 타임스탬프예요 — 파티셔닝에는 파일명의 날짜(Amplitude가 파일을 export한 시간)가 아니라 이걸 사용해야 해요.

API 엔드포인트

# Export API URL
https://amplitude.com/api/2/export?start={YYYYMMDD}T{HH}&end={YYYYMMDD}T{HH}

# Example: Get hour 10 of 2026-01-26
curl -u "API_KEY:SECRET_KEY" 
  "https://amplitude.com/api/2/export?start=20260126T10&end=20260126T11"

API는 요청당 하나의 ZIP 파일을 반환해요. 각 요청은 시간 단위로 지정된 범위를 커버해요. 응답은 위에서 설명한 ZIP > GZIP > NDJSON 중첩 형식이에요.

에러 코드

코드의미조치
200성공데이터 처리
400데이터 > 4GB건너뛰기 (더 작은 시간 범위 사용)
404데이터 없음조용한 시간대에 정상
429Rate limit 초과exponential backoff로 재시도
504서버 타임아웃로그 남기고 건너뛰기

404는 에러가 아니에요 — 해당 시간 동안 이벤트가 기록되지 않았다는 의미예요. 400은 응답이 4GB를 초과한다는 의미이므로, 시간 범위를 좁혀서 재시도하면 돼요.

이 지식을 사용할 때

이 형식은 Amplitude Export API 응답을 소비하는 모든 ETL이나 데이터 파이프라인을 구축할 때 적용돼요. Amplitude export 파일에서 gzip.decompress()가 실패하는 이유를 디버깅하거나, 저장 전 raw Amplitude 데이터의 validation 로직(빈 ZIP 처리, BadZipFile 가드)을 작성할 때도 관련돼요.

적용되지 않는 경우

  • Amplitude Batch Event Upload API — upload API는 일반 JSON을 받아요. 이 중첩 형식은 Export API 응답에만 적용돼요.
  • Amplitude SDK나 integration — Segment, mParticle, 또는 Amplitude의 자체 warehouse sync를 사용한다면, 데이터는 해당 integration의 형식으로 도착하지, 이 ZIP+GZIP 중첩이 아니에요.
  • 다른 analytics 플랫폼 — Mixpanel, Heap, PostHog 등은 각자의 export 형식이 있어요. Amplitude의 중첩을 공유한다고 가정하면 안 돼요.

핵심 요약

.json.gz 확장자는 거짓말이에요. Amplitude export 파일은 gzip 파일을 포함하는 ZIP 아카이브이고, 그 gzip 안에 newline-delimited JSON이 있어요. 파일 확장자를 신뢰하는 대신 항상 magic bytes(PK = ZIP, \x1f\x8b = GZIP)를 확인하세요. 두 레이어 모두 처리하는 extraction 파이프라인을 구축하고, 빈 파일이나 손상된 export에 대한 validation을 추가하세요.

Comments

enko