Skip to content

Commit 33e969d

Browse files
authored
Merge pull request #11 from NCTUCSUnion/dev
fix: implement analytics tracking across various components for user …
2 parents f139fd8 + edce1e5 commit 33e969d

5 files changed

Lines changed: 280 additions & 3 deletions

File tree

frontend/src/components/Navbar.vue

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
v-if="isAuthenticated && userData?.is_admin"
4242
icon="pi pi-cog"
4343
label="系統管理"
44-
@click="$router.push('/admin')"
44+
@click="handleNavigateAdmin"
4545
severity="secondary"
4646
size="small"
4747
outlined
@@ -106,7 +106,7 @@
106106
severity="secondary"
107107
size="small"
108108
outlined
109-
@click="toggleTheme"
109+
@click="handleToggleTheme"
110110
/>
111111
</div>
112112
</template>
@@ -268,6 +268,7 @@ import { useTheme } from '../utils/useTheme'
268268
import { authService } from '../api'
269269
import { useRouter } from 'vue-router'
270270
import { useToast } from 'primevue/usetoast'
271+
import { trackEvent, EVENTS } from '../utils/analytics'
271272
272273
export default {
273274
name: 'AppNavbar',
@@ -343,8 +344,17 @@ export default {
343344
},
344345
},
345346
methods: {
347+
handleToggleTheme() {
348+
trackEvent(EVENTS.TOGGLE_THEME, {
349+
from: this.isDarkTheme ? 'dark' : 'light',
350+
to: this.isDarkTheme ? 'light' : 'dark',
351+
})
352+
this.toggleTheme()
353+
},
354+
346355
openLoginDialog() {
347356
this.loginVisible = true
357+
trackEvent(EVENTS.LOGIN, { type: 'dialog-open' })
348358
},
349359
350360
async handleLocalLogin() {
@@ -366,9 +376,13 @@ export default {
366376
this.checkAuthentication()
367377
this.username = ''
368378
this.password = ''
379+
380+
trackEvent(EVENTS.LOGIN_LOCAL, { success: true })
381+
369382
await this.router.push('/archive')
370383
} catch (error) {
371384
console.error('Login failed:', error)
385+
trackEvent(EVENTS.LOGIN_LOCAL, { success: false })
372386
this.toast.add({
373387
severity: 'error',
374388
summary: '登入失敗',
@@ -382,6 +396,7 @@ export default {
382396
383397
handleOAuthLogin() {
384398
this.loginVisible = false
399+
trackEvent(EVENTS.LOGIN_OAUTH, { provider: 'NYCU' })
385400
authService.login()
386401
},
387402
@@ -411,8 +426,10 @@ export default {
411426
async handleLogout() {
412427
try {
413428
await authService.logout()
429+
trackEvent(EVENTS.LOGOUT, { success: true })
414430
} catch (error) {
415431
console.error('Logout API failed:', error)
432+
trackEvent(EVENTS.LOGOUT, { success: false })
416433
}
417434
418435
sessionStorage.removeItem('authToken')
@@ -425,12 +442,19 @@ export default {
425442
426443
handleTitleClick() {
427444
if (this.isAuthenticated) {
445+
trackEvent(EVENTS.NAVIGATE_ARCHIVE, { from: 'title-click' })
428446
this.$router.push('/archive')
429447
}
430448
},
431449
450+
handleNavigateAdmin() {
451+
trackEvent(EVENTS.NAVIGATE_ADMIN, { from: 'navbar' })
452+
this.$router.push('/admin')
453+
},
454+
432455
openIssueReportDialog() {
433456
this.issueReportVisible = true
457+
trackEvent(EVENTS.OPEN_ISSUE_REPORT)
434458
},
435459
436460
closeIssueReportDialog() {
@@ -458,6 +482,13 @@ export default {
458482
submitIssueReport() {
459483
const { type, title, description, contact } = this.issueForm
460484
485+
trackEvent(EVENTS.SUBMIT_ISSUE_REPORT, {
486+
type,
487+
hasContact: !!contact,
488+
titleLength: title.length,
489+
descriptionLength: description.length,
490+
})
491+
461492
const systemInfo = this.getSystemInfo()
462493
const issueBody = this.formatIssueBody(description, contact, systemInfo, type)
463494
const repoOwner = 'nctucsunion'

frontend/src/components/UploadArchiveDialog.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ import { useToast } from 'primevue/usetoast'
349349
import { courseService, archiveService } from '../api'
350350
import PdfPreviewModal from './PdfPreviewModal.vue'
351351
import { PDFDocument } from 'pdf-lib'
352+
import { trackEvent, EVENTS } from '../utils/analytics'
352353
353354
const props = defineProps({
354355
modelValue: {
@@ -561,6 +562,12 @@ function previewUploadFile() {
561562
const fileUrl = URL.createObjectURL(new Blob([form.value.file], { type: 'application/pdf' }))
562563
uploadPreviewUrl.value = fileUrl
563564
showUploadPreview.value = true
565+
566+
trackEvent(EVENTS.PREVIEW_ARCHIVE, {
567+
context: 'upload-dialog',
568+
fileName: form.value.filename,
569+
fileSize: form.value.file.size,
570+
})
564571
} catch (error) {
565572
console.error('Preview error:', error)
566573
uploadPreviewError.value = true

frontend/src/utils/analytics.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Analytics utility for tracking user events
3+
* Uses Umami analytics for event tracking
4+
*/
5+
6+
/**
7+
* Track a custom event
8+
* @param {string} eventName - Name of the event
9+
* @param {object} eventData - Optional event data/properties
10+
*/
11+
export const trackEvent = (eventName, eventData = {}) => {
12+
try {
13+
// Check if umami is available
14+
if (typeof window !== 'undefined' && window.umami) {
15+
window.umami.track(eventName, eventData)
16+
// console.log('Event tracked:', eventName, eventData)
17+
} else {
18+
// console.warn('Umami not loaded, event not tracked:', eventName, eventData)
19+
}
20+
} catch (error) {
21+
console.error('Error tracking event:', error, eventName, eventData)
22+
}
23+
}
24+
25+
/**
26+
* Track page view
27+
* @param {string} pageName - Name of the page
28+
*/
29+
export const trackPageView = (pageName) => {
30+
trackEvent('pageview', { page: pageName })
31+
}
32+
33+
// Pre-defined event names for consistency
34+
export const EVENTS = {
35+
// Theme events
36+
TOGGLE_THEME: 'toggle-theme',
37+
38+
// Auth events
39+
LOGIN: 'login',
40+
LOGOUT: 'logout',
41+
LOGIN_OAUTH: 'login-oauth',
42+
LOGIN_LOCAL: 'login-local',
43+
44+
// Navigation events
45+
TOGGLE_SIDEBAR: 'toggle-sidebar',
46+
NAVIGATE_HOME: 'navigate-home',
47+
NAVIGATE_ARCHIVE: 'navigate-archive',
48+
NAVIGATE_ADMIN: 'navigate-admin',
49+
50+
// Archive events
51+
VIEW_ARCHIVE: 'view-archive',
52+
DOWNLOAD_ARCHIVE: 'download-archive',
53+
PREVIEW_ARCHIVE: 'preview-archive',
54+
UPLOAD_ARCHIVE: 'upload-archive',
55+
EDIT_ARCHIVE: 'edit-archive',
56+
DELETE_ARCHIVE: 'delete-archive',
57+
SEARCH_COURSE: 'search-course',
58+
SELECT_COURSE: 'select-course',
59+
FILTER_ARCHIVES: 'filter-archives',
60+
61+
// Admin events
62+
CREATE_COURSE: 'create-course',
63+
UPDATE_COURSE: 'update-course',
64+
DELETE_COURSE: 'delete-course',
65+
CREATE_USER: 'create-user',
66+
UPDATE_USER: 'update-user',
67+
DELETE_USER: 'delete-user',
68+
VIEW_ANALYTICS: 'view-analytics',
69+
OPEN_ANALYTICS_NEW_TAB: 'open-analytics-new-tab',
70+
71+
// Issue report events
72+
OPEN_ISSUE_REPORT: 'open-issue-report',
73+
SUBMIT_ISSUE_REPORT: 'submit-issue-report',
74+
75+
// Tab/Panel events
76+
SWITCH_TAB: 'switch-tab',
77+
}

frontend/src/views/Admin.vue

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="h-full px-2 md:px-4 admin-container">
33
<div class="card h-full flex flex-col">
4-
<Tabs value="0" class="flex-1">
4+
<Tabs value="0" class="flex-1" @update:value="handleTabChange">
55
<TabList>
66
<Tab value="0">課程管理</Tab>
77
<Tab value="1">使用者管理</Tab>
@@ -382,6 +382,7 @@ import {
382382
updateUser,
383383
deleteUser,
384384
} from '../api'
385+
import { trackEvent, EVENTS } from '../utils/analytics'
385386
386387
const confirm = useConfirm()
387388
const toast = useToast()
@@ -436,6 +437,7 @@ const umamiLoading = ref(false)
436437
437438
const openUmamiInNewTab = () => {
438439
if (umamiShareUrl.value) {
440+
trackEvent(EVENTS.OPEN_ANALYTICS_NEW_TAB)
439441
window.open(umamiShareUrl.value, '_blank')
440442
}
441443
}
@@ -570,6 +572,7 @@ const openCreateDialog = () => {
570572
courseFormErrors.value = {}
571573
editingCourse.value = null
572574
showCourseDialog.value = true
575+
trackEvent(EVENTS.CREATE_COURSE, { action: 'open-dialog' })
573576
}
574577
575578
const openEditDialog = (course) => {
@@ -580,6 +583,7 @@ const openEditDialog = (course) => {
580583
courseFormErrors.value = {}
581584
editingCourse.value = course
582585
showCourseDialog.value = true
586+
trackEvent(EVENTS.UPDATE_COURSE, { action: 'open-dialog', courseName: course.name })
583587
}
584588
585589
const closeCourseDialog = () => {
@@ -614,6 +618,11 @@ const saveCourse = async () => {
614618
try {
615619
if (editingCourse.value) {
616620
await updateCourse(editingCourse.value.id, courseForm.value)
621+
trackEvent(EVENTS.UPDATE_COURSE, {
622+
action: 'submit',
623+
courseName: courseForm.value.name,
624+
category: courseForm.value.category,
625+
})
617626
toast.add({
618627
severity: 'success',
619628
summary: '成功',
@@ -622,6 +631,11 @@ const saveCourse = async () => {
622631
})
623632
} else {
624633
await createCourse(courseForm.value)
634+
trackEvent(EVENTS.CREATE_COURSE, {
635+
action: 'submit',
636+
courseName: courseForm.value.name,
637+
category: courseForm.value.category,
638+
})
625639
toast.add({
626640
severity: 'success',
627641
summary: '成功',
@@ -660,6 +674,10 @@ const confirmDeleteCourse = (course) => {
660674
const deleteCourseAction = async (course) => {
661675
try {
662676
await deleteCourse(course.id)
677+
trackEvent(EVENTS.DELETE_COURSE, {
678+
courseName: course.name,
679+
category: course.category,
680+
})
663681
toast.add({
664682
severity: 'success',
665683
summary: '成功',
@@ -688,6 +706,7 @@ const openCreateUserDialog = () => {
688706
userFormErrors.value = {}
689707
editingUser.value = null
690708
showUserDialog.value = true
709+
trackEvent(EVENTS.CREATE_USER, { action: 'open-dialog' })
691710
}
692711
693712
const openEditUserDialog = (user) => {
@@ -700,6 +719,7 @@ const openEditUserDialog = (user) => {
700719
userFormErrors.value = {}
701720
editingUser.value = user
702721
showUserDialog.value = true
722+
trackEvent(EVENTS.UPDATE_USER, { action: 'open-dialog', userName: user.name })
703723
}
704724
705725
const closeUserDialog = () => {
@@ -750,6 +770,11 @@ const saveUser = async () => {
750770
updateData.password = userForm.value.password
751771
}
752772
await updateUser(editingUser.value.id, updateData)
773+
trackEvent(EVENTS.UPDATE_USER, {
774+
action: 'submit',
775+
userName: userForm.value.name,
776+
isAdmin: userForm.value.is_admin,
777+
})
753778
toast.add({
754779
severity: 'success',
755780
summary: '成功',
@@ -758,6 +783,11 @@ const saveUser = async () => {
758783
})
759784
} else {
760785
await createUser(userForm.value)
786+
trackEvent(EVENTS.CREATE_USER, {
787+
action: 'submit',
788+
userName: userForm.value.name,
789+
isAdmin: userForm.value.is_admin,
790+
})
761791
toast.add({
762792
severity: 'success',
763793
summary: '成功',
@@ -796,6 +826,10 @@ const confirmDeleteUser = (user) => {
796826
const deleteUserAction = async (user) => {
797827
try {
798828
await deleteUser(user.id)
829+
trackEvent(EVENTS.DELETE_USER, {
830+
userName: user.name,
831+
isAdmin: user.is_admin,
832+
})
799833
toast.add({
800834
severity: 'success',
801835
summary: '成功',
@@ -848,6 +882,22 @@ const formatDateTime = (dateString) => {
848882
}
849883
}
850884
885+
const handleTabChange = (value) => {
886+
const tabNames = {
887+
0: 'courses',
888+
1: 'users',
889+
2: 'analytics',
890+
}
891+
892+
trackEvent(EVENTS.SWITCH_TAB, {
893+
tab: tabNames[value] || value,
894+
})
895+
896+
if (value === '2') {
897+
trackEvent(EVENTS.VIEW_ANALYTICS)
898+
}
899+
}
900+
851901
onMounted(() => {
852902
loadCourses()
853903
loadUsers()

0 commit comments

Comments
 (0)