feat: 请求日志 + 前端埋点系统

后端:
- 新增 logger 中间件,记录所有请求到 logs/ 目录
- 新增 /api/track 接口接收前端埋点数据

前端:
- 新增 tracker 工具,支持页面访问/操作/错误埋点
- request.ts 添加 API 错误自动埋点
- App.vue 初始化全局错误追踪

文档:
- CLAUDE.md 更新项目结构
- DEV.md 添加埋点和日志说明
This commit is contained in:
2026-06-08 14:28:11 +08:00
parent 2f122d0a96
commit 0de4c9832c
9 changed files with 290 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import { useGroupStore } from '@/stores/group'
import { useConfigStore } from '@/stores/config'
import { saveLoginResult } from '@/utils/request'
import { markReady } from '@/utils/app-ready'
import { setupErrorTracking, trackAction, trackError } from '@/utils/tracker'
/** 登录重试最多3次 */
async function loginWithRetry(loginFn: () => Promise<any>, retries = 3): Promise<boolean> {
@@ -15,6 +16,7 @@ async function loginWithRetry(loginFn: () => Promise<any>, retries = 3): Promise
return true
} catch (e) {
console.error(`[Auth] Login attempt ${i + 1} failed:`, e)
trackError(e as Error, { context: 'login', attempt: i + 1 })
if (i < retries - 1) {
// 等待1秒后重试
await new Promise(resolve => setTimeout(resolve, 1000))
@@ -25,6 +27,10 @@ async function loginWithRetry(loginFn: () => Promise<any>, retries = 3): Promise
}
onLaunch(() => {
// 初始化错误追踪
setupErrorTracking()
trackAction('app_launch')
const groupStore = useGroupStore()
const configStore = useConfigStore()
groupStore.init()

View File

@@ -11,7 +11,7 @@ export const useUserStore = defineStore('user', () => {
})
const slogan = computed(() => {
return userInfo.value?.slogan || '记账小能手'
return userInfo.value?.slogan
})
const avatarUrl = computed(() => {

View File

@@ -1,5 +1,6 @@
import { API_BASE } from '@/config'
import type { LoginResult } from '@/api/auth'
import { trackApiError } from './tracker'
interface RequestOptions {
url: string
@@ -88,6 +89,8 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
if (data.code === 0) {
resolve(data.data)
} else {
// 埋点API 业务错误
trackApiError(options.url, statusCode, data.message || '请求失败')
// "用户不存在" 不弹 toast首次登录竞态
if (data.code !== 40400 || data.message !== '用户不存在') {
uni.showToast({ title: data.message || '请求失败', icon: 'none' })
@@ -96,6 +99,8 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
}
},
fail: (err: any) => {
// 埋点:网络错误
trackApiError(options.url, 0, '网络错误')
uni.showToast({ title: '网络错误', icon: 'none' })
reject(err)
}

140
client/src/utils/tracker.ts Normal file
View File

@@ -0,0 +1,140 @@
/**
* 前端埋点工具
* 记录用户关键操作和错误信息
*/
import { request } from './request'
interface TrackEvent {
event: string
page?: string
data?: Record<string, any>
}
/** 上报队列(批量上报,减少请求) */
let eventQueue: TrackEvent[] = []
let flushTimer: number | null = null
const FLUSH_INTERVAL = 5000 // 5秒上报一次
const MAX_QUEUE_SIZE = 20
/** 获取当前页面路径 */
function getCurrentPage(): string {
const pages = getCurrentPages()
if (pages.length > 0) {
return `/${pages[pages.length - 1].route}`
}
return '/unknown'
}
/** 批量上报事件 */
async function flushEvents() {
if (eventQueue.length === 0) return
const events = [...eventQueue]
eventQueue = []
try {
await request({
url: '/track',
method: 'POST',
data: { events }
})
} catch {
// 上报失败不处理,避免影响用户体验
}
}
/** 添加事件到队列 */
function addToQueue(event: TrackEvent) {
eventQueue.push(event)
// 队列满时立即上报
if (eventQueue.length >= MAX_QUEUE_SIZE) {
flushEvents()
return
}
// 设置定时上报
if (!flushTimer) {
flushTimer = setTimeout(() => {
flushTimer = null
flushEvents()
}, FLUSH_INTERVAL) as unknown as number
}
}
/**
* 追踪页面访问
*/
export function trackPageView(pagePath?: string) {
addToQueue({
event: 'page_view',
page: pagePath || getCurrentPage()
})
}
/**
* 追踪用户操作
* @param action 操作名称,如 'click_save', 'submit_form'
* @param data 附加数据
*/
export function trackAction(action: string, data?: Record<string, any>) {
addToQueue({
event: 'action',
page: getCurrentPage(),
data: { action, ...data }
})
}
/**
* 追踪错误
*/
export function trackError(error: Error | string, extra?: Record<string, any>) {
const errorInfo = typeof error === 'string'
? { message: error }
: { message: error.message, stack: error.stack?.slice(0, 500) }
addToQueue({
event: 'error',
page: getCurrentPage(),
data: { ...errorInfo, ...extra }
})
}
/**
* 追踪 API 请求失败
*/
export function trackApiError(url: string, status: number, message: string) {
addToQueue({
event: 'api_error',
page: getCurrentPage(),
data: { url, status, message }
})
}
/**
* 设置全局错误监听
*/
export function setupErrorTracking() {
// Vue 错误处理
uni.onError((err: string) => {
trackError(err, { type: 'global' })
})
// 未处理的 Promise rejection
// #ifdef H5
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
trackError(String(event.reason), { type: 'unhandledrejection' })
})
}
// #endif
}
export default {
trackPageView,
trackAction,
trackError,
trackApiError,
setupErrorTracking
}