GA 입문 5편 — Data API · Admin API · 외부 통합 깊이

2026-05-17Google Analytics 입문에서 운영까지

GA 입문 5편. Data API · Admin API 깊이 — runReport · batchRunReports · runPivotReport · runRealtimeReport 의 4 메서드, Dimension/Metric/DateRange/Filter/OrderBy 구조, 페이지네이션, quota, Admin API 의 Property·Stream·Custom Dimension/Metric·Audience·Conversion·Property Linking CRUD, 인증 (Service Account · OAuth), Python·Node 코드 패턴, Terraform·Slackbot·BI 도구 통합까지. UI 가 아닌 코드 자동화 자리.

📚 Google Analytics 입문에서 운영까지 · 5편 — Data API · Admin API · 외부 통합 깊이

이 글은 Google Analytics 입문에서 운영까지 시리즈 5편이에요. 4편 Event · Audience 까지 = 데이터 수집 + segment 자리. 이번 글부터는 데이터 추출 · 운영 자동화 자리.

이번 글의 범위

GA4 (구글 애널리틱스 4세대) 의 API 자동화. 두 가지 API + 각자의 자리:

API 자리
Data API report 데이터를 우리 시스템으로 추출 (BI · Slackbot · dashboard)
Admin API Property · Stream · Audience · Custom Dimension 관리 (Terraform · IaC)

UI 클릭만으로 운영 = 작은 회사 OK, 큰 회사 운영 자동화 X. 두 API 가 GA4 의 코드 인터페이스 전부.

Data API — Report 데이터 추출

4 메서드

┌────────────────────────┬────────────────────────┐
│ Method                 │ 용도                    │
├────────────────────────┼────────────────────────┤
│ runReport              │ 단일 report (가장 흔함)  │
│ batchRunReports        │ 여러 report 한 호출      │
│ runPivotReport         │ pivot table (다축 분석)  │
│ runRealtimeReport      │ 최근 30분 실시간 data    │
└────────────────────────┴────────────────────────┘

runReport 의 기본 구조

from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange, Dimension, Metric, RunReportRequest,
    Filter, FilterExpression, OrderBy
)

client = BetaAnalyticsDataClient()

request = RunReportRequest(
    property=f"properties/{property_id}",
    dimensions=[
        Dimension(name="country"),
        Dimension(name="deviceCategory"),
    ],
    metrics=[
        Metric(name="activeUsers"),
        Metric(name="sessions"),
        Metric(name="totalRevenue"),
    ],
    date_ranges=[
        DateRange(start_date="2026-04-01", end_date="2026-04-30"),
    ],
)

response = client.run_report(request)

for row in response.rows:
    country = row.dimension_values[0].value
    device = row.dimension_values[1].value
    users = row.metric_values[0].value
    sessions = row.metric_values[1].value
    revenue = row.metric_values[2].value
    print(f"{country} | {device} | {users} | {sessions} | {revenue}")

Dimension · Metric 의 카탈로그

GA4 의 모든 dimension · metric 이름 은 공식 카탈로그:

Dimension 예 (분석 축):
  - 시간:        date · hour · dateHour · ...
  - 사용자:      country · city · language · deviceCategory · ...
  - 트래픽 소스: source · medium · campaignName · ...
  - 페이지/콘텐츠: pagePath · pageTitle · screenName · ...
  - Event:       eventName · ...
  - E-commerce:  itemId · itemName · itemCategory · ...
  - Custom:      customUser:tier · customEvent:section · ...

Metric 예 (측정값):
  - 사용자:    totalUsers · activeUsers · newUsers · ...
  - 세션:      sessions · averageSessionDuration · bounceRate · ...
  - Engagement: engagedSessions · engagementRate · userEngagementDuration · ...
  - Event:    eventCount · eventCountPerUser · ...
  - E-commerce: totalRevenue · purchaseRevenue · transactions · ...
  - Conversion: conversions · conversionRate · ...

