Skip to content

chore: GA4 이벤트 트래킹 작업#46

Merged
kangdy25 merged 3 commits intomainfrom
chore/ga4-event
Mar 1, 2026
Merged

chore: GA4 이벤트 트래킹 작업#46
kangdy25 merged 3 commits intomainfrom
chore/ga4-event

Conversation

@kangdy25
Copy link
Collaborator

@kangdy25 kangdy25 commented Mar 1, 2026

🚀 chore: GA4 이벤트 트래킹 작업

📝 변경사항

  • 링크 공유 GA4 이벤트 트래킹 추가
  • 참석자 정보 등록 GA4 이벤트 트래킹 추가
  • 출발역 등록 GA4 이벤트 트래킹 추가

✅ 체크리스트

  • 코드 리뷰를 받았습니다
  • 테스트를 완료했습니다
  • 린터 에러가 없습니다
  • 타입 에러가 없습니다
  • 브라우저에서 테스트를 완료했습니다
  • 모바일에서 테스트를 완료했습니다 (해당되는 경우)

📸 스크린샷

UI 변경 사항이 있다면 이미지를 드래그해서 넣어주세요!

💬 리뷰어 전달사항

  • 리뷰어가 특별히 확인해야 할 사항이 있다면 적어주세요.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 앱 전반에 걸쳐 사용자 상호작용 분석 기능 강화 (미팅 생성, 공유, 참가자 등록, 결과 공유 등)
  • Style

    • UI 스타일 일관화 (border radius 조정)

@kangdy25 kangdy25 requested a review from kim3360 March 1, 2026 14:32
@vercel
Copy link

vercel bot commented Mar 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mingling-frontend Ready Ready Preview, Comment Mar 1, 2026 2:32pm

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

개요

GA4 이벤트 추적 기능을 앱 전체에 추가했습니다. 미팅 생성, 공유, 출발지 제출, 결과 공유, 사용자 등록, 클립보드 복사 시 GA4 이벤트를 전송하며, localStorage에서 browser_id와 호스트 플래그를 관리합니다.

변경 사항

코호트 / 파일 요약
미팅 생성 시 호스트 플래그 저장
app/create/page.tsx
미팅 생성 시 is_host_${meetingId} localStorage 엔트리를 추가하여 호스팅 브라우저를 표시합니다.
미팅 페이지 GA4 추적
app/meeting/[id]/page.tsx
공유 버튼 클릭, 초대 버튼 클릭, 출발지 제출 시 GA4 이벤트(share_link, departure_location_submitted)를 전송합니다. localStorage에서 browser_id와 호스트 플래그를 조회하여 역할 정보를 포함합니다.
결과 페이지 공유 추적 및 스타일 조정
app/result/[id]/page.tsx
결과 공유 클릭 시 GA4 이벤트를 전송하고, 다중 결과 항목과 버튼의 border-radius 스타일을 조정합니다.
참가자 등록 이벤트 추적
components/join/joinForm.tsx
joinForm 제출 전에 "participant_registration" GA4 이벤트를 전송하며, browser_id, 역할, 미팅 ID를 포함합니다.
클립보드 복사 이벤트 추적
components/share/shareContent.tsx
복사 버튼 클릭 시 GA4 이벤트를 전송하는 handleCopyClickWithGA 래퍼 함수를 추가합니다.

예상 코드 리뷰 시간

🎯 3 (보통) | ⏱️ ~20분

관련 가능성이 있는 PR

추천 리뷰어

  • kim3360
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 GA4 이벤트 트래킹 작업이라는 실제 변경사항을 명확하게 요약하고 있으며, 변경 내용과 일치합니다.
Description check ✅ Passed PR 설명은 템플릿의 주요 섹션을 포함하고 있으며, 변경사항, 체크리스트를 완벽하게 작성했습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/ga4-event

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/create/page.tsx (1)

147-166: ⚠️ Potential issue | 🟠 Major

GA/로컬스토리지 예외가 모임 생성 성공 흐름을 실패로 오인시킬 수 있습니다.

