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
+17 -12
View File
@@ -1,16 +1,21 @@
import { createUserTracker, getSessionId } from '@dukang/client-logging';
import { apiBase } from './api';
export function track(eventName: string, params?: Record<string, unknown>) {
const token = localStorage.getItem('accessToken');
if (!token) return;
const tracker = createUserTracker({
apiBase,
clientApp: 'USER_H5',
});
void fetch(`${apiBase}/analytics/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'X-Client-App': 'USER_H5',
},
body: JSON.stringify({ events: [{ eventName, params }] }),
}).catch(() => {});
export { getSessionId };
export function track(eventName: string, params?: Record<string, unknown>) {
tracker.track(eventName, params);
}
export function trackPageView(eventName: string, params?: Record<string, unknown>) {
tracker.trackPageView(eventName, params);
}
export function initUserAnalytics() {
tracker.trackSessionStart();
}
+11
View File
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
import { trackPageView } from './analytics';
export function usePageView(eventName: string, params?: Record<string, unknown>) {
const fired = useRef(false);
useEffect(() => {
if (fired.current) return;
fired.current = true;
trackPageView(eventName, params);
}, [eventName, params]);
}
+6
View File
@@ -2,10 +2,16 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { getRouterBasename } from '@dukang/weixin-sdk';
import { installClientErrorReporting } from '@dukang/client-logging';
import App from './App';
import { initUserAnalytics } from './lib/analytics';
import { apiBase } from './lib/api';
import './styles.css';
import './styles/legal.css';
installClientErrorReporting({ apiBase, clientApp: 'USER_H5' });
initUserAnalytics();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter basename={getRouterBasename()}>
@@ -6,6 +6,7 @@ import { request } from '../lib/api';
import { buildAddressListUrl, readCheckoutContext } from '../lib/navigation';
import { DEFAULT_REGION, formatRegion } from '../lib/region-data';
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
import { usePageView } from '../lib/usePageView';
type AddressForm = {
receiverName: string;
@@ -21,6 +22,7 @@ export default function AddressEditPage() {
const { id } = useParams();
const [params] = useSearchParams();
const isEdit = Boolean(id);
usePageView('address_edit', { mode: isEdit ? 'edit' : 'create' });
const navigate = useNavigate();
const [pickerOpen, setPickerOpen] = useState(false);
const [saving, setSaving] = useState(false);
@@ -4,6 +4,7 @@ import SubPageHeader from '../components/SubPageHeader';
import { request } from '../lib/api';
import { useUserSession } from '../contexts/UserSessionContext';
import { buildAddressEditUrl, buildAddressListUrl, buildOrderConfirmUrl, hasCheckoutContext, readCheckoutContext } from '../lib/navigation';
import { usePageView } from '../lib/usePageView';
type Address = {
id: string;
@@ -25,6 +26,7 @@ function formatAddress(a: Address) {
}
export default function AddressListPage() {
usePageView('address_list_view');
const { profile } = useUserSession();
const [list, setList] = useState<Address[]>([]);
const [pendingAddress, setPendingAddress] = useState<Address | null>(null);
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { TICKET_TYPE_LABELS, type TicketTypeDto } from '@dukang/shared-types';
import SubPageHeader from '../components/SubPageHeader';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
type TicketRow = {
id: string;
@@ -23,6 +24,7 @@ const STATUS_LABEL: Record<string, string> = {
};
export default function AfterSaleListPage() {
usePageView('after_sale_list_view');
const navigate = useNavigate();
const [items, setItems] = useState<TicketRow[]>([]);
const [loading, setLoading] = useState(true);
+3
View File
@@ -8,6 +8,8 @@ import {
import SubPageHeader from '../components/SubPageHeader';
import { request } from '../lib/api';
import { uploadFileToOss } from '../lib/upload';
import { usePageView } from '../lib/usePageView';
import { track } from '../lib/analytics';
type OrderRow = {
id: string;
@@ -21,6 +23,7 @@ type OrderRow = {
const STEPS = ['类型', '订单', '凭证', '完成'] as const;
export default function AfterSalePage() {
usePageView('after_sale_apply');
const navigate = useNavigate();
const [params] = useSearchParams();
const presetOrderId = params.get('orderId') || '';
@@ -2,9 +2,11 @@ import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
export default function BenefitDetailPage() {
const { id } = useParams();
usePageView('benefit_detail_view', { couponId: id });
const navigate = useNavigate();
const [data, setData] = useState<{ coupon: Record<string, unknown>; ledgers: Array<Record<string, unknown>> } | null>(null);
useEffect(() => {
+2
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import BrandLogo from '@dukang/shared-ui/BrandLogo';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
type BenefitSummary = {
totalBalance: number;
@@ -44,6 +45,7 @@ function buildRedeemUrl(coupon?: CouponItem) {
}
export default function BenefitPage() {
usePageView('benefit_page_view');
const navigate = useNavigate();
const listRef = useRef<HTMLElement>(null);
const [summary, setSummary] = useState<BenefitSummary | null>(null);
@@ -7,6 +7,7 @@ import { buildOrderAddressSelectUrl } from '../lib/navigation';
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
import { handleShareButtonClick } from '../lib/wechat-share';
import ContactCustomerSheet from '../components/ContactCustomerSheet';
import { usePageView } from '../lib/usePageView';
type OrderItem = {
productName: string;
@@ -131,6 +132,7 @@ function fullReceiverAddress(order: Order) {
export default function OrderDetailPage() {
const { id } = useParams();
usePageView('order_detail_view', { orderId: id });
const navigate = useNavigate();
const [order, setOrder] = useState<Order | null>(null);
const [showCs, setShowCs] = useState(false);
+2
View File
@@ -4,6 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
import AppImage from '@dukang/shared-ui/AppImage';
import OrderStatusTabs from '@dukang/shared-ui/OrderStatusTabs';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
const TABS = [
{ key: 'all', label: '全部' },
@@ -36,6 +37,7 @@ function orderStatusLabel(order: Record<string, unknown>) {
export default function OrderListPage() {
const [params, setParams] = useSearchParams();
const tab = params.get('tab') || 'all';
usePageView('order_list_view', { tab });
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] });
const navigate = useNavigate();
+8
View File
@@ -15,6 +15,8 @@ import {
} from '../lib/pay-wechat';
import { applyWechatLoginResult, handleWechatAuthCallback } from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { usePageView } from '../lib/usePageView';
import { track } from '../lib/analytics';
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -32,6 +34,7 @@ async function waitOrderPaid(orderId: string, maxAttempts = 15) {
export default function PayPage() {
const [params] = useSearchParams();
const orderId = params.get('orderId') || '';
usePageView('pay_page_view', { orderId });
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [authLoading, setAuthLoading] = useState(false);
@@ -161,6 +164,11 @@ export default function PayPage() {
return;
}
setMsg(e instanceof Error ? e.message : '支付失败');
track('pay_fail', {
orderId,
failReason: e instanceof Error ? e.message : '支付失败',
pagePath: '/pay',
});
} finally {
setLoading(false);
}
+4 -1
View File
@@ -155,7 +155,10 @@ export default function ProductDetailPage() {
<button
type="button"
className="product-detail-buy-btn"
onClick={() => navigate(`/order/confirm?productId=${id}&qty=2`)}
onClick={() => {
track('product_click', { refType: 'PRODUCT', refId: id, productId: id });
navigate(`/order/confirm?productId=${id}&qty=2`);
}}
>
</button>
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { REDEEM_TOKEN_TTL_SECONDS } from '@dukang/shared-types';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { usePageView } from '../lib/usePageView';
export default function RedeemCodePage() {
usePageView('redeem_code_view');
const navigate = useNavigate();
const token = sessionStorage.getItem('redeemToken') || '';
const amount = sessionStorage.getItem('redeemAmount') || '0';
+2
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { REDEEM_TOKEN_TTL_SECONDS } from '@dukang/shared-types';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
const POLL_INTERVAL_MS = 2500;
@@ -37,6 +38,7 @@ function formatTimer(seconds: number) {
}
export default function RedeemPage() {
usePageView('benefit_redeem_start');
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const couponId = searchParams.get('couponId') ?? undefined;
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
type RedeemRecord = {
id: string;
@@ -46,6 +47,7 @@ function StarRating({
}
export default function RedeemSuccessPage() {
usePageView('redeem_success_view');
const navigate = useNavigate();
const location = useLocation();
const [serviceScore, setServiceScore] = useState(5);