여기서 시험 함정 — Dimension 과 Metric 의 호환성. 모든 조합이 가능 X. 예 — transactionId (dimension) + bounceRate (metric) = 호환 안 됨 (트랜잭션은 bounce 와 무관). 호환 안 되면 400 error 또는 빈 데이터.

Filter 의 구조

# 1. 단일 dimension filter
request = RunReportRequest(
    ...,
    dimension_filter=FilterExpression(
        filter=Filter(
            field_name="country",
            string_filter=Filter.StringFilter(value="KR")
        )
    )
)

# 2. AND / OR 조합
request = RunReportRequest(
    ...,
    dimension_filter=FilterExpression(
        and_group=FilterExpression.AndGroup(expressions=[
            FilterExpression(filter=Filter(
                field_name="country",
                string_filter=Filter.StringFilter(value="KR")
            )),
            FilterExpression(filter=Filter(
                field_name="deviceCategory",
                string_filter=Filter.StringFilter(value="mobile")
            ))
        ])
    )
)

# 3. Metric filter (예 — revenue > 50000)
request = RunReportRequest(
    ...,
    metric_filter=FilterExpression(
        filter=Filter(
            field_name="totalRevenue",
            numeric_filter=Filter.NumericFilter(
                operation=Filter.NumericFilter.Operation.GREATER_THAN,
                value={"int64_value": 50000}
            )
        )
    )
)

dimension_filter = dimension 으로 행 줄이기, metric_filter = metric 값으로 행 줄이기.

OrderBy · Limit · Offset

request = RunReportRequest(
    ...,
    order_bys=[
        OrderBy(metric=OrderBy.MetricOrderBy(metric_name="totalRevenue"),
                desc=True),
        OrderBy(dimension=OrderBy.DimensionOrderBy(dimension_name="country"))
    ],
    limit=100,
    offset=0,
)

페이지네이션

# 1차 호출
response = client.run_report(request)
print(f"Total rows: {response.row_count}")  # 전체 행 수
print(f"This page rows: {len(response.rows)}")  # 이 호출에서 받은 행

# 다음 페이지 (limit=100 이면 offset 100, 200, 300, ...)
all_rows = []
offset = 0
limit = 100

while True:
    request.limit = limit
    request.offset = offset
    response = client.run_report(request)
    all_rows.extend(response.rows)

    if len(response.rows) < limit:
        break  # 마지막 페이지
    offset += limit

Quota — 호출 한도

Standard Property:
  - Core Tokens: 25,000 / 일
  - Realtime Tokens: 10,000 / 일
  - Properties Per Project: 50

Property 360:
  - Core Tokens: 250,000 / 일 (Standard 10배)
  - Realtime Tokens: 50,000 / 일

호출 1번이 복잡한 query여러 token 소비. 너무 복잡한 query = single call 이지만 quota 빨리 소진.

Quota header 응답에서 확인 가능:

# response.metadata 의 token 사용량
for usage in response.property_quota.tokens_per_day.consumed:
    print(f"Consumed: {usage}")

batchRunReports — 여러 report 한 호출

from google.analytics.data_v1beta.types import BatchRunReportsRequest

batch_request = BatchRunReportsRequest(
    property=f"properties/{property_id}",
    requests=[
        # Report 1: 사용자별 매출
        RunReportRequest(
            dimensions=[Dimension(name="userType")],
            metrics=[Metric(name="totalRevenue")],
            date_ranges=[DateRange(start_date="7daysAgo", end_date="today")]
        ),
        # Report 2: 국가별 사용자
        RunReportRequest(
            dimensions=[Dimension(name="country")],
            metrics=[Metric(name="activeUsers")],
            date_ranges=[DateRange(start_date="7daysAgo", end_date="today")]
        ),
        # 최대 5개 report 한 호출
    ]
)

batch_response = client.batch_run_reports(batch_request)