Line 139에서 모임 생성이 이미 성공했더라도, Line 149~166에서 예외가 나면 Line 192 catch로 진입해 사용자에게 실패 토스트를 보여줍니다. 분석 로직은 비즈니스 성공 흐름을 깨지 않도록 분리해 주세요.

🔧 제안 수정안
       if (result.success && result.data?.meetingId) {
         const { meetingId } = result.data;
         console.log('생성된 ID:', meetingId);

-        if (typeof window !== 'undefined') {
-          let browserId = localStorage.getItem('browser_id');
-          if (!browserId) {
-            const randomStr = Math.random().toString(36).substring(2, 15);
-            browserId = `bid_${randomStr}${Date.now().toString(36)}`;
-            localStorage.setItem('browser_id', browserId);
-          }
-
-          localStorage.setItem(`is_host_${meetingId}`, 'true');
-
-          sendGAEvent('event', 'url_created', {
-            meeting_url_id: meetingId,
-            participant_count_expected: capacity,
-            browser_id: browserId,
-            entry_method: 'url_direct',
-          });
-        }
+        try {
+          if (typeof window !== 'undefined') {
+            let browserId = localStorage.getItem('browser_id');
+            if (!browserId) {
+              const randomStr = Math.random().toString(36).substring(2, 15);
+              browserId = `bid_${randomStr}${Date.now().toString(36)}`;
+              localStorage.setItem('browser_id', browserId);
+            }
+
+            localStorage.setItem(`is_host_${meetingId}`, 'true');
+
+            sendGAEvent('event', 'url_created', {
+              meeting_url_id: meetingId,
+              participant_count_expected: capacity,
+              browser_id: browserId,
+              entry_method: 'url_direct',
+            });
+          }
+        } catch (analyticsError) {
+          console.warn('GA tracking skipped:', analyticsError);
+        }

         router.push(`/share/${meetingId}`);
       }

