feat(analytics): persona logging upgrade and Sentry system config

Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 22:30:02 +08:00
parent ab10431001
commit 89fd333702
91 changed files with 1805 additions and 136 deletions
+121
View File
@@ -0,0 +1,121 @@
import { getSessionId } from './session';
export type TrackParams = Record<string, unknown>;
export type UserTrackerOptions = {
apiBase: string;
clientApp: string;
getToken?: () => string | null;
};
export function createUserTracker(opts: UserTrackerOptions) {
const getToken = opts.getToken ?? (() => localStorage.getItem('accessToken'));
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}`;
void fetch(`${opts.apiBase}/analytics/events`, {
method: 'POST',
headers,
body: JSON.stringify({
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,
...params,
});
}
function trackSessionStart() {
track('session_start', {
pagePath: typeof window !== 'undefined' ? window.location.pathname : undefined,
});
}
return { track, trackPageView, trackSessionStart };
}
export type StoreTrackerOptions = {
apiBase: string;
clientApp: string;
getToken: () => string | null;
getStoreId: () => string | null;
};
export function createStoreTracker(opts: StoreTrackerOptions) {
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',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'X-Client-App': opts.clientApp,
},
body: JSON.stringify({
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,
...params,
});
}
return { track, trackPageView };
}
export type PartnerTrackerOptions = {
apiBase: string;
clientApp: string;
getToken: () => string | null;
};
export function createPartnerTracker(opts: PartnerTrackerOptions) {
function track(eventName: string, params?: TrackParams) {
const token = opts.getToken();
if (!token) return;
void fetch(`${opts.apiBase}/analytics/partner-events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'X-Client-App': opts.clientApp,
},
body: JSON.stringify({
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,
...params,
});
}
return { track, trackPageView };
}
@@ -0,0 +1,93 @@
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}`;
void fetch(`${opts.apiBase}/common/client-errors`, {
method: 'POST',
headers,
body: JSON.stringify({
level: payload.level,
category: payload.category,
message: payload.message,
stack: payload.stack,
pagePath: window.location.pathname,
clientApp: opts.clientApp,
extra: payload.extra,
}),
}).catch(() => {});
};
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}`;
void fetch(`${opts.apiBase}/common/client-errors`, {
method: 'POST',
headers,
body: JSON.stringify({
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(() => {});
}
+17
View File
@@ -0,0 +1,17 @@
export { getSessionId, touchSession } from './session';
export {
createUserTracker,
createStoreTracker,
createPartnerTracker,
type TrackParams,
type UserTrackerOptions,
type StoreTrackerOptions,
type PartnerTrackerOptions,
} from './analytics';
export {
installClientErrorReporting,
reportApiError,
type ClientErrorLevel,
type ClientErrorCategory,
type ClientErrorReporterOptions,
} from './client-error';
+18
View File
@@ -0,0 +1,18 @@
const SESSION_KEY = 'dukang_session_id';
export function getSessionId(): string {
if (typeof localStorage === 'undefined') return 'ssr';
let id = localStorage.getItem(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);
}
return id;
}
export function touchSession(): string {
return getSessionId();
}