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:
@@ -9,6 +9,7 @@
|
||||
"lint": "echo ok"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dukang/client-logging": "workspace:*",
|
||||
"@dukang/shared-ui": "workspace:*",
|
||||
"@dukang/shared-types": "workspace:*",
|
||||
"@dukang/weixin-sdk": "workspace:*",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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('');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
Reference in New Issue
Block a user