Also applies to: 192-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/create/page.tsx` around lines 147 - 166, After the meeting creation
succeeds, the localStorage writes and sendGAEvent calls (references:
localStorage.getItem, localStorage.setItem(`is_host_${meetingId}`), and
sendGAEvent('event','url_created',...)) can throw and currently bubble into the
main catch path; wrap all browser-only analytics and storage logic in its own
try/catch (or run it async without awaiting) so any exceptions are swallowed or
logged but not rethrown, and ensure you do not call the failure toast from these
errors; keep the existing typeof window guard and still perform browser-id
generation inside the isolated block so analytics failures never change the
success flow.
🧹 Nitpick comments (1)
app/meeting/[id]/page.tsx (1)

80-84: browser_id 생성/조회 로직을 헬퍼로 추출하는 게 안전합니다.

동일 로직이 두 군데에 복제되어 있어 추후 필드명/포맷 변경 시 누락 위험이 큽니다. 파일 내부 헬퍼로 통일해 주세요.

Also applies to: 140-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/meeting/`[id]/page.tsx around lines 80 - 84, Extract the duplicated
browser_id creation/lookup into a single helper (e.g., getOrCreateBrowserId)
inside this module and replace both inline blocks (the code that calls
localStorage.getItem('browser_id'), generates `bid_${...}` and calls
localStorage.setItem) with calls to that helper; ensure the helper encapsulates
the key name ('browser_id') and format logic so future key/format changes touch
only getOrCreateBrowserId and update all call sites (also replace the duplicate
at the other occurrence referenced in the review).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/meeting/`[id]/page.tsx:
- Around line 100-104: The NUDGE handler handleNudgeClick currently opens the
NUDGE modal via openModal('NUDGE', { meetingId: id }, e) but then calls
trackShareEvent(), causing the action to be recorded as a share_link; update
handleNudgeClick to send a distinct telemetry event (e.g.,
trackEvent('nudge_click') or trackShareEvent with an explicit location/name
param like trackShareEvent({ name: 'nudge', location: 'meeting_page' })) so
NUDGE actions are not counted as share_link; modify the call site in
handleNudgeClick and adjust any track* helper signatures if needed to accept an
event name or metadata.

In `@app/result/`[id]/page.tsx:
- Around line 163-181: The share handler handleResultShareClick currently
performs GA and localStorage operations before calling openModal, so any
exception (from localStorage or sendGAEvent) can prevent the modal from opening;
change the flow to guarantee openModal is invoked regardless of analytics errors
by either calling openModal first or wrapping the analytics/localStorage block
in a try/catch and treating sendGAEvent/localStorage as best-effort (log errors
but do not rethrow), keeping references to sendGAEvent,
localStorage.getItem/setItem and openModal so you update those exact calls in
handleResultShareClick.

In `@components/join/joinForm.tsx`:
- Around line 69-90: Move the sendGAEvent call out of the pre-API block and
instead invoke it only after the participant registration API returns success;
locate the sendGAEvent invocation in joinForm.tsx (it uses meetingId, browserId,
isRemembered and role computed from
localStorage.getItem(`is_host_${meetingId}`)) and relocate it to the success
branch of the existing participation API call (the block currently labeled "기존
참여 API 호출 로직" / the registration API promise resolution). Ensure the GA call is
non-blocking and wrapped in a try/catch (or .catch) so any GA/localStorage
errors do not prevent or alter the main registration flow, and preserve the same
payload fields (meeting_url_id, user_cookie_id, role, remember_me).

---

Outside diff comments:
In `@app/create/page.tsx`:
- Around line 147-166: After the meeting creation succeeds, the localStorage
writes and sendGAEvent calls (references: localStorage.getItem,
localStorage.setItem(`is_host_${meetingId}`), and
sendGAEvent('event','url_created',...)) can throw and currently bubble into the
main catch path; wrap all browser-only analytics and storage logic in its own
try/catch (or run it async without awaiting) so any exceptions are swallowed or
logged but not rethrown, and ensure you do not call the failure toast from these
errors; keep the existing typeof window guard and still perform browser-id
generation inside the isolated block so analytics failures never change the
success flow.

---

Nitpick comments:
In `@app/meeting/`[id]/page.tsx:
- Around line 80-84: Extract the duplicated browser_id creation/lookup into a
single helper (e.g., getOrCreateBrowserId) inside this module and replace both
inline blocks (the code that calls localStorage.getItem('browser_id'), generates
`bid_${...}` and calls localStorage.setItem) with calls to that helper; ensure
the helper encapsulates the key name ('browser_id') and format logic so future
key/format changes touch only getOrCreateBrowserId and update all call sites
(also replace the duplicate at the other occurrence referenced in the review).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32a9f89 and b45ddf4.

📒 Files selected for processing (5)
  • app/create/page.tsx
  • app/meeting/[id]/page.tsx
  • app/result/[id]/page.tsx
  • components/join/joinForm.tsx
  • components/share/shareContent.tsx

Comment on lines +100 to 104
// 재촉하기 버튼 전용 핸들러
const handleNudgeClick = (e: React.MouseEvent<HTMLButtonElement>) => {
openModal('NUDGE', { meetingId: id }, e);
trackShareEvent();
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

재촉하기(NUDGE) 액션이 share_link로 집계됩니다.

Line 102는 NUDGE 모달 오픈인데 Line 103에서 공유 이벤트(share_link)를 전송하고 있어 공유 지표가 오염될 수 있습니다. NUDGE 전용 이벤트로 분리하거나 최소한 location/event name을 구분해 주세요.

🔧 제안 수정안
 const handleNudgeClick = (e: React.MouseEvent<HTMLButtonElement>) => {
   openModal('NUDGE', { meetingId: id }, e);
-  trackShareEvent();
+  if (typeof window !== 'undefined') {
+    let browserId = localStorage.getItem('browser_id');
+    if (!browserId) {
+      browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
+      localStorage.setItem('browser_id', browserId);
+    }
+
+    sendGAEvent('event', 'nudge_opened', {
+      meeting_url_id: id,
+      browser_id: browserId,
+      location: 'meeting_room',
+    });
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/meeting/`[id]/page.tsx around lines 100 - 104, The NUDGE handler
handleNudgeClick currently opens the NUDGE modal via openModal('NUDGE', {
meetingId: id }, e) but then calls trackShareEvent(), causing the action to be
recorded as a share_link; update handleNudgeClick to send a distinct telemetry
event (e.g., trackEvent('nudge_click') or trackShareEvent with an explicit
location/name param like trackShareEvent({ name: 'nudge', location:
'meeting_page' })) so NUDGE actions are not counted as share_link; modify the
call site in handleNudgeClick and adjust any track* helper signatures if needed
to accept an event name or metadata.

