-
Notifications
You must be signed in to change notification settings - Fork 1
chore: GA4 이벤트 트래킹 작업 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { getMeetingUserId } from '@/lib/storage'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useQueryClient } from '@tanstack/react-query'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Loading from '@/components/loading/loading'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getRandomHexColor } from '@/lib/color'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { sendGAEvent } from '@next/third-parties/google'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function Page() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -159,6 +160,26 @@ export default function Page() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.replace(`/meeting/${id}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+163
to
+181
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GA 전송 예외가 공유 모달 오픈을 막을 수 있습니다. 현재는 Line 180 🔧 제안 수정안 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearRelatedCache(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -266,7 +287,7 @@ export default function Page() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={result.id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setSelectedResultId(result.id)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`flex cursor-pointer flex-col gap-3.75 rounded-[4px] border bg-white p-5 ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`flex cursor-pointer flex-col gap-3.75 rounded border bg-white p-5 ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedResultId === result.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'border-blue-5 border-2' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'border-gray-2 hover:bg-gray-1' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -296,7 +317,7 @@ export default function Page() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleRecommendClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-gray-8 hover:bg-gray-9 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal text-white transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-gray-8 hover:bg-gray-9 h-10 flex-1 cursor-pointer rounded text-[15px] font-normal text-white transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 주변 장소 추천 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -314,7 +335,7 @@ export default function Page() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-gray-1 hover:bg-gray-2 text-blue-5 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-gray-1 hover:bg-gray-2 text-blue-5 h-10 flex-1 cursor-pointer rounded text-[15px] font-normal transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 환승 경로 보기 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -328,7 +349,7 @@ export default function Page() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={(e) => openModal('SHARE', { meetingId: id }, e)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleResultShareClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="bg-blue-5 hover:bg-blue-8 absolute right-5 bottom-0 left-5 flex h-12 items-center justify-center gap-2.5 rounded text-lg font-semibold text-white transition-transform active:scale-[0.98] md:right-0 md:left-0" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Image src="/icon/share-white.svg" alt="공유 아이콘" width={20} height={20} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import { setMeetingUserId } from '@/lib/storage'; | ||
| import Image from 'next/image'; | ||
| import { getRandomHexColor } from '@/lib/color'; | ||
| import { sendGAEvent } from '@next/third-parties/google'; | ||
|
|
||
| interface JoinFormProps { | ||
| meetingId: string; | ||
|
|
@@ -65,6 +66,28 @@ | |
| e.preventDefault(); | ||
| if (!isFormValid || !meetingId) return; | ||
|
|
||
| // ⭐️ 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 호출 로직 | ||
|
Comment on lines
+69
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 등록 성공 이벤트를 API 호출 전에 전송하면 지표 왜곡과 기능 차단 위험이 있습니다. 현재 구조는 (1) 실제 등록 실패도 🔧 제안 수정안- 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 |
||
| try { | ||
| // @ts-ignore | ||
| const result = await participantEnter.mutateAsync({ | ||
|
|
@@ -82,7 +105,7 @@ | |
| setErrorMessage(result.data?.message || '모임 참여에 실패했습니다. 다시 시도해주세요.'); | ||
| show(); | ||
| } | ||
| } catch (error: any) { | ||
| const errorData = error.data || error.response?.data; | ||
| setErrorMessage(errorData?.message || '모임 참여에 실패했습니다. 다시 시도해주세요.'); | ||
| show(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
재촉하기(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