Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
36589e1
refactor(analytics): unify GA4 module with taxonomy naming
ArticPenguin Apr 27, 2026
a3da33f
refactor(app): update GA4 comment to reflect lifecycle events
ArticPenguin Apr 27, 2026
7cbf159
refactor(settings): use auth and settings taxonomy events
ArticPenguin Apr 27, 2026
62c155c
refactor(main-layout): use sendSearchSubmit for header search
ArticPenguin Apr 27, 2026
aa3db6a
feat(analytics): add new taxonomy event helpers
ArticPenguin Apr 27, 2026
76a424b
feat(template-editor): add template lifecycle GA4 events
ArticPenguin Apr 27, 2026
89697a9
feat(auth): add login/verification result GA4 events
ArticPenguin Apr 27, 2026
42f51af
feat(alerts): add alerts GA4 events
ArticPenguin Apr 27, 2026
34657e5
feat(todo): add todo GA4 events
ArticPenguin Apr 27, 2026
4bacea8
feat(gallery): add gallery GA4 events
ArticPenguin Apr 27, 2026
982e1fc
fix(analytics): connect missing sendTemplateItemAdd and sendSystemErr…
ArticPenguin Apr 27, 2026
166e38f
fix(gallery): include sort in search debounce effect deps
ArticPenguin Apr 27, 2026
7601a52
fix(todo): isolate view_open analytics from fetch loop
ArticPenguin Apr 27, 2026
ed7c9cc
fix(alerts): classify department alerts by category whitelist
ArticPenguin Apr 27, 2026
47e25f1
refactor(analytics): align cloneFail with other fail helpers
ArticPenguin Apr 27, 2026
aec8312
refactor(analytics): remove sendGAEvent export and improve inline com…
ArticPenguin Apr 27, 2026
2ad373d
fix(analytics): route all events to production endpoint for DebugView
ArticPenguin Apr 27, 2026
666cfb6
docs(taxonomy): sync GA4-Data-Taxonomy with current implementation
ArticPenguin Apr 27, 2026
d428c9f
docs(taxonomy): remove extension_close from lifecycle events
ArticPenguin Apr 27, 2026
897efb1
fix(analytics): remove response.json() call on production endpoint
ArticPenguin Apr 27, 2026
cae17c6
feat(analytics): add remaining taxonomy event helpers
ArticPenguin Apr 27, 2026
3917fda
feat(analytics): connect banner, settings_open, template create/delet…
ArticPenguin Apr 27, 2026
89f5e88
feat(analytics): connect template editor item events
ArticPenguin Apr 27, 2026
ad2675e
feat(analytics): connect labs and alerts subscription events
ArticPenguin Apr 27, 2026
ce13ad4
docs(taxonomy): mark all remaining events as implemented
ArticPenguin Apr 27, 2026
db97c0b
docs(taxonomy): add full event reference table (47 events)
ArticPenguin Apr 27, 2026
f10a01a
refactor(analytics): apply taxonomy convention, restore legacy events…
ArticPenguin Apr 28, 2026
757357a
docs(taxonomy): update event reference tables for new MP_ naming conv…
ArticPenguin Apr 28, 2026
a895b9c
fix: address GA4 review feedback
ArticPenguin Apr 28, 2026
4c8cc50
fix: address GA4 taxonomy review
ArticPenguin Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions docs/GA4-Data-Taxonomy.md

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@ import { Outlet } from "react-router-dom";
import { ErrorBoundary } from "react-error-boundary";
import { Toaster } from "./components/ui/sonner";
import { PostedTemplatesProvider } from "./contexts/PostedTemplatesContext";
import { sendPageView } from "./utils/analytics";
import { sendExtensionOpen, sendPageView, sendError } from "./utils/analytics";
import { debugLog } from "@/utils/logger";
import "./App.css";