for report in batch_response.reports:
    print(f"Report rows: {len(report.rows)}")

용도dashboard 의 여러 위젯한 호출 로 가져옴. latency 감소 + quota token 절약.

runPivotReport — pivot table

일반 report:
  country | device | users
  ───────────────────────
  KR      | mobile | 100
  KR      | desktop| 50
  US      | mobile | 80
  US      | desktop| 30

Pivot report:
           mobile  desktop
  ───────────────────────
  KR      | 100  | 50
  US      | 80   | 30
from google.analytics.data_v1beta.types import RunPivotReportRequest, Pivot

request = RunPivotReportRequest(
    property=f"properties/{property_id}",
    dimensions=[Dimension(name="country"), Dimension(name="deviceCategory")],
    metrics=[Metric(name="activeUsers")],
    date_ranges=[DateRange(start_date="7daysAgo", end_date="today")],
    pivots=[
        Pivot(
            field_names=["country"],
            limit=10
        ),
        Pivot(
            field_names=["deviceCategory"],
            limit=5
        )
    ]
)

response = client.run_pivot_report(request)

복잡한 다축 분석 = pivot 이 효율적. 일반 report 는 행 늘어남.

runRealtimeReport — 실시간

from google.analytics.data_v1beta.types import RunRealtimeReportRequest

request = RunRealtimeReportRequest(
    property=f"properties/{property_id}",
    dimensions=[Dimension(name="country"), Dimension(name="eventName")],
    metrics=[Metric(name="activeUsers"), Metric(name="eventCount")]
)

response = client.run_realtime_report(request)
Realtime 한계:
  - 최근 30분 (Standard) · 60분 (360) 만 query 가능
  - DateRange 없음 (모든 query 가 "지금")
  - 일부 dimension/metric 제한 (custom dimension 일부 X)
  - 가장 최근 event 박힌 후 *몇 초 내* 반영

용도실시간 dashboard · 이벤트 모니터링 · 현재 활성 사용자 표시.

Admin API — 운영 자동화

Admin API 의 6 영역

1. Account · Property · Stream  (구조 관리)
2. Custom Dimension · Custom Metric  (분석 축 관리)
3. Audience  (사용자 segment CRUD)
4. Conversion event (Key Event)  (전환 표시)
5. Property Linking  (BigQuery · Google Ads · Firebase · DV360 · SA360)
6. Access Binding (alpha)  (사용자 권한)

Property · Stream 관리

from google.analytics.admin_v1beta import AnalyticsAdminServiceClient
from google.analytics.admin_v1beta.types import Property, DataStream

client = AnalyticsAdminServiceClient()

# Property 만들기
new_property = Property(
    parent="accounts/123456789",
    display_name="Production Site",
    industry_category="TECHNOLOGY",
    time_zone="Asia/Seoul",
    currency_code="KRW"
)
created = client.create_property(property=new_property)
print(f"Created: {created.name}")  # "properties/987654321"

# Web Data Stream 만들기
new_stream = DataStream(
    parent=created.name,
    type_=DataStream.DataStreamType.WEB_DATA_STREAM,
    display_name="Production Web",
    web_stream_data=DataStream.WebStreamData(
        default_uri="https://example.com"
    )
)
stream = client.create_data_stream(data_stream=new_stream, parent=created.name)
print(f"Measurement ID: {stream.web_stream_data.measurement_id}")

Custom Dimension · Metric

from google.analytics.admin_v1beta.types import CustomDimension, CustomMetric

# Custom Dimension (event scope)
custom_dim = CustomDimension(
    parameter_name="user_tier",
    display_name="User Tier",
    scope=CustomDimension.DimensionScope.USER,
    description="premium · standard · free"
)
client.create_custom_dimension(
    parent="properties/987654321",
    custom_dimension=custom_dim
)

