feat: 从 React Context 迁移到 Redux,实现了:

1.  集中式状态管理 - PostHog 状态与应用状态统一管理
  2.  自动追踪机制 - Middleware 自动拦截 Redux actions 进行追踪
  3.  Redux DevTools 支持 - 可视化调试所有 PostHog 事件
  4.  离线事件缓存 - 网络恢复时自动刷新缓存事件
  5.  性能优化 Hooks - 提供轻量级 Hook 避免不必要的重渲染
This commit is contained in:
zdl
2025-10-28 20:51:10 +08:00
parent 6506cb222b
commit fb76e442f7
5 changed files with 900 additions and 33 deletions

View File

@@ -0,0 +1,281 @@
// src/store/middleware/posthogMiddleware.js
import { trackPageView } from '../../lib/posthog';
import { trackEvent } from '../slices/posthogSlice';
import { logger } from '../../utils/logger';
import {
ACTIVATION_EVENTS,
RETENTION_EVENTS,
SPECIAL_EVENTS,
REVENUE_EVENTS,
} from '../../lib/constants';
// ==================== 自动追踪规则配置 ====================
/**
* Action 到 PostHog 事件的映射
* 当这些 Redux actions 被 dispatch 时,自动追踪对应的 PostHog 事件
*/
const ACTION_TO_EVENT_MAP = {
// ==================== 登录/登出 ====================
'auth/login/fulfilled': {
event: ACTIVATION_EVENTS.USER_LOGGED_IN,
getProperties: (action) => ({
login_method: action.payload?.login_method || 'unknown',
user_id: action.payload?.user?.id,
}),
},
'auth/logout': {
event: SPECIAL_EVENTS.USER_LOGGED_OUT,
getProperties: () => ({}),
},
'auth/wechatLogin/fulfilled': {
event: ACTIVATION_EVENTS.USER_LOGGED_IN,
getProperties: (action) => ({
login_method: 'wechat',
user_id: action.payload?.user?.id,
}),
},
// ==================== Community/新闻模块 ====================
'communityData/fetchHotEvents/fulfilled': {
event: RETENTION_EVENTS.NEWS_LIST_VIEWED,
getProperties: (action) => ({
event_count: action.payload?.length || 0,
source: 'community_page',
}),
},
'communityData/fetchPopularKeywords/fulfilled': {
event: RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED,
getProperties: () => ({
feature: 'popular_keywords',
}),
},
// ==================== 搜索 ====================
'search/submit': {
event: RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED,
getProperties: (action) => ({
query: action.payload?.query,
category: action.payload?.category,
}),
},
'search/filterApplied': {
event: RETENTION_EVENTS.SEARCH_FILTER_APPLIED,
getProperties: (action) => ({
filter_type: action.payload?.filterType,
filter_value: action.payload?.filterValue,
}),
},
// ==================== 支付/订阅 ====================
'payment/initiated': {
event: REVENUE_EVENTS.PAYMENT_INITIATED,
getProperties: (action) => ({
amount: action.payload?.amount,
payment_method: action.payload?.method,
subscription_tier: action.payload?.tier,
}),
},
'payment/success': {
event: REVENUE_EVENTS.PAYMENT_SUCCESSFUL,
getProperties: (action) => ({
amount: action.payload?.amount,
transaction_id: action.payload?.transactionId,
subscription_tier: action.payload?.tier,
}),
},
'subscription/upgraded': {
event: REVENUE_EVENTS.SUBSCRIPTION_UPGRADED,
getProperties: (action) => ({
from_tier: action.payload?.fromTier,
to_tier: action.payload?.toTier,
}),
},
// ==================== 错误追踪 ====================
'error/occurred': {
event: SPECIAL_EVENTS.ERROR_OCCURRED,
getProperties: (action) => ({
error_type: action.payload?.errorType,
error_message: action.payload?.message,
stack_trace: action.payload?.stack,
}),
},
'api/error': {
event: SPECIAL_EVENTS.API_ERROR,
getProperties: (action) => ({
endpoint: action.payload?.endpoint,
status_code: action.payload?.statusCode,
error_message: action.payload?.message,
}),
},
};
// ==================== 页面路由追踪配置 ====================
/**
* 路由变化的 action type根据不同路由库调整
*/
const LOCATION_CHANGE_ACTIONS = [
'@@router/LOCATION_CHANGE', // Redux-first router
'router/navigate', // 自定义路由 action
];
/**
* 根据路径识别页面类型
*/
const getPageTypeFromPath = (pathname) => {
if (pathname === '/home' || pathname === '/') {
return { page_type: 'landing' };
} else if (pathname.startsWith('/auth/')) {
return { page_type: 'auth' };
} else if (pathname.startsWith('/community')) {
return { page_type: 'feature', feature_name: 'community' };
} else if (pathname.startsWith('/concepts')) {
return { page_type: 'feature', feature_name: 'concepts' };
} else if (pathname.startsWith('/stocks')) {
return { page_type: 'feature', feature_name: 'stocks' };
} else if (pathname.startsWith('/limit-analyse')) {
return { page_type: 'feature', feature_name: 'limit_analyse' };
} else if (pathname.startsWith('/trading-simulation')) {
return { page_type: 'feature', feature_name: 'trading_simulation' };
} else if (pathname.startsWith('/company')) {
return { page_type: 'detail', content_type: 'company' };
} else if (pathname.startsWith('/event-detail')) {
return { page_type: 'detail', content_type: 'event' };
}
return { page_type: 'other' };
};
// ==================== 中间件实现 ====================
/**
* PostHog Middleware
* 自动拦截 Redux actions 并追踪对应的 PostHog 事件
*/
const posthogMiddleware = (store) => (next) => (action) => {
// 先执行 action
const result = next(action);
// 获取当前 PostHog 状态
const state = store.getState();
const posthogState = state.posthog;
// 如果 PostHog 未初始化,不追踪(事件会被缓存到 eventQueue
if (!posthogState?.isInitialized) {
return result;
}
try {
// ==================== 1. 自动追踪特定 actions ====================
if (ACTION_TO_EVENT_MAP[action.type]) {
const { event, getProperties } = ACTION_TO_EVENT_MAP[action.type];
const properties = getProperties(action);
// 通过 dispatch 追踪事件(会走 Redux 状态管理)
store.dispatch(trackEvent({ eventName: event, properties }));
logger.debug('PostHog Middleware', `自动追踪事件: ${event}`, properties);
}
// ==================== 2. 路由变化追踪 ====================
if (LOCATION_CHANGE_ACTIONS.includes(action.type)) {
const location = action.payload?.location || action.payload;
const pathname = location?.pathname || window.location.pathname;
const search = location?.search || window.location.search;
// 识别页面类型
const pageProperties = getPageTypeFromPath(pathname);
// 追踪页面浏览
trackPageView(pathname, {
...pageProperties,
page_path: pathname,
page_search: search,
page_title: document.title,
referrer: document.referrer,
});
logger.debug('PostHog Middleware', `页面浏览追踪: ${pathname}`, pageProperties);
}
// ==================== 3. 离线事件处理 ====================
// 检测网络状态变化
if (action.type === 'network/online') {
// 恢复在线时,刷新缓存的事件
const { eventQueue } = posthogState;
if (eventQueue && eventQueue.length > 0) {
logger.info('PostHog Middleware', `网络恢复,刷新 ${eventQueue.length} 个缓存事件`);
// 这里可以 dispatch flushCachedEvents但为了避免循环依赖直接在 slice 中处理
}
}
// ==================== 4. 性能追踪(可选) ====================
// 追踪耗时较长的 actions
const startTime = action.meta?.startTime;
if (startTime) {
const duration = Date.now() - startTime;
if (duration > 1000) {
// 超过 1 秒的操作
store.dispatch(trackEvent({
eventName: SPECIAL_EVENTS.PAGE_LOAD_TIME,
properties: {
action_type: action.type,
duration_ms: duration,
is_slow: true,
},
}));
}
}
} catch (error) {
logger.error('PostHog Middleware', '追踪失败', error, { actionType: action.type });
}
return result;
};
// ==================== 工具函数 ====================
/**
* 创建带性能追踪的 action creator
* 用法: dispatch(withTiming(someAction(payload)))
*/
export const withTiming = (action) => ({
...action,
meta: {
...action.meta,
startTime: Date.now(),
},
});
/**
* 手动触发页面浏览追踪
* 用于非路由跳转的场景(如 Modal、Tab 切换)
*/
export const trackModalView = (modalName, properties = {}) => (dispatch) => {
dispatch(trackEvent({
eventName: '$pageview',
properties: {
modal_name: modalName,
page_type: 'modal',
...properties,
},
}));
};
/**
* 追踪 Tab 切换
*/
export const trackTabChange = (tabName, properties = {}) => (dispatch) => {
dispatch(trackEvent({
eventName: RETENTION_EVENTS.NEWS_TAB_CLICKED,
properties: {
tab_name: tabName,
...properties,
},
}));
};
// ==================== Export ====================
export default posthogMiddleware;