diff --git a/apps/mini-user/config/index.ts b/apps/mini-user/config/index.ts index 9efa05f..10d1eef 100644 --- a/apps/mini-user/config/index.ts +++ b/apps/mini-user/config/index.ts @@ -9,10 +9,9 @@ const isDevMode = process.argv.includes('development'); /** 小程序/H5 请求的后端 origin(不含 /api);本地默认本机,生产构建默认远程 */ -// TODO(prod-release): 发生产前改回 https://api.dukanghaoke.com const API_ORIGIN = process.env.VITE_API_TARGET ?? - (isDevMode ? 'http://localhost:3000' : 'https://api-test.dukanghaoke.com'); + (isDevMode ? 'http://localhost:3000' : 'https://api.dukanghaoke.com'); const requireFromApp = createRequire(path.resolve(__dirname, '../package.json')); diff --git a/apps/mini-user/src/lib/analytics.ts b/apps/mini-user/src/lib/analytics.ts index d21c820..3d8479e 100644 --- a/apps/mini-user/src/lib/analytics.ts +++ b/apps/mini-user/src/lib/analytics.ts @@ -1,10 +1,25 @@ +import Taro from '@tarojs/taro'; import { createUserTracker, getSessionId } from '@dukang/client-logging'; import { API_BASE, CLIENT_APP, getToken } from './api'; +function currentPagePath(): string | undefined { + try { + const pages = Taro.getCurrentPages(); + const cur = pages[pages.length - 1] as { route?: string; $taroPath?: string } | undefined; + return cur?.$taroPath || cur?.route || undefined; + } catch { + return undefined; + } +} + const tracker = createUserTracker({ apiBase: API_BASE, clientApp: CLIENT_APP, getToken, + getPagePath: currentPagePath, + postJson: ({ url, headers, body }) => { + void Taro.request({ url, method: 'POST', header: headers, data: body }).catch(() => {}); + }, }); export { getSessionId }; diff --git a/packages/client-logging/src/analytics.ts b/packages/client-logging/src/analytics.ts index f0b67e8..f1ba8fa 100644 --- a/packages/client-logging/src/analytics.ts +++ b/packages/client-logging/src/analytics.ts @@ -1,3 +1,4 @@ +import { postJson, type PostJson } from './http'; import { getSessionId } from './session'; export type TrackParams = Record; @@ -6,10 +7,20 @@ export type UserTrackerOptions = { apiBase: string; clientApp: string; getToken?: () => string | null; + postJson?: PostJson; + getPagePath?: () => string | undefined; }; +function resolvePagePath(getPagePath?: () => string | undefined): string | undefined { + if (getPagePath) return getPagePath(); + if (typeof window !== 'undefined') return window.location.pathname; + return undefined; +} + export function createUserTracker(opts: UserTrackerOptions) { - const getToken = opts.getToken ?? (() => localStorage.getItem('accessToken')); + const send = opts.postJson ?? postJson; + const getToken = opts.getToken ?? (() => + typeof localStorage !== 'undefined' ? localStorage.getItem('accessToken') : null); function track(eventName: string, params?: TrackParams) { const sessionId = getSessionId(); @@ -20,27 +31,27 @@ export function createUserTracker(opts: UserTrackerOptions) { }; if (token) headers.Authorization = `Bearer ${token}`; - void fetch(`${opts.apiBase}/analytics/events`, { - method: 'POST', + send({ + url: `${opts.apiBase}/analytics/events`, headers, - body: JSON.stringify({ + body: { sessionId, clientApp: opts.clientApp, events: [{ eventName, params: { sessionId, ...params } }], - }), - }).catch(() => {}); + }, + }); } function trackPageView(eventName: string, params?: TrackParams) { track(eventName, { - pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, + pagePath: resolvePagePath(opts.getPagePath), ...params, }); } function trackSessionStart() { track('session_start', { - pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, + pagePath: resolvePagePath(opts.getPagePath), }); } @@ -52,32 +63,36 @@ export type StoreTrackerOptions = { clientApp: string; getToken: () => string | null; getStoreId: () => string | null; + postJson?: PostJson; + getPagePath?: () => string | undefined; }; export function createStoreTracker(opts: StoreTrackerOptions) { + const send = opts.postJson ?? postJson; + function track(eventName: string, params?: TrackParams) { const token = opts.getToken(); const storeId = opts.getStoreId(); if (!token || !storeId) return; - void fetch(`${opts.apiBase}/analytics/store-events`, { - method: 'POST', + send({ + url: `${opts.apiBase}/analytics/store-events`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, 'X-Client-App': opts.clientApp, }, - body: JSON.stringify({ + body: { sessionId: getSessionId(), storeId, events: [{ eventName, params: { sessionId: getSessionId(), storeId, ...params } }], - }), - }).catch(() => {}); + }, + }); } function trackPageView(eventName: string, params?: TrackParams) { track(eventName, { - pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, + pagePath: resolvePagePath(opts.getPagePath), ...params, }); } @@ -89,30 +104,34 @@ export type PartnerTrackerOptions = { apiBase: string; clientApp: string; getToken: () => string | null; + postJson?: PostJson; + getPagePath?: () => string | undefined; }; export function createPartnerTracker(opts: PartnerTrackerOptions) { + const send = opts.postJson ?? postJson; + function track(eventName: string, params?: TrackParams) { const token = opts.getToken(); if (!token) return; - void fetch(`${opts.apiBase}/analytics/partner-events`, { - method: 'POST', + send({ + url: `${opts.apiBase}/analytics/partner-events`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, 'X-Client-App': opts.clientApp, }, - body: JSON.stringify({ + body: { sessionId: getSessionId(), events: [{ eventName, params: { sessionId: getSessionId(), ...params } }], - }), - }).catch(() => {}); + }, + }); } function trackPageView(eventName: string, params?: TrackParams) { track(eventName, { - pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, + pagePath: resolvePagePath(opts.getPagePath), ...params, }); } diff --git a/packages/client-logging/src/client-error.ts b/packages/client-logging/src/client-error.ts index 8c8146a..7c8aa82 100644 --- a/packages/client-logging/src/client-error.ts +++ b/packages/client-logging/src/client-error.ts @@ -1,3 +1,5 @@ +import { postJson } from './http'; + export type ClientErrorLevel = 'fatal' | 'error' | 'warn' | 'info'; export type ClientErrorCategory = | 'js_error' @@ -31,10 +33,10 @@ export function installClientErrorReporting(opts: ClientErrorReporterOptions) { }; if (token) headers.Authorization = `Bearer ${token}`; - void fetch(`${opts.apiBase}/common/client-errors`, { - method: 'POST', + postJson({ + url: `${opts.apiBase}/common/client-errors`, headers, - body: JSON.stringify({ + body: { level: payload.level, category: payload.category, message: payload.message, @@ -42,8 +44,8 @@ export function installClientErrorReporting(opts: ClientErrorReporterOptions) { pagePath: window.location.pathname, clientApp: opts.clientApp, extra: payload.extra, - }), - }).catch(() => {}); + }, + }); }; window.addEventListener('error', (ev) => { @@ -78,16 +80,16 @@ export function reportApiError( }; if (token) headers.Authorization = `Bearer ${token}`; - void fetch(`${opts.apiBase}/common/client-errors`, { - method: 'POST', + postJson({ + url: `${opts.apiBase}/common/client-errors`, headers, - body: JSON.stringify({ + body: { level: 'warn', category: input.category ?? 'api_error', message: input.message.slice(0, 1000), pagePath: window.location.pathname, clientApp: opts.clientApp, extra: { status: input.status, url: input.url }, - }), - }).catch(() => {}); + }, + }); } diff --git a/packages/client-logging/src/http.ts b/packages/client-logging/src/http.ts new file mode 100644 index 0000000..07b83c7 --- /dev/null +++ b/packages/client-logging/src/http.ts @@ -0,0 +1,46 @@ +export type PostJsonInput = { + url: string; + headers?: Record; + body: unknown; +}; + +export type PostJson = (input: PostJsonInput) => void; + +type WxLike = { + request?: (opts: { + url: string; + method?: string; + header?: Record; + data?: unknown; + fail?: () => void; + }) => void; +}; + +function getWx(): WxLike | undefined { + return (globalThis as { wx?: WxLike }).wx; +} + +/** fire-and-forget POST JSON;优先 fetch,小程序回退 wx.request */ +export function postJson(input: PostJsonInput): void { + const headers = input.headers ?? { 'Content-Type': 'application/json' }; + + if (typeof fetch === 'function') { + void fetch(input.url, { + method: 'POST', + headers, + body: JSON.stringify(input.body), + }).catch(() => {}); + return; + } + + const wxApi = getWx(); + if (wxApi?.request) { + wxApi.request({ + url: input.url, + method: 'POST', + header: headers, + data: input.body, + fail: () => {}, + }); + } +} diff --git a/packages/client-logging/src/index.ts b/packages/client-logging/src/index.ts index a2d7ca3..407e11c 100644 --- a/packages/client-logging/src/index.ts +++ b/packages/client-logging/src/index.ts @@ -8,6 +8,7 @@ export { type StoreTrackerOptions, type PartnerTrackerOptions, } from './analytics'; +export { postJson, type PostJson, type PostJsonInput } from './http'; export { installClientErrorReporting, reportApiError, diff --git a/packages/client-logging/src/session.ts b/packages/client-logging/src/session.ts index 8f892a6..393dad3 100644 --- a/packages/client-logging/src/session.ts +++ b/packages/client-logging/src/session.ts @@ -1,14 +1,50 @@ const SESSION_KEY = 'dukang_session_id'; +type WxStorage = { + getStorageSync?: (key: string) => unknown; + setStorageSync?: (key: string, value: unknown) => void; +}; + +function getWxStorage(): WxStorage | undefined { + return (globalThis as { wx?: WxStorage }).wx; +} + +function readStorage(key: string): string | null { + if (typeof localStorage !== 'undefined') { + return localStorage.getItem(key); + } + try { + const v = getWxStorage()?.getStorageSync?.(key); + return v != null && v !== '' ? String(v) : null; + } catch { + return null; + } +} + +function writeStorage(key: string, value: string): void { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(key, value); + return; + } + try { + getWxStorage()?.setStorageSync?.(key, value); + } catch { + /* ignore */ + } +} + +function newSessionId(): string { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID(); + } + return `s_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; +} + export function getSessionId(): string { - if (typeof localStorage === 'undefined') return 'ssr'; - let id = localStorage.getItem(SESSION_KEY); + let id = readStorage(SESSION_KEY); if (!id) { - id = - typeof crypto !== 'undefined' && 'randomUUID' in crypto - ? crypto.randomUUID() - : `s_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; - localStorage.setItem(SESSION_KEY, id); + id = newSessionId(); + writeStorage(SESSION_KEY, id); } return id; }