# Custom Metric
custom_metric = CustomMetric(
    parameter_name="cart_value",
    display_name="Cart Value",
    measurement_unit=CustomMetric.MeasurementUnit.CURRENCY,
    scope=CustomMetric.MetricScope.EVENT,
    description="합계 cart 가치"
)
client.create_custom_metric(
    parent="properties/987654321",
    custom_metric=custom_metric
)

여기서 정말 중요한 시험 함정 — Custom Dimension 의 scopeevent · session · user · item (이전 글 2편 의 4 layer). 한 번 만들면 변경 불가. 삭제 후 재생성 만 가능. 코드로 만들 때 scope 신중.

Audience CRUD (생성·조회·수정·삭제)

from google.analytics.admin_v1beta.types import (
    Audience, AudienceFilterClause, AudienceSimpleFilter,
    AudienceFilterExpression, AudienceFilter
)

# 카트 이탈자 audience
audience = Audience(
    display_name="Cart Abandoners 7d",
    description="최근 7일 add_to_cart 했지만 purchase 안 함",
    membership_duration_days=30,
    filter_clauses=[
        # Include: add_to_cart
        AudienceFilterClause(
            clause_type=AudienceFilterClause.AudienceClauseType.INCLUDE,
            simple_filter=AudienceSimpleFilter(
                scope=AudienceFilterScope.AUDIENCE_FILTER_SCOPE_WITHIN_SAME_SESSION,
                filter_expression=AudienceFilterExpression(
                    dimension_or_metric_filter=AudienceFilter(
                        field_name="eventName",
                        string_filter=AudienceFilter.StringFilter(value="add_to_cart")
                    )
                )
            )
        ),
        # Exclude: purchase
        AudienceFilterClause(
            clause_type=AudienceFilterClause.AudienceClauseType.EXCLUDE_PERMANENTLY,
            simple_filter=AudienceSimpleFilter(
                scope=AudienceFilterScope.AUDIENCE_FILTER_SCOPE_ACROSS_ALL_SESSIONS,
                filter_expression=AudienceFilterExpression(
                    dimension_or_metric_filter=AudienceFilter(
                        field_name="eventName",
                        string_filter=AudienceFilter.StringFilter(value="purchase")
                    )
                )
            )
        )
    ]
)

created_audience = client.create_audience(
    parent="properties/987654321",
    audience=audience
)

UI 의 audience builder동등 기능. 단 한 번 만들면 대부분 변경 불가 (membership duration · display_name 만 변경 가능).

Conversion Event 등록

from google.analytics.admin_v1beta.types import KeyEvent

# 회원가입 = conversion (Key Event)
key_event = KeyEvent(
    event_name="sign_up",
    counting_method=KeyEvent.CountingMethod.ONCE_PER_SESSION,
    default_value=KeyEvent.DefaultValue(
        numeric_value=5000.0,
        currency_code="KRW"
    )
)
client.create_key_event(
    parent="properties/987654321",
    key_event=key_event
)

GA4 의 conversion event = 2026 부터 Key Event 로 명명. 동일 개념. — 공식 docs

Property Linking

# BigQuery Export 자동화 (6편의 핵심)
from google.analytics.admin_v1beta.types import BigQueryLink

bq_link = BigQueryLink(
    project="our-gcp-project",
    daily_export_enabled=True,
    streaming_export_enabled=True,
    export_streams=["streams/12345"],
    excluded_events=["debug_event"],
    dataset_location="asia-northeast3"  # 서울
)
client.create_big_query_link(
    parent="properties/987654321",
    big_query_link=bq_link
)

# Google Ads Linking
from google.analytics.admin_v1beta.types import GoogleAdsLink

ads_link = GoogleAdsLink(
    customer_id="1234567890",
    ads_personalization_enabled=True
)
client.create_google_ads_link(
    parent="properties/987654321",
    google_ads_link=ads_link
)

property linking = UI 의 클릭 자동화. 새 property 만들 때 동시에 BQ · Ads · Firebase linking 코드 1번.

Access Binding (alpha) — 권한 관리

