feat: 技术支持工单/企微权限/开发版本管理/消息推送等迭代
This commit is contained in:
@@ -1,140 +0,0 @@
|
||||
import { postJson, type PostJson } from './http';
|
||||
import { getSessionId } from './session';
|
||||
|
||||
export type TrackParams = Record<string, unknown>;
|
||||
|
||||
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 send = opts.postJson ?? postJson;
|
||||
const getToken = opts.getToken ?? (() =>
|
||||
typeof localStorage !== 'undefined' ? localStorage.getItem('accessToken') : null);
|
||||
|
||||
function track(eventName: string, params?: TrackParams) {
|
||||
const sessionId = getSessionId();
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': opts.clientApp,
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
send({
|
||||
url: `${opts.apiBase}/analytics/events`,
|
||||
headers,
|
||||
body: {
|
||||
sessionId,
|
||||
clientApp: opts.clientApp,
|
||||
events: [{ eventName, params: { sessionId, ...params } }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function trackPageView(eventName: string, params?: TrackParams) {
|
||||
track(eventName, {
|
||||
pagePath: resolvePagePath(opts.getPagePath),
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
function trackSessionStart() {
|
||||
track('session_start', {
|
||||
pagePath: resolvePagePath(opts.getPagePath),
|
||||
});
|
||||
}
|
||||
|
||||
return { track, trackPageView, trackSessionStart };
|
||||
}
|
||||
|
||||
export type StoreTrackerOptions = {
|
||||
apiBase: string;
|
||||
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;
|
||||
|
||||
send({
|
||||
url: `${opts.apiBase}/analytics/store-events`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Client-App': opts.clientApp,
|
||||
},
|
||||
body: {
|
||||
sessionId: getSessionId(),
|
||||
storeId,
|
||||
events: [{ eventName, params: { sessionId: getSessionId(), storeId, ...params } }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function trackPageView(eventName: string, params?: TrackParams) {
|
||||
track(eventName, {
|
||||
pagePath: resolvePagePath(opts.getPagePath),
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
return { track, trackPageView };
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
send({
|
||||
url: `${opts.apiBase}/analytics/partner-events`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Client-App': opts.clientApp,
|
||||
},
|
||||
body: {
|
||||
sessionId: getSessionId(),
|
||||
events: [{ eventName, params: { sessionId: getSessionId(), ...params } }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function trackPageView(eventName: string, params?: TrackParams) {
|
||||
track(eventName, {
|
||||
pagePath: resolvePagePath(opts.getPagePath),
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
return { track, trackPageView };
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { postJson } from './http';
|
||||
|
||||
export type ClientErrorLevel = 'fatal' | 'error' | 'warn' | 'info';
|
||||
export type ClientErrorCategory =
|
||||
| 'js_error'
|
||||
| 'unhandled_rejection'
|
||||
| 'api_error'
|
||||
| 'network'
|
||||
| 'render'
|
||||
| 'bridge'
|
||||
| 'other';
|
||||
|
||||
export type ClientErrorReporterOptions = {
|
||||
apiBase: string;
|
||||
clientApp: string;
|
||||
getToken?: () => string | null;
|
||||
};
|
||||
|
||||
export function installClientErrorReporting(opts: ClientErrorReporterOptions) {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const report = (payload: {
|
||||
level: ClientErrorLevel;
|
||||
category: ClientErrorCategory;
|
||||
message: string;
|
||||
stack?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
}) => {
|
||||
const token = opts.getToken?.() ?? localStorage.getItem('accessToken');
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': opts.clientApp,
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
postJson({
|
||||
url: `${opts.apiBase}/common/client-errors`,
|
||||
headers,
|
||||
body: {
|
||||
level: payload.level,
|
||||
category: payload.category,
|
||||
message: payload.message,
|
||||
stack: payload.stack,
|
||||
pagePath: window.location.pathname,
|
||||
clientApp: opts.clientApp,
|
||||
extra: payload.extra,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('error', (ev) => {
|
||||
report({
|
||||
level: 'error',
|
||||
category: 'js_error',
|
||||
message: ev.message || 'Unknown error',
|
||||
stack: ev.error instanceof Error ? ev.error.stack : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (ev) => {
|
||||
const reason = ev.reason;
|
||||
report({
|
||||
level: 'error',
|
||||
category: 'unhandled_rejection',
|
||||
message: reason instanceof Error ? reason.message : String(reason),
|
||||
stack: reason instanceof Error ? reason.stack : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function reportApiError(
|
||||
opts: ClientErrorReporterOptions,
|
||||
input: { message: string; status?: number; url?: string; category?: ClientErrorCategory },
|
||||
) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const token = opts.getToken?.() ?? localStorage.getItem('accessToken');
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': opts.clientApp,
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
postJson({
|
||||
url: `${opts.apiBase}/common/client-errors`,
|
||||
headers,
|
||||
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 },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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,18 +0,0 @@
|
||||
export { getSessionId, touchSession } from './session';
|
||||
export {
|
||||
createUserTracker,
|
||||
createStoreTracker,
|
||||
createPartnerTracker,
|
||||
type TrackParams,
|
||||
type UserTrackerOptions,
|
||||
type StoreTrackerOptions,
|
||||
type PartnerTrackerOptions,
|
||||
} from './analytics';
|
||||
export { postJson, type PostJson, type PostJsonInput } from './http';
|
||||
export {
|
||||
installClientErrorReporting,
|
||||
reportApiError,
|
||||
type ClientErrorLevel,
|
||||
type ClientErrorCategory,
|
||||
type ClientErrorReporterOptions,
|
||||
} from './client-error';
|
||||
@@ -1,54 +0,0 @@
|
||||
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 {
|
||||
let id = readStorage(SESSION_KEY);
|
||||
if (!id) {
|
||||
id = newSessionId();
|
||||
writeStorage(SESSION_KEY, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function touchSession(): string {
|
||||
return getSessionId();
|
||||
}
|
||||
Reference in New Issue
Block a user