Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
개요GA4 이벤트 추적 기능을 앱 전체에 추가했습니다. 미팅 생성, 공유, 출발지 제출, 결과 공유, 사용자 등록, 클립보드 복사 시 GA4 이벤트를 전송하며, localStorage에서 browser_id와 호스트 플래그를 관리합니다. 변경 사항
예상 코드 리뷰 시간🎯 3 (보통) | ⏱️ ~20분 관련 가능성이 있는 PR
추천 리뷰어
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorGA/로컬스토리지 예외가 모임 생성 성공 흐름을 실패로 오인시킬 수 있습니다.
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
📒 Files selected for processing (5)
app/create/page.tsxapp/meeting/[id]/page.tsxapp/result/[id]/page.tsxcomponents/join/joinForm.tsxcomponents/share/shareContent.tsx
| // 재촉하기 버튼 전용 핸들러 | ||
| const handleNudgeClick = (e: React.MouseEvent<HTMLButtonElement>) => { | ||
| openModal('NUDGE', { meetingId: id }, e); | ||
| trackShareEvent(); | ||
| }; |
There was a problem hiding this comment.
재촉하기(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.
| 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); | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| // ⭐️ 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 호출 로직 |
There was a problem hiding this comment.
등록 성공 이벤트를 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).
🚀 chore: GA4 이벤트 트래킹 작업
📝 변경사항
✅ 체크리스트
📸 스크린샷
💬 리뷰어 전달사항
Summary by CodeRabbit
릴리스 노트
New Features
Style