from google.analytics.admin_v1alpha.types import AccessBinding

# 사용자 권한 추가
binding = AccessBinding(
    user="alice@company.com",
    roles=["predefinedRoles/read", "predefinedRoles/analyst"]
)
client.create_access_binding(
    parent="properties/987654321",
    access_binding=binding
)

알파 단계이지만 대규모 권한 관리 에 매우 유용. SAML (인증 표준) /SSO (단일 로그인) 와 결합 시 전사 권한 자동화.

인증 — Service Account vs OAuth

Service Account (서버 자동화)

사용 case:
  - 서버 cron job
  - dashboard backend
  - Slackbot
  - ETL pipeline

방식:
  1. GCP Console > Service Accounts > 새 service account 생성
  2. JSON 키 다운로드 (절대 git X)
  3. GA4 Property > Property Access Management > service account email 추가
     - Role: Viewer (Data API only) 또는 Editor (Admin API)
import os
from google.analytics.data_v1beta import BetaAnalyticsDataClient

# 환경 변수 또는 명시
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = '/path/to/service-account.json'

client = BetaAnalyticsDataClient()
# 자동 인증 — 별도 코드 X

OAuth (사용자 인증)

사용 case:
  - 사용자 본인의 GA 데이터 접근 (SaaS · 분석 도구)
  - 한 사람의 여러 property 자동 발견

방식:
  1. GCP Console > OAuth Client ID 생성
  2. Redirect URI 설정
  3. 사용자 → "Sign in with Google" → 사용자 허락
  4. OAuth token 으로 client 초기화

대부분 회사 = Service Account. OAuth = 외부 사용자 GA 접근 SaaS (구독형 소프트웨어) 만.

인증 함정

- service account JSON 을 git public 에 박음 → 즉시 GA 데이터 누출
- Property Access 에 service account email 안 추가 → 401 error
- Editor role 안 주고 Admin API 호출 → 403 error
- OAuth scope 부족 (analytics.readonly 만 → Admin API 불가)

해결 — secret 관리 (Secret Manager · Vault) + scope 정확.

Terraform 으로 GA4 관리 (IaC, Infrastructure as Code)

Terraform Provider

terraform {
  required_providers {
    googleworkspace = {
      source = "hashicorp/googleworkspace"
    }
  }
}

# 또는 별도 GA Provider (community)
provider "googleanalytics" {
  credentials = file("service-account.json")
}

Property + Stream 정의

resource "googleanalytics_property" "main" {
  account_id     = "accounts/123456789"
  display_name   = "Production Site"
  industry_category = "TECHNOLOGY"
  time_zone      = "Asia/Seoul"
  currency_code  = "KRW"
}

resource "googleanalytics_data_stream" "web" {
  property_id   = googleanalytics_property.main.name
  display_name  = "Production Web"
  type          = "WEB_DATA_STREAM"
  web_data {
    default_uri = "https://example.com"
  }
}

resource "googleanalytics_custom_dimension" "user_tier" {
  property_id    = googleanalytics_property.main.name
  parameter_name = "user_tier"
  display_name   = "User Tier"
  scope          = "USER"
}

resource "googleanalytics_audience" "cart_abandoners" {
  property_id   = googleanalytics_property.main.name
  display_name  = "Cart Abandoners 7d"
  membership_duration_days = 30
  # ... 조건
}

장점 — Property 가 git 으로 추적되고, staging · production 의 일관성이 자동으로 잡힌다. PR review 로 모든 변경을 검토할 수 있고, 재해 복구도 setup 재현으로 끝난다.

단점:

  • 일부 resource 의 Provider 미완성 (alpha 기능)
  • Audience 같은 복잡한 spec 의 HCL (HashiCorp Configuration Language) 표현 복잡

외부 통합 패턴

Pattern 1: 일 dashboard 자동화

# Cron: 매일 09:00 Seoul
import schedule
from datetime import date, timedelta