Comment on lines +163 to +181
const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// 1. GA 데이터 먼저 전송!
if (typeof window !== 'undefined') {
let browserId = localStorage.getItem('browser_id');
if (!browserId) {
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
localStorage.setItem('browser_id', browserId);
}

sendGAEvent('event', 'share_link', {
meeting_url_id: id,
location: 'place_list', // PM님 명세: 결과 리스트 페이지
browser_id: browserId,
});
}

// 2. 안전하게 전송 후 모달 띄우기
openModal('SHARE', { meetingId: id }, e);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

GA 전송 예외가 공유 모달 오픈을 막을 수 있습니다.

현재는 Line 180 openModal이 GA 전송 뒤에 있어, 전송/스토리지 예외 시 공유 버튼이 무반응처럼 보일 수 있습니다. 공유 UX는 항상 열리고 분석은 best-effort로 처리해 주세요.

🔧 제안 수정안
 const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
-  if (typeof window !== 'undefined') {
-    let browserId = localStorage.getItem('browser_id');
-    if (!browserId) {
-      browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
-      localStorage.setItem('browser_id', browserId);
-    }
-
-    sendGAEvent('event', 'share_link', {
-      meeting_url_id: id,
-      location: 'place_list',
-      browser_id: browserId,
-    });
-  }
-
-  openModal('SHARE', { meetingId: id }, e);
+  try {
+    if (typeof window !== 'undefined') {
+      let browserId = localStorage.getItem('browser_id');
+      if (!browserId) {
+        browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
+        localStorage.setItem('browser_id', browserId);
+      }
+      sendGAEvent('event', 'share_link', {
+        meeting_url_id: id,
+        location: 'place_list',
+        browser_id: browserId,
+      });
+    }
+  } catch (analyticsError) {
+    console.warn('GA tracking skipped:', analyticsError);
+  } finally {
+    openModal('SHARE', { meetingId: id }, e);
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// 1. GA 데이터 먼저 전송!
if (typeof window !== 'undefined') {
let browserId = localStorage.getItem('browser_id');
if (!browserId) {
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
localStorage.setItem('browser_id', browserId);
}
sendGAEvent('event', 'share_link', {
meeting_url_id: id,
location: 'place_list', // PM님 명세: 결과 리스트 페이지
browser_id: browserId,
});
}
// 2. 안전하게 전송 후 모달 띄우기
openModal('SHARE', { meetingId: id }, e);
};
const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
try {
if (typeof window !== 'undefined') {
let browserId = localStorage.getItem('browser_id');
if (!browserId) {
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
localStorage.setItem('browser_id', browserId);
}
sendGAEvent('event', 'share_link', {
meeting_url_id: id,
location: 'place_list',
browser_id: browserId,
});
}
} catch (analyticsError) {
console.warn('GA tracking skipped:', analyticsError);
} finally {
openModal('SHARE', { meetingId: id }, e);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/result/`[id]/page.tsx around lines 163 - 181, The share handler
handleResultShareClick currently performs GA and localStorage operations before
calling openModal, so any exception (from localStorage or sendGAEvent) can
prevent the modal from opening; change the flow to guarantee openModal is
invoked regardless of analytics errors by either calling openModal first or
wrapping the analytics/localStorage block in a try/catch and treating
sendGAEvent/localStorage as best-effort (log errors but do not rethrow), keeping
references to sendGAEvent, localStorage.getItem/setItem and openModal so you
update those exact calls in handleResultShareClick.

Comment on lines +69 to +90
// ⭐️ 1. 비즈니스 로직 실행 전 GA 이벤트 선(先) 전송!
if (typeof window !== 'undefined') {
let browserId = localStorage.getItem('browser_id');
if (!browserId) {
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
localStorage.setItem('browser_id', browserId);
}

// ⭐️ 개발자님의 완벽한 엣지케이스 방어 로직!
// 로컬스토리지에 '이 방의 생성자(방장)'라는 징표가 있는지 확인
const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
const userRole = isHost ? 'host' : 'participant';

sendGAEvent('event', 'participant_registration', {
meeting_url_id: meetingId,
user_cookie_id: browserId, // 생성 때 썼던 그 browserId와 일치하게 됨!
role: userRole, // 완벽하게 방장/참여자 구분 완료
remember_me: isRemembered ? 'yes' : 'no',
});
}

// ⭐️ 2. 기존 참여 API 호출 로직
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

등록 성공 이벤트를 API 호출 전에 전송하면 지표 왜곡과 기능 차단 위험이 있습니다.

현재 구조는 (1) 실제 등록 실패도 participant_registration으로 집계될 수 있고, (2) GA/로컬스토리지 예외 시 참여 API 호출이 스킵될 수 있습니다. 성공 이후 비차단 방식으로 보내는 쪽이 안전합니다.

🔧 제안 수정안
-    if (typeof window !== 'undefined') {
-      let browserId = localStorage.getItem('browser_id');
-      if (!browserId) {
-        browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
-        localStorage.setItem('browser_id', browserId);
-      }
-
-      const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
-      const userRole = isHost ? 'host' : 'participant';
-
-      sendGAEvent('event', 'participant_registration', {
-        meeting_url_id: meetingId,
-        user_cookie_id: browserId,
-        role: userRole,
-        remember_me: isRemembered ? 'yes' : 'no',
-      });
-    }
-
     try {
       // `@ts-ignore`
       const result = await participantEnter.mutateAsync({
         meetingId,
         data: {
           userId: name,
           password,
         },
       });

       if (result.success) {
+        try {
+          if (typeof window !== 'undefined') {
+            let browserId = localStorage.getItem('browser_id');
+            if (!browserId) {
+              browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
+              localStorage.setItem('browser_id', browserId);
+            }
+            const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
+            const userRole = isHost ? 'host' : 'participant';
+            sendGAEvent('event', 'participant_registration', {
+              meeting_url_id: meetingId,
+              user_cookie_id: browserId,
+              role: userRole,
+              remember_me: isRemembered ? 'yes' : 'no',
+            });
+          }
+        } catch (analyticsError) {
+          console.warn('GA tracking skipped:', analyticsError);
+        }
         setMeetingUserId(meetingId, name, isRemembered);
         router.push(`/meeting/${meetingId}`);
       } else {

Also applies to: 91-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/join/joinForm.tsx` around lines 69 - 90, Move the sendGAEvent call
out of the pre-API block and instead invoke it only after the participant
registration API returns success; locate the sendGAEvent invocation in
joinForm.tsx (it uses meetingId, browserId, isRemembered and role computed from
localStorage.getItem(`is_host_${meetingId}`)) and relocate it to the success
branch of the existing participation API call (the block currently labeled "기존
참여 API 호출 로직" / the registration API promise resolution). Ensure the GA call is
non-blocking and wrapped in a try/catch (or .catch) so any GA/localStorage
errors do not prevent or alter the main registration flow, and preserve the same
payload fields (meeting_url_id, user_cookie_id, role, remember_me).

@kangdy25 kangdy25 merged commit e96a305 into main Mar 1, 2026
4 checks passed
@kangdy25 kangdy25 deleted the chore/ga4-event branch March 1, 2026 15:25
@coderabbitai coderabbitai bot mentioned this pull request Mar 1, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants