merge(dev): release v3.4.10

This commit is contained in:
2026-08-03 22:54:23 +08:00
7 changed files with 157 additions and 39 deletions
+1 -2
View File
@@ -9,10 +9,9 @@ const isDevMode =
process.argv.includes('development'); process.argv.includes('development');
/** 小程序/H5 请求的后端 origin(不含 /api);本地默认本机,生产构建默认远程 */ /** 小程序/H5 请求的后端 origin(不含 /api);本地默认本机,生产构建默认远程 */
// TODO(prod-release): 发生产前改回 https://api.dukanghaoke.com
const API_ORIGIN = const API_ORIGIN =
process.env.VITE_API_TARGET ?? 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')); const requireFromApp = createRequire(path.resolve(__dirname, '../package.json'));
+15
View File
@@ -1,10 +1,25 @@
import Taro from '@tarojs/taro';
import { createUserTracker, getSessionId } from '@dukang/client-logging'; import { createUserTracker, getSessionId } from '@dukang/client-logging';
import { API_BASE, CLIENT_APP, getToken } from './api'; 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({ const tracker = createUserTracker({
apiBase: API_BASE, apiBase: API_BASE,
clientApp: CLIENT_APP, clientApp: CLIENT_APP,
getToken, getToken,
getPagePath: currentPagePath,
postJson: ({ url, headers, body }) => {
void Taro.request({ url, method: 'POST', header: headers, data: body }).catch(() => {});
},
}); });
export { getSessionId }; export { getSessionId };
+39 -20
View File
@@ -1,3 +1,4 @@
import { postJson, type PostJson } from './http';
import { getSessionId } from './session'; import { getSessionId } from './session';
export type TrackParams = Record<string, unknown>; export type TrackParams = Record<string, unknown>;
@@ -6,10 +7,20 @@ export type UserTrackerOptions = {
apiBase: string; apiBase: string;
clientApp: string; clientApp: string;
getToken?: () => string | null; 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) { 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) { function track(eventName: string, params?: TrackParams) {
const sessionId = getSessionId(); const sessionId = getSessionId();
@@ -20,27 +31,27 @@ export function createUserTracker(opts: UserTrackerOptions) {
}; };
if (token) headers.Authorization = `Bearer ${token}`; if (token) headers.Authorization = `Bearer ${token}`;
void fetch(`${opts.apiBase}/analytics/events`, { send({
method: 'POST', url: `${opts.apiBase}/analytics/events`,
headers, headers,
body: JSON.stringify({ body: {
sessionId, sessionId,
clientApp: opts.clientApp, clientApp: opts.clientApp,
events: [{ eventName, params: { sessionId, ...params } }], events: [{ eventName, params: { sessionId, ...params } }],
}), },
}).catch(() => {}); });
} }
function trackPageView(eventName: string, params?: TrackParams) { function trackPageView(eventName: string, params?: TrackParams) {
track(eventName, { track(eventName, {
pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, pagePath: resolvePagePath(opts.getPagePath),
...params, ...params,
}); });
} }
function trackSessionStart() { function trackSessionStart() {
track('session_start', { track('session_start', {
pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, pagePath: resolvePagePath(opts.getPagePath),
}); });
} }
@@ -52,32 +63,36 @@ export type StoreTrackerOptions = {
clientApp: string; clientApp: string;
getToken: () => string | null; getToken: () => string | null;
getStoreId: () => string | null; getStoreId: () => string | null;
postJson?: PostJson;
getPagePath?: () => string | undefined;
}; };
export function createStoreTracker(opts: StoreTrackerOptions) { export function createStoreTracker(opts: StoreTrackerOptions) {
const send = opts.postJson ?? postJson;
function track(eventName: string, params?: TrackParams) { function track(eventName: string, params?: TrackParams) {
const token = opts.getToken(); const token = opts.getToken();
const storeId = opts.getStoreId(); const storeId = opts.getStoreId();
if (!token || !storeId) return; if (!token || !storeId) return;
void fetch(`${opts.apiBase}/analytics/store-events`, { send({
method: 'POST', url: `${opts.apiBase}/analytics/store-events`,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
'X-Client-App': opts.clientApp, 'X-Client-App': opts.clientApp,
}, },
body: JSON.stringify({ body: {
sessionId: getSessionId(), sessionId: getSessionId(),
storeId, storeId,
events: [{ eventName, params: { sessionId: getSessionId(), storeId, ...params } }], events: [{ eventName, params: { sessionId: getSessionId(), storeId, ...params } }],
}), },
}).catch(() => {}); });
} }
function trackPageView(eventName: string, params?: TrackParams) { function trackPageView(eventName: string, params?: TrackParams) {
track(eventName, { track(eventName, {
pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, pagePath: resolvePagePath(opts.getPagePath),
...params, ...params,
}); });
} }
@@ -89,30 +104,34 @@ export type PartnerTrackerOptions = {
apiBase: string; apiBase: string;
clientApp: string; clientApp: string;
getToken: () => string | null; getToken: () => string | null;
postJson?: PostJson;
getPagePath?: () => string | undefined;
}; };
export function createPartnerTracker(opts: PartnerTrackerOptions) { export function createPartnerTracker(opts: PartnerTrackerOptions) {
const send = opts.postJson ?? postJson;
function track(eventName: string, params?: TrackParams) { function track(eventName: string, params?: TrackParams) {
const token = opts.getToken(); const token = opts.getToken();
if (!token) return; if (!token) return;
void fetch(`${opts.apiBase}/analytics/partner-events`, { send({
method: 'POST', url: `${opts.apiBase}/analytics/partner-events`,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
'X-Client-App': opts.clientApp, 'X-Client-App': opts.clientApp,
}, },
body: JSON.stringify({ body: {
sessionId: getSessionId(), sessionId: getSessionId(),
events: [{ eventName, params: { sessionId: getSessionId(), ...params } }], events: [{ eventName, params: { sessionId: getSessionId(), ...params } }],
}), },
}).catch(() => {}); });
} }
function trackPageView(eventName: string, params?: TrackParams) { function trackPageView(eventName: string, params?: TrackParams) {
track(eventName, { track(eventName, {
pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined, pagePath: resolvePagePath(opts.getPagePath),
...params, ...params,
}); });
} }
+12 -10
View File
@@ -1,3 +1,5 @@
import { postJson } from './http';
export type ClientErrorLevel = 'fatal' | 'error' | 'warn' | 'info'; export type ClientErrorLevel = 'fatal' | 'error' | 'warn' | 'info';
export type ClientErrorCategory = export type ClientErrorCategory =
| 'js_error' | 'js_error'
@@ -31,10 +33,10 @@ export function installClientErrorReporting(opts: ClientErrorReporterOptions) {
}; };
if (token) headers.Authorization = `Bearer ${token}`; if (token) headers.Authorization = `Bearer ${token}`;
void fetch(`${opts.apiBase}/common/client-errors`, { postJson({
method: 'POST', url: `${opts.apiBase}/common/client-errors`,
headers, headers,
body: JSON.stringify({ body: {
level: payload.level, level: payload.level,
category: payload.category, category: payload.category,
message: payload.message, message: payload.message,
@@ -42,8 +44,8 @@ export function installClientErrorReporting(opts: ClientErrorReporterOptions) {
pagePath: window.location.pathname, pagePath: window.location.pathname,
clientApp: opts.clientApp, clientApp: opts.clientApp,
extra: payload.extra, extra: payload.extra,
}), },
}).catch(() => {}); });
}; };
window.addEventListener('error', (ev) => { window.addEventListener('error', (ev) => {
@@ -78,16 +80,16 @@ export function reportApiError(
}; };
if (token) headers.Authorization = `Bearer ${token}`; if (token) headers.Authorization = `Bearer ${token}`;
void fetch(`${opts.apiBase}/common/client-errors`, { postJson({
method: 'POST', url: `${opts.apiBase}/common/client-errors`,
headers, headers,
body: JSON.stringify({ body: {
level: 'warn', level: 'warn',
category: input.category ?? 'api_error', category: input.category ?? 'api_error',
message: input.message.slice(0, 1000), message: input.message.slice(0, 1000),
pagePath: window.location.pathname, pagePath: window.location.pathname,
clientApp: opts.clientApp, clientApp: opts.clientApp,
extra: { status: input.status, url: input.url }, extra: { status: input.status, url: input.url },
}), },
}).catch(() => {}); });
} }
+46
View File
@@ -0,0 +1,46 @@
export type PostJsonInput = {
url: string;
headers?: Record<string, string>;
body: unknown;
};
export type PostJson = (input: PostJsonInput) => void;
type WxLike = {
request?: (opts: {
url: string;
method?: string;
header?: Record<string, string>;
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: () => {},
});
}
}
+1
View File
@@ -8,6 +8,7 @@ export {
type StoreTrackerOptions, type StoreTrackerOptions,
type PartnerTrackerOptions, type PartnerTrackerOptions,
} from './analytics'; } from './analytics';
export { postJson, type PostJson, type PostJsonInput } from './http';
export { export {
installClientErrorReporting, installClientErrorReporting,
reportApiError, reportApiError,
+43 -7
View File
@@ -1,14 +1,50 @@
const SESSION_KEY = 'dukang_session_id'; 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 { export function getSessionId(): string {
if (typeof localStorage === 'undefined') return 'ssr'; let id = readStorage(SESSION_KEY);
let id = localStorage.getItem(SESSION_KEY);
if (!id) { if (!id) {
id = id = newSessionId();
typeof crypto !== 'undefined' && 'randomUUID' in crypto writeStorage(SESSION_KEY, id);
? crypto.randomUUID()
: `s_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
localStorage.setItem(SESSION_KEY, id);
} }
return id; return id;
} }