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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
Reference in New Issue
Block a user