프론트엔드 GA Sentry 설정 - 100-hours-a-week/KTB4-10th-Cloud GitHub Wiki
프론트엔드 사용자 흐름과 오류를 확인하기 위해 GA, Sentry, Clarity를 다음과 같이 구분해서 사용한다.
GA
→ 주요 사용자 행동이 얼마나 발생했는지 확인
→ 사용자가 주요 흐름을 어디까지 진행했는지 확인
Sentry
→ 프론트엔드에서 발생한 Error / Exception 확인
→ 오류가 발생한 코드와 Stack Trace 확인
Clarity
→ 실제 사용자 행동과 세션 확인
→ 오류나 지연 상황에서 사용자가 어떻게 반응했는지 확인
이번 프론트엔드 작업 범위는 다음과 같다.
GA
- 로그인 이벤트
- 가이드북 생성 이벤트
Sentry
- Error Monitoring 초기화
- 개인정보 수집 최소화
Source Map
- Sentry Wizard를 이용한 기본 설정
- Sentry 업로드 후 배포 산출물의 Source Map 제거
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',
})
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 이벤트
여기는 가능하시다면 붙여주시고 아니면 패스하셔도 무방합니다.
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와 구분한다.
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은 생성 완료까지 사용자가 기다린 시간을 계산하기 위해 사용한다.
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와 연결해 보관하고 최종 상태 확인 시 차이를 계산할 수 있도록 구현한다.
Sentry는 프론트엔드 Error Monitoring 용도로 사용한다.
npm install @sentry/react
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 && 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
},
})
}
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를 초기화한다.
.env.example에 추가한다.
VITE_SENTRY_DSN=
실제 운영 DSN은 CD에서 전달한다.
Production에서 발생한 Error의 Stack Trace를 원본 TypeScript/React 코드 기준으로 확인하기 위해 Source Map을 Sentry에 업로드한다.
Source Map 설정은 직접 처음부터 작성하지 않고 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 업로드 설정
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 원본 코드로 표시되는지 확인