def daily_report():
    yesterday = (date.today() - timedelta(days=1)).isoformat()

    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[Dimension(name="country")],
        metrics=[
            Metric(name="activeUsers"),
            Metric(name="sessions"),
            Metric(name="totalRevenue"),
            Metric(name="conversions")
        ],
        date_ranges=[DateRange(start_date=yesterday, end_date=yesterday)]
    )

    response = client.run_report(request)

    # Slack 보내기
    slack_message = format_daily_report(response)
    post_to_slack("#analytics", slack_message)

schedule.every().day.at("09:00").do(daily_report)

Pattern 2: Slackbot — "어제 매출은?"

@bot.command("/ga")
async def ga_query(ctx, query: str):
    # query = "어제 매출" · "지난 7일 사용자" 등
    parsed = parse_natural_query(query)  # LLM 기반

    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=parsed["dimensions"],
        metrics=parsed["metrics"],
        date_ranges=parsed["date_ranges"]
    )

    response = client.run_report(request)
    return format_for_slack(response)

Pattern 3: BI 도구 (Looker · Metabase) 통합

대부분 BI 도구:
  - GA4 connector 내장
  - 내부적으로 Data API 호출
  - 우리가 직접 코드 X

코드 통합 case:
  - 회사 자체 BI dashboard
  - GA + 사내 데이터베이스 결합 view
# 우리 dashboard 의 backend API
@app.get("/api/dashboard/revenue")
async def get_revenue(start: str, end: str):
    # GA4 revenue
    ga_response = client.run_report(...)

    # 우리 DB 의 실제 매출 결합 (GA 측정 누락 검증)
    db_revenue = await db.fetch_revenue(start, end)

    return {
        "ga_revenue": parse_ga(ga_response),
        "db_revenue": db_revenue,
        "delta_percent": calculate_delta(...)
    }

Pattern 4: 이상 감지 (Anomaly Detection)

# 매시 평소와 다른 패턴 감지
def check_anomalies():
    realtime_request = RunRealtimeReportRequest(
        property=f"properties/{PROPERTY_ID}",
        metrics=[Metric(name="activeUsers")]
    )
    current = client.run_realtime_report(realtime_request)

    # 지난 7일 같은 시각 평균
    avg_last_7d = get_baseline(hour=datetime.now().hour)

    current_users = int(current.rows[0].metric_values[0].value)

    if current_users < avg_last_7d * 0.5:
        alert(f"활성 사용자 50% 이하: {current_users} vs {avg_last_7d}")
    if current_users > avg_last_7d * 2.0:
        alert(f"활성 사용자 2배 (좋은 일 또는 봇): {current_users} vs {avg_last_7d}")

Pattern 5: 회사 dashboard 의 KPI (핵심 성과 지표)

# CEO dashboard — 5초마다 refresh
async def get_company_kpis():
    batch = BatchRunReportsRequest(
        property=f"properties/{PROPERTY_ID}",
        requests=[
            # 오늘 매출
            RunReportRequest(
                metrics=[Metric(name="totalRevenue")],
                date_ranges=[DateRange(start_date="today", end_date="today")]
            ),
            # 오늘 신규 가입
            RunReportRequest(
                dimensions=[Dimension(name="eventName")],
                metrics=[Metric(name="eventCount")],
                date_ranges=[DateRange(start_date="today", end_date="today")],
                dimension_filter=event_filter("sign_up")
            ),
            # 실시간 활성 사용자
            # ... 최대 5개 report 한 호출
        ]
    )
    response = client.batch_run_reports(batch)
    return format_kpis(response)

1 batch 호출 = 5 개 KPI 동시 조회. 매번 refresh 시 5 호출 → 1 호출. quota 절약.

함정 정리

사고 1: Dimension · Metric 호환 안 됨

원인transactionId (dimension) + bounceRate (metric) 같은 논리적 불가 조합.

해결 — 공식 카탈로그의 Compatibility chart 확인. 의심 시 작은 query 로 검증 먼저.

사고 2: 페이지네이션 누락 — 데이터 잘림

원인limit=10000 만 주고 response.row_count 안 봄 → 큰 데이터 일부만 처리.

해결row_countlen(rows) 비교. 부족하면 offset 늘려 다시.

사고 3: Realtime 의 dimension 제한

원인runRealtimeReportCustom Dimension (user scope) 사용 → error 또는 빈 값.

해결 — Realtime 에서 허용 dimension 만. 공식 docs 의 Realtime-compatible 확인.

사고 4: Property Quota 소진

원인 — 너무 자주 호출 (5초마다 dashboard refresh) → quota 소진 → 429 error.

해결cache + batchRunReports + quota header 모니터.

사고 5: Service Account 권한 부족

원인 — Data API 만 박혔는데 Admin API 호출.

해결Editor role 또는 Admin role 부여. Admin API 는 Editor 이상.

사고 6: Custom Dimension scope 잘못

원인event scope 로 만든 후 user scope 이 더 적합 알아챔. 변경 불가.

해결삭제 후 재생성. 또는 처음부터 scope 신중.

사고 7: Audience 변경 시 데이터 손실

원인 — Audience 의 filter 변경 후 재생성 → 기존 audience 의 history 손실.

해결 — Audience 는 대부분 변경 불가. 새 audience 만들고 기존 audience 유지 + 광고 연결 새 audience 로 마이그.

사고 8: Property Linking 의 순서

원인 — BigQuery Linking 했지만 BQ 의 dataset 권한 부여 X → linking 보이지만 데이터 도착 X.

해결 — Linking + BQ dataset 의 IAM 권한 둘 다.

사고 9: API key 의 service account JSON git commit

원인 — 실수로 service account JSON 을 git commit + push public.

해결즉시 key 폐기 + git 히스토리 정리 (BFG · filter-repo) + Secret Manager · Vault. 이미 노출되었으면 해당 key 사용 history 감사.

사고 10: Terraform 의 audience drift

원인 — Terraform 으로 만든 audience 를 UI 에서 직접 수정 → drift (state ≠ 실제) → 다음 apply 시 복원.

해결UI 수정 금지. 모든 변경 Terraform 거쳐. drift detection job + alert.

운영 권장 패턴

Pattern 1: 환경 분리 + IaC

환경:
  - dev      → Property A
  - staging  → Property B
  - prod     → Property C

Terraform:
  - terraform/ga4/dev/main.tf
  - terraform/ga4/staging/main.tf
  - terraform/ga4/prod/main.tf

  → 동일 모듈, 다른 값
  → CI/CD: terraform plan + manual approval + apply

Pattern 2: Quota 모니터

# 매 호출 후 quota 사용량 로그
def log_quota(response):
    quota = response.property_quota
    logger.info(f"Core tokens remaining: {quota.tokens_per_day.remaining}")
    if quota.tokens_per_day.remaining < 1000:
        alert("Quota soon depleted!")

Pattern 3: cache 전략

# 같은 query 의 결과 cache
@functools.lru_cache(maxsize=128)
def get_revenue_report(start_date: str, end_date: str):
    # 같은 인자 = 같은 결과 → cache hit
    return client.run_report(...)

# Redis 사용 시 — 만료 5분
@cache_redis(ttl=300)
def get_realtime_users():
    return client.run_realtime_report(...)

Pattern 4: Error retry + backoff

import time
from google.api_core import retry

@retry.Retry(
    initial=1.0,
    maximum=60.0,
    multiplier=2.0,
    predicate=retry.if_exception_type(ResourceExhausted, ServiceUnavailable)
)
def query_with_retry(request):
    return client.run_report(request)

Pattern 5: 데이터 검증 — DB vs GA

# 매일 GA 매출 vs DB 매출 비교
async def reconcile_revenue(date_str: str):
    ga_revenue = await get_ga_revenue(date_str)
    db_revenue = await get_db_revenue(date_str)

    delta_percent = abs(ga_revenue - db_revenue) / db_revenue * 100

    if delta_percent > 5:
        alert(
            f"GA vs DB 매출 불일치 {date_str}: "
            f"GA={ga_revenue}, DB={db_revenue}, delta={delta_percent}%"
        )

GA 측정의 실제 비즈니스 데이터 vs 일치 확인측정 누락 · Consent block · ad blocker 검증.

시험 직전 한 번 더 — Data API · Admin API 함정 압축 노트

Data API 4 메서드

  • runReport — 단일 report (가장 흔함)
  • batchRunReports — 최대 5개 한 호출 (latency 절약)
  • runPivotReport — pivot table (다축 분석)
  • runRealtimeReport — 최근 30분 (Standard) · 60분 (360)

Data API 구조

  • Dimension (분석 축) + Metric (측정값) + DateRange (기간) + Filter + OrderBy + Limit + Offset
  • dimension_filter (행 줄이기) vs metric_filter (값 조건)
  • Dimension · Metric 호환 안 되면 error 또는 빈 값
  • Realtime 의 dimension 제한 (Custom Dimension 일부 X)

Quota

  • Standard Property: Core 25,000 / 일 · Realtime 10,000 / 일
  • 360: Core 250,000 / 일 (10배)
  • 한 호출이 token 여러 개 소비 (복잡한 query)
  • response.property_quota 로 사용량 확인

Admin API 6 영역

  1. Account · Property · Stream (구조 관리)
  2. Custom Dimension · Custom Metric (분석 축)
  3. Audience CRUD (대부분 변경 불가)
  4. Conversion (Key Event) (전환 표시)
  5. Property Linking (BQ · Ads · Firebase · DV360 · SA360)
  6. Access Binding (alpha — 권한)

Custom Dimension 의 scope 함정

  • event · session · user · item 4 layer
  • 한 번 만들면 scope 변경 불가
  • 삭제 후 재생성만 가능
  • 코드 자동화 시 scope 신중

Audience 의 변경 한계

  • display_name · membership_duration_days 만 변경 가능
  • filter 조건 변경 X
  • 변경 필요 시 새 audience 만들고 광고 마이그

인증

  • Service Account = 서버 자동화 (cron · backend · bot)
  • OAuth = 사용자 인증 (SaaS · 외부 GA 접근)
  • JSON key git commit 금지 (Secret Manager · Vault)
  • Property Access 에 service account email 추가 필수

권한 role

  • Viewer = Data API 만 (report 읽기)
  • Analyst = audience · key event 만들기 가능
  • Editor = Admin API 전체 + 모든 변경
  • Administrator = 사용자 권한 관리 포함

Terraform / IaC

  • Provider 일부 alpha 영역 미완성
  • staging · production 일관성 + git 추적
  • drift = UI 수정 시 발생 (UI 수정 금지)
  • terraform plan + approval + apply (CI/CD)

통합 패턴

  • 일 dashboard cron (batchRunReports + Slack)
  • Slackbot (자연어 → GA query)
  • BI 도구 (Looker · Metabase 의 GA connector)
  • 이상 감지 (Realtime + baseline)
  • KPI batch (5초 refresh의 batch 호출)

사고

  • Dimension · Metric 호환 안 됨
  • 페이지네이션 누락 (큰 데이터 잘림)
  • Realtime dimension 제한 무시
  • Quota 소진 (429 error)
  • Service Account 권한 부족
  • Custom Dimension scope 잘못 (변경 불가)
  • Audience 변경 시 history 손실
  • Property Linking 의 BQ IAM 권한 누락
  • API key git commit (즉시 폐기)
  • Terraform drift (UI 수정 금지)

공식 문서: Data API v1 · Admin API v1 에서 client library 와 API 레퍼런스를 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!