feat: 从 React Context 迁移到 Redux,实现了:
1. ✅ 集中式状态管理 - PostHog 状态与应用状态统一管理 2. ✅ 自动追踪机制 - Middleware 自动拦截 Redux actions 进行追踪 3. ✅ Redux DevTools 支持 - 可视化调试所有 PostHog 事件 4. ✅ 离线事件缓存 - 网络恢复时自动刷新缓存事件 5. ✅ 性能优化 Hooks - 提供轻量级 Hook 避免不必要的重渲染
This commit is contained in:
281
src/store/middleware/posthogMiddleware.js
Normal file
281
src/store/middleware/posthogMiddleware.js
Normal 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;
|
||||
Reference in New Issue
Block a user