Files
vf_react/src/store/middleware/posthogMiddleware.js
zdl fb76e442f7 feat: 从 React Context 迁移到 Redux,实现了:
1.  集中式状态管理 - PostHog 状态与应用状态统一管理
  2.  自动追踪机制 - Middleware 自动拦截 Redux actions 进行追踪
  3.  Redux DevTools 支持 - 可视化调试所有 PostHog 事件
  4.  离线事件缓存 - 网络恢复时自动刷新缓存事件
  5.  性能优化 Hooks - 提供轻量级 Hook 避免不必要的重渲染
2025-10-28 20:51:10 +08:00

282 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;