프론트엔드 GA Sentry 설정 - 100-hours-a-week/KTB4-10th-Cloud GitHub Wiki

0. 목적

프론트엔드 사용자 흐름과 오류를 확인하기 위해 GA, Sentry, Clarity를 다음과 같이 구분해서 사용한다.

GA
→ 주요 사용자 행동이 얼마나 발생했는지 확인
→ 사용자가 주요 흐름을 어디까지 진행했는지 확인

Sentry → 프론트엔드에서 발생한 Error / Exception 확인 → 오류가 발생한 코드와 Stack Trace 확인

Clarity → 실제 사용자 행동과 세션 확인 → 오류나 지연 상황에서 사용자가 어떻게 반응했는지 확인

이번 프론트엔드 작업 범위는 다음과 같다.

GA
  1. 로그인 이벤트
  2. 가이드북 생성 이벤트

Sentry

  1. Error Monitoring 초기화
  2. 개인정보 수집 최소화

Source Map

  1. Sentry Wizard를 이용한 기본 설정
  2. Sentry 업로드 후 배포 산출물의 Source Map 제거

1. GA

1-1. 공통 함수

생성

src/shared/lib/analytics.ts
type AnalyticsParams = Record<string, string | number | boolean>

declare global { interface Window { gtag?: ( command: 'event', eventName: string, params?: AnalyticsParams, ) => void } }

export function trackEvent( eventName: string, params?: AnalyticsParams, ): void { if (!import.meta.env.PROD) return

window.gtag?.('event', eventName, params) }

개발 환경의 테스트 이벤트가 운영 GA 데이터에 포함되지 않도록 Production에서만 전송한다.

사용 예시:

trackEvent('login', {
method: 'kakao',
})

2. 로그인 이벤트

이벤트

login
method: kakao

수정

src/features/auth/ui/AuthCompletePage.tsx

로그인 버튼을 클릭한 시점이 아니라 OAuth 인증이 실제로 완료된 시점에 기록한다.

import { trackEvent } from '../../../shared/lib/analytics.ts'

기존 로그인 완료 처리 부분에 추가한다.

.then(([member, preferences]) => {
const needsOnboarding =
member.status === 'ONBOARDING' || preferences.length === 0

if (active) { trackEvent('login', { method: 'kakao', })

navigate(
  needsOnboarding ? routes.preferences : routes.map,
  { replace: true },
)

} })

흐름:

로그인 버튼 클릭
↓
Kakao OAuth
↓
인증 완료
↓
login 이벤트

3. 가이드북 생성 이벤트 ( 선택 )

여기는 가능하시다면 붙여주시고 아니면 패스하셔도 무방합니다.

3-1. 이벤트

guidebook_generate_start
guidebook_generate_accepted
guidebook_generate_success
guidebook_generate_fail
이벤트 기록 시점
guidebook_generate_start 실제 생성 요청 시작
guidebook_generate_accepted 백엔드가 생성 작업을 정상 접수
guidebook_generate_success 생성 작업이 COMPLETED
guidebook_generate_fail 생성 작업이 FAILED

PENDING, PROCESSING은 Polling 과정에서 반복 조회되는 상태이므로 GA 이벤트로 기록하지 않는다.

전체 흐름:

generate_start
      ↓
POST 요청
      ↓
202 Accepted
      ↓
generate_accepted
      ↓
PENDING / PROCESSING
      ↓
  ┌───────────┐
  ↓           ↓
COMPLETED   FAILED
  ↓           ↓
success      fail

202 Accepted는 생성 완료가 아니라 생성 작업이 정상적으로 접수됐다는 의미이므로 success와 구분한다.


3-2. 생성 시작 / 접수

수정

src/features/guidebook/ui/GuidebookCreatePage.tsx

현재 생성 요청은 응답 유실 시 동일한 Idempotency-Key로 다시 요청할 수 있으므로, 동일 요청의 재전송을 새로운 생성 시도로 기록하지 않는다.

const isNewGeneration = submission.current === null

const attempt = submission.current ?? { key: crypto.randomUUID(), body: { ...form }, startedAt: Date.now(), }

submission.current = attempt

if (isNewGeneration) { trackEvent('guidebook_generate_start') }

const next = await createBook(attempt.body, attempt.key)

trackEvent('guidebook_generate_accepted')

track(next)

추가:

import { trackEvent } from '../../../shared/lib/analytics.ts'

startedAt은 생성 완료까지 사용자가 기다린 시간을 계산하기 위해 사용한다.


3-3. 생성 성공 / 실패

수정

src/features/guidebook/model/GenerationProvider.tsx

현재 다음 요청을 통해 약 2초마다 생성 상태를 확인한다.

const next = await getJob(jobId!, controller.signal)

PENDING, PROCESSING 상태에서는 GA 이벤트를 발생시키지 않는다.

최종 상태가 확인됐을 때만 기록한다.

if (next.status === 'COMPLETED') {
trackEvent('guidebook_generate_success', {
generation_time_ms: /* 생성 시작부터 완료 확인까지의 시간 */,
})
}

if (next.status === 'FAILED') { trackEvent('guidebook_generate_fail', { generation_time_ms: /* 생성 시작부터 실패 확인까지의 시간 */, }) }

generation_time_ms는 순수 AI 처리 시간이 아니라,

생성 요청 시작
↓
백엔드 요청
↓
AI 생성
↓
Polling
↓
프론트가 COMPLETED / FAILED 최초 확인

까지의 사용자 관점 생성 대기 시간을 의미한다.

따라서 생성 시작 시각을 job_id와 연결해 보관하고 최종 상태 확인 시 차이를 계산할 수 있도록 구현한다.


4. Sentry

Sentry는 프론트엔드 Error Monitoring 용도로 사용한다.


4-1. Sentry SDK 설치

npm install @sentry/react

4-2. Sentry 초기화

생성

src/shared/lib/sentry.ts
import * as Sentry from '@sentry/react'

export function initializeSentry(): void { const dsn = import.meta.env.VITE_SENTRY_DSN?.trim()

Sentry.init({ dsn,

// Production 환경 + DSN이 존재할 때만 활성화
enabled: import.meta.env.PROD &amp;&amp; Boolean(dsn),

environment: 'production',

// 기본 사용자 식별정보 수집 비활성화
sendDefaultPii: false,

beforeSend(event) {
  // 사용자 식별정보 제거
  if (event.user) {
    delete event.user.email
    delete event.user.username
    delete event.user.ip_address
  }

  // URL Query Parameter 제거
  if (event.request?.url) {
    try {
      const url = new URL(event.request.url)
      event.request.url = `${url.origin}${url.pathname}`
    } catch {
      // URL 파싱 실패 시 원본 유지
    }
  }

  return event
},

}) }


4-3. main.tsx 연결

수정

src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './app/App.tsx'
import { initializeSentry } from './shared/lib/sentry.ts'
import './styles/global.css'

initializeSentry()

createRoot(document.getElementById('root')!).render( <StrictMode> <BrowserRouter> <App /> </BrowserRouter> </StrictMode>, )

React 앱이 렌더링되기 전에 Sentry를 초기화한다.


4-4. 환경변수

.env.example에 추가한다.

VITE_SENTRY_DSN=

실제 운영 DSN은 CD에서 전달한다.


5. Source Map 설정

Production에서 발생한 Error의 Stack Trace를 원본 TypeScript/React 코드 기준으로 확인하기 위해 Source Map을 Sentry에 업로드한다.

Source Map 설정은 직접 처음부터 작성하지 않고 Sentry Wizard를 이용해 기본 설정을 구성한다.

5-1. Sentry Wizard 실행

프로젝트 루트에서 실행한다.

npx @sentry/wizard@latest -i sourcemaps

Wizard에서 다음 작업을 진행한다.

Sentry 로그인
↓
Organization 선택
↓
kgb-front 프로젝트 선택
↓
Vite 프로젝트 확인
↓
Source Map 설정 적용

Wizard가 프로젝트 파일을 수정하므로 실행 후 변경된 파일을 확인한다.

특히 다음 항목이 적용됐는지 확인한다.

□ @sentry/vite-plugin 설치
□ vite.config.ts에 sentryVitePlugin 추가
□ Production Source Map 생성 설정
□ Sentry Organization / Project 설정
□ Source Map 업로드 설정

5-2. Source Map 배포 방지

Source Map은 Sentry의 Stack Trace 복원에는 필요하지만 실제 서비스에서는 사용자에게 제공할 필요가 없다.

따라서 다음 구조로 처리한다.

Vite Production Build
↓
JavaScript + Source Map 생성
↓
Sentry Vite Plugin
↓
Source Map → Sentry 업로드
↓
Source Map 삭제
↓
dist → S3 배포
↓
CloudFront

Wizard 실행 후 vite.config.ts를 확인한다.

import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: {
sourcemap: "hidden", // Source map generation must be turned on ("hidden", true, etc.)
},
plugins: [
// Put the Sentry Vite plugin after all other plugins
sentryVitePlugin({
org: "kgb-w6",
project: "kgb-front",
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
// As you're enabling client source maps, you probably want to delete them after they're uploaded to Sentry.
// Set the appropriate glob pattern for your output folder - some glob examples below:
filesToDeleteAfterUpload: [
".//.map",
".//public//*.map",
"./dist//client/**/*.map",
],
},
}),
],
});

따라서 최종적으로 S3에 배포되는 dist에는 Source Map이 포함되지 않도록 한다.

SENTRY_AUTH_TOKEN 값은 프론트엔드 코드나 .env.example에 작성하지 않는다.
실제 토큰은 CI/CD의 GitHub Secret으로 등록하며 배포 설정은 인프라 담당자가 진행한다.

프론트엔드에서는 Wizard 실행 후 다음 항목까지만 확인한다.

□ @sentry/vite-plugin 설치
□ vite.config.ts에 sentryVitePlugin 추가
□ build.sourcemap 설정
□ Organization: kgb-w6
□ Project: kgb-front
□ filesToDeleteAfterUpload 설정
□ 변경된 설정 파일 Commit

배포 단계에서는 인프라 담당자가 다음 항목을 확인한다.

□ SENTRY_AUTH_TOKEN GitHub Secret 등록
□ VITE_SENTRY_DSN 운영 환경변수 등록
□ Production Build 성공
□ Source Map Sentry 업로드 성공
□ Build 이후 dist에 .map 파일이 남지 않는지 확인
□ S3 / CloudFront에서 .map 파일이 노출되지 않는지 확인
□ 실제 Error 발생 시 Sentry Stack Trace가 TS/TSX 원본 코드로 표시되는지 확인

⚠️ **GitHub.com Fallback** ⚠️