function App() {
// Google Analytics: Extension 열릴 때 페이지뷰 전송
// GA4: popup mount 시 first_open / session_start / extension_open 자동 전송
useEffect(() => {
debugLog(
"%c여길 열어보시다니...\n이 참에 직접 코드 기여도 해주시는 건 어떤가요?",
"font-family: Nanum Gothic; color: darkgreen; padding: 6px; border-radius: 4px; font-size:14px",
);
debugLog("https://github.com/Turtle-Hwan/LinKU");
sendPageView("LinKU Extension - Popup", "chrome-extension://linku/popup");
sendExtensionOpen("popup_home", "popup");
sendPageView("LinKU Extension - Popup");
}, []);

return (
<ErrorBoundary
onError={(error: unknown) => {
const msg = error instanceof Error ? error.message : String(error);
sendError("react_error_boundary", msg, "popup_home");
}}
fallback={
<div className="w-[500px] h-[600px] flex items-center justify-center p-8">
<div className="text-center space-y-4">
Expand Down
45 changes: 25 additions & 20 deletions src/components/Editor/EditorHeader/EditorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ import {
import { getTemplate } from '@/apis/templates';
import { areTemplatesEqual } from '@/utils/templateUtils';
import { debugLog, errorLog } from '@/utils/logger';
import {
sendTemplateSaveSuccess,
sendTemplateSaveFail,
sendTemplateSyncSuccess,
sendTemplateSyncFail,
sendTemplatePublishSuccess,
sendTemplatePublishFail,
} from '@/utils/analytics';

export const EditorHeader = () => {
const { state, dispatch } = useEditorContext();
const { syncToServer } = useTemplateSync();
const { publishTemplate } = useTemplatePublish();
const { loadPostedTemplates } = usePostedTemplates();

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch({ type: 'UPDATE_TEMPLATE_NAME', payload: e.target.value });
};
Expand Down Expand Up @@ -100,24 +107,18 @@ export const EditorHeader = () => {

dispatch({ type: 'SAVE_SUCCESS', payload: savedTemplate });

const origin = savedTemplate.cloned ? 'cloned' : 'owned';
sendTemplateSaveSuccess(savedTemplate.templateId, origin, savedTemplate.items.length);

toast.success('저장 완료', {
description: '템플릿이 로컬에 저장되었습니다.',
});
} catch (error) {
errorLog('[EditorHeader] Save failed:', error);
dispatch({
type: 'SAVE_FAILED',
payload:
error instanceof Error
? error.message
: '저장 중 오류가 발생했습니다.',
});
toast.error('저장 실패', {
description:
error instanceof Error
? error.message
: '저장 중 오류가 발생했습니다.',
});
const errMsg = error instanceof Error ? error.message : '저장 중 오류가 발생했습니다.';
dispatch({ type: 'SAVE_FAILED', payload: errMsg });
sendTemplateSaveFail(state.template.templateId, 'save_error', errMsg);
toast.error('저장 실패', { description: errMsg });
}
};

Expand Down Expand Up @@ -145,15 +146,18 @@ export const EditorHeader = () => {

if (result.success && result.data) {
dispatch({ type: 'SYNC_SUCCESS', payload: result.data });
sendTemplateSyncSuccess(
state.template.templateId,
result.data.items?.length ?? state.template.items.length
);
toast.success('동기화 완료', {
description: '템플릿이 서버에 동기화되었습니다.',
});
} else {
const errorMsg = result.error || '동기화에 실패했습니다.';
dispatch({ type: 'SYNC_FAILED', payload: errorMsg });
toast.error('동기화 실패', {
description: errorMsg,
});
sendTemplateSyncFail(state.template.templateId, 'sync_failed', errorMsg);
toast.error('동기화 실패', { description: errorMsg });
}
};

Expand All @@ -170,13 +174,14 @@ export const EditorHeader = () => {

if (result.success) {
await loadPostedTemplates();
sendTemplatePublishSuccess(state.template.templateId, currentItems.length);
toast.success('게시 완료', {
description: '템플릿이 공개 갤러리에 게시되었습니다.',
});
} else {
toast.error('게시 실패', {
description: result.error || '게시에 실패했습니다.',
});
const errMsg = result.error || '게시에 실패했습니다.';
sendTemplatePublishFail(state.template.templateId, 'publish_failed', errMsg);
toast.error('게시 실패', { description: errMsg });
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { validateLinkForm } from '@/utils/formValidation';
import { IconGrid } from '@/components/Editor/shared/IconGrid';
import type { TemplateIcon, TemplateItem } from '@/types/api';
import { InputGroup } from '@/components/Editor/shared/InputGroup';
import { sendTemplateItemAdd, sendTemplateItemUpdate, sendTemplateItemDelete } from '@/utils/analytics';

export const ItemPropertiesPanel = () => {
const { state, dispatch } = useEditorContext();
Expand Down Expand Up @@ -48,6 +49,7 @@ export const ItemPropertiesPanel = () => {
key={selectedItem.templateItemId}
selectedItem={selectedItem}
isFromStaging={isFromStaging}
templateId={state.template?.templateId}
defaultIcons={state.defaultIcons}
userIcons={state.userIcons}
dispatch={dispatch}
Expand All @@ -58,6 +60,7 @@ export const ItemPropertiesPanel = () => {
interface ItemPropertiesPanelFormProps {
selectedItem: TemplateItem;
isFromStaging: boolean;
templateId: number | undefined;
defaultIcons: ReturnType<typeof useEditorContext>['state']['defaultIcons'];
userIcons: ReturnType<typeof useEditorContext>['state']['userIcons'];
dispatch: ReturnType<typeof useEditorContext>['dispatch'];
Expand All @@ -66,6 +69,7 @@ interface ItemPropertiesPanelFormProps {
const ItemPropertiesPanelForm = ({
selectedItem,
isFromStaging,
templateId,
defaultIcons,
userIcons,
dispatch,
Expand Down Expand Up @@ -138,24 +142,28 @@ const ItemPropertiesPanelForm = ({
},
});

sendTemplateItemUpdate('properties', templateId);
toast.success('변경사항이 저장되었습니다.');
};

const handleDelete = () => {
if (isFromStaging) {
// Permanently delete from staging
dispatch({ type: 'REMOVE_FROM_STAGING', payload: selectedItem.templateItemId });
sendTemplateItemDelete('staging', templateId);
toast.info('아이템이 영구 삭제되었습니다.');
} else {
// Move canvas item to staging
dispatch({ type: 'MOVE_TO_STAGING', payload: selectedItem.templateItemId });
sendTemplateItemDelete('canvas', templateId);
toast.info('아이템이 임시 저장 공간으로 이동되었습니다.');
}
};

const handleMoveToCanvas = () => {
if (!isFromStaging) return;
dispatch({ type: 'MOVE_TO_CANVAS', payload: selectedItem.templateItemId });
sendTemplateItemAdd('button', templateId);
toast.success('아이템이 캔버스에 추가되었습니다.');
};

Expand Down
9 changes: 8 additions & 1 deletion src/components/EmailVerificationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 건국대 이메일 인증 다이얼로그
*/

import { useState } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Mail, ArrowLeft, Loader2 } from 'lucide-react';
import {
Expand All @@ -22,6 +22,7 @@ import {
validateAuthCode,
} from '@/utils/formValidation';
import { errorLog } from '@/utils/logger';
import { sendAuthEmailVerificationStart, sendAuthEmailVerificationSuccess } from '@/utils/analytics';

interface EmailVerificationDialogProps {
open: boolean;
Expand All @@ -43,6 +44,11 @@ export function EmailVerificationDialog({
const [authCode, setAuthCode] = useState('');
const [isLoading, setIsLoading] = useState(false);

// 다이얼로그가 열릴 때 인증 시작 이벤트 전송
useEffect(() => {
if (open) sendAuthEmailVerificationStart('settings_dialog');
}, [open]);

// Full email address
const kuMail = emailId ? `${emailId}${EMAIL_DOMAIN}` : '';

Expand Down Expand Up @@ -102,6 +108,7 @@ export function EmailVerificationDialog({
toast.success('이메일 인증이 완료되었습니다!');
// Store verified email
await chrome.storage.local.set({ kuMail });
sendAuthEmailVerificationSuccess('konkuk.ac.kr');
// Trigger re-login to get member token
onVerificationComplete();
handleClose();
Expand Down
2 changes: 2 additions & 0 deletions src/components/Labs/LibrarySeatSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@/apis';
import { LibrarySeatRoom } from '@/types/api';
import { loadECampusCredentials } from '@/utils/credentials';
import { sendLabsFeatureUse } from '@/utils/analytics';

const LibrarySeatSection = () => {
const [rooms, setRooms] = useState<LibrarySeatRoom[]>([]);
Expand Down Expand Up @@ -75,6 +76,7 @@ const LibrarySeatSection = () => {
}, [fetchSeatRooms]);

const handleOpenRoom = (roomId: number) => {
sendLabsFeatureUse('library_seat', 'success');
openLibraryReservationPage(roomId);
};

Expand Down
3 changes: 3 additions & 0 deletions src/components/Labs/QRGeneratorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
import { Info, Download, Check, Upload, X } from "lucide-react";
import QRCode from "qrcode";
import { warnLog } from '@/utils/logger';
import { sendLabsFeatureUse } from '@/utils/analytics';

// LinKU 로고 (public/assets/icon128.png) - 고해상도 사용
const LINKU_LOGO_URL = "/assets/icon128.png";
Expand Down Expand Up @@ -120,9 +121,11 @@ const QRGeneratorSection = () => {
const dataUrl = await generateQRWithLogo(activeUrl, logoSrc);
setQrDataUrl(dataUrl);
setError("");
sendLabsFeatureUse('qr_generator', 'success');
} catch {
setError("QR 코드 생성에 실패했습니다");
setQrDataUrl("");
sendLabsFeatureUse('qr_generator', 'fail');
} finally {
setIsGenerating(false);
}
Expand Down
6 changes: 6 additions & 0 deletions src/components/LabsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import {
Dialog,
DialogContent,
Expand All @@ -9,13 +10,18 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import ServerClockSection from "./Labs/ServerClockSection";
import QRGeneratorSection from "./Labs/QRGeneratorSection";
import LibrarySeatSection from "./Labs/LibrarySeatSection";
import { sendLabsOpen } from "@/utils/analytics";

interface LabsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

const LabsDialog = ({ open, onOpenChange }: LabsDialogProps) => {
useEffect(() => {
if (open) sendLabsOpen();
}, [open]);

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
Expand Down
12 changes: 3 additions & 9 deletions src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Input } from "./ui/input";
import { Search, Settings, FlaskConical } from "lucide-react";
import SettingsDialog from "./SettingsDialog";
import LabsDialog from "./LabsDialog";
import { sendButtonClick, sendGAEvent } from "@/utils/analytics";
import { sendButtonClick, sendSearchSubmit } from "@/utils/analytics";

const MainLayout = () => {
return (
Expand Down Expand Up @@ -41,10 +41,7 @@ const Header = () => {
onChange={(e) => setText((e.target as HTMLInputElement).value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
sendGAEvent("search", {
search_term: text,
search_location: "header"
});
sendSearchSubmit(text, "header");
window.open(
`https://search.konkuk.ac.kr/main.do?keyword=${text}`
);
Expand All @@ -56,10 +53,7 @@ const Header = () => {
<div className="flex items-center gap-2 shrink-0">
<FlaskConical
className="w-5 h-5 text-gray-600 cursor-pointer"
onClick={() => {
sendButtonClick("labs_icon", "header");
setShowLabs(true);
}}
onClick={() => setShowLabs(true)}
/>
<Settings
className="w-5 h-5 text-gray-600 cursor-pointer"
Expand Down
Loading
Loading