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
+1
View File
@@ -9,6 +9,7 @@
"lint": "echo ok"
},
"dependencies": {
"@dukang/client-logging": "workspace:*",
"@dukang/shared-ui": "workspace:*",
"@dukang/shared-types": "workspace:*",
"@dukang/weixin-sdk": "workspace:*",
+26
View File
@@ -0,0 +1,26 @@
import { createStoreTracker } from '@dukang/client-logging';
import { apiBase } from './api';
const tracker = createStoreTracker({
apiBase,
clientApp: 'SHOP_H5',
getToken: () => localStorage.getItem('shopAccessToken'),
getStoreId: () => {
try {
const raw = localStorage.getItem('shopSession');
if (!raw) return null;
const parsed = JSON.parse(raw) as { storeId?: string };
return parsed.storeId ?? null;
} catch {
return null;
}
},
});
export function trackStore(eventName: string, params?: Record<string, unknown>) {
tracker.track(eventName, params);
}
export function trackStorePageView(eventName: string, params?: Record<string, unknown>) {
tracker.trackPageView(eventName, params);
}
+11
View File
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
import { trackStorePageView } from './analytics';
export function useStorePageView(eventName: string, params?: Record<string, unknown>) {
const fired = useRef(false);
useEffect(() => {
if (fired.current) return;
fired.current = true;
trackStorePageView(eventName, params);
}, [eventName, params]);
}
+7 -2
View File
@@ -3,12 +3,17 @@ import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { getRouterBasename } from '@dukang/weixin-sdk';
import { StoreSessionProvider } from './contexts/StoreSessionContext';
import { installClientErrorReporting } from './lib/client-error';
import { installClientErrorReporting } from '@dukang/client-logging';
import App from './App';
import { apiBase } from './lib/api';
import './styles.css';
import './styles/legal.css';
installClientErrorReporting();
installClientErrorReporting({
apiBase,
clientApp: 'SHOP_H5',
getToken: () => localStorage.getItem('shopAccessToken'),
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
+4
View File
@@ -14,6 +14,8 @@ import {
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import WechatScanAuthModal from '../components/WechatScanAuthModal';
import { useStorePageView } from '../lib/usePageView';
import { trackStore } from '../lib/analytics';
const PENDING_SCAN_KEY = 'shop_pending_scan';
@@ -33,6 +35,7 @@ function formatScanError(e: unknown): string {
}
export default function HomePage() {
useStorePageView('store_home_view');
const navigate = useNavigate();
const { applySession } = useStoreSession();
const [searchParams, setSearchParams] = useSearchParams();
@@ -74,6 +77,7 @@ export default function HomePage() {
}, [loadDashboard]);
async function runScan(opts?: { postAuthWarmup?: boolean }) {
trackStore('store_redeem_scan_start');
if (!isWechatEnv()) {
setScanMsg('请在微信内打开门店端进行扫码核销');
return;
+2
View File
@@ -13,8 +13,10 @@ import {
handleShopWechatLoginResult,
} from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { useStorePageView } from '../lib/usePageView';
export default function MinePage() {
useStorePageView('store_mine_view');
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { resetSession, store: sessionStore, applySession } = useStoreSession();
+2
View File
@@ -4,6 +4,7 @@ import type { StorePackagesResponse } from '@dukang/shared-types';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import ShopPackagesForm from '../components/ShopPackagesForm';
import { request } from '../lib/api';
import { useStorePageView } from '../lib/usePageView';
import {
emptyPackage,
normalizePackageFormItems,
@@ -12,6 +13,7 @@ import {
} from '../lib/storePackages';
export default function PackagesPage() {
useStorePageView('store_packages_view');
const navigate = useNavigate();
const [items, setItems] = useState<PackageFormItem[]>([emptyPackage()]);
const [pending, setPending] = useState<StorePackagesResponse['pendingRequest']>(null);
@@ -2,12 +2,14 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { RedeemPhoneDirectPrepareResult } from '@dukang/shared-types';
import { request } from '../lib/api';
import { useStorePageView } from '../lib/usePageView';
function formatAmount(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
export default function PhoneRedeemPage() {
useStorePageView('store_phone_redeem_view');
const navigate = useNavigate();
const [phone, setPhone] = useState('');
const [amount, setAmount] = useState('');
+2
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { REDEEM_CHANNEL_LABELS, type RedeemChannel, type RedeemStatsDto } from '@dukang/shared-types';
import { request } from '../lib/api';
import { useStorePageView } from '../lib/usePageView';
type RangeKey = 'today' | '7d' | '30d';
type StatusFilter = 'all' | 'pending' | 'paid';
@@ -30,6 +31,7 @@ function channelLabel(channel: unknown): string {
}
export default function RecordsPage() {
useStorePageView('store_records_view');
const [records, setRecords] = useState<Array<Record<string, unknown>>>([]);
const [range, setRange] = useState<RangeKey>('today');
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import WeakNetFallbackPanel from '../components/WeakNetFallbackPanel';
import { request } from '../lib/api';
import { reportRedeemFailure } from '../lib/redeem-failure';
import { useStorePageView } from '../lib/usePageView';
function formatAmount(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
@@ -17,6 +18,7 @@ type Preview = {
};
export default function RedeemConfirmPage() {
useStorePageView('store_redeem_confirm_view');
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [token, setToken] = useState('');
+2
View File
@@ -4,6 +4,7 @@ import { STORE_STAFF_ROLE_LABELS, type StoreStaffRole } from '@dukang/shared-typ
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { useStoreSession } from '../contexts/StoreSessionContext';
import { getStoreProfile, request } from '../lib/api';
import { useStorePageView } from '../lib/usePageView';
type StaffItem = {
id: string;
@@ -23,6 +24,7 @@ function maskPhone(phone: string) {
}
export default function StaffPage() {
useStorePageView('store_staff_view');
const navigate = useNavigate();
const { store } = useStoreSession();
const profile = store ?? getStoreProfile();
+2
View File
@@ -8,6 +8,7 @@ import {
} from '@dukang/shared-types';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import { request } from '../lib/api';
import { useStorePageView } from '../lib/usePageView';
type StatusFilter = 'all' | StoreWithdrawStatus;
@@ -16,6 +17,7 @@ function formatMoney(n: number) {
}
export default function WithdrawPage() {
useStorePageView('store_withdraw_view');
const navigate = useNavigate();
const [summary, setSummary] = useState<StoreWithdrawSummaryDto | null>(null);
const [items, setItems] = useState<StoreWithdrawRequestDto[]>([]);