Files
vf_react/src/store/slices/posthogSlice.js
zdl 3ba8944b96 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

300 lines
7.8 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/slices/posthogSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import {
initPostHog,
identifyUser as posthogIdentifyUser,
resetUser as posthogResetUser,
trackEvent as posthogTrackEvent,
getFeatureFlag as posthogGetFeatureFlag,
optIn as posthogOptIn,
optOut as posthogOptOut,
hasOptedOut as posthogHasOptedOut
} from '../../lib/posthog';
import { logger } from '../../utils/logger';
// ==================== Initial State ====================
const initialState = {
// 初始化状态
isInitialized: false,
initError: null,
// 用户信息
user: null,
// 事件队列(用于离线缓存)
eventQueue: [],
// Feature Flags
featureFlags: {},
// 配置
config: {
apiKey: process.env.REACT_APP_POSTHOG_KEY || null,
apiHost: process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com',
sessionRecording: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true',
},
// 统计
stats: {
totalEvents: 0,
lastEventTime: null,
},
};
// ==================== Async Thunks ====================
/**
* 初始化 PostHog SDK
*/
export const initializePostHog = createAsyncThunk(
'posthog/initialize',
async (_, { getState, rejectWithValue }) => {
try {
const { config } = getState().posthog;
if (!config.apiKey) {
logger.warn('PostHog', '未配置 API Key分析功能将被禁用');
return { isInitialized: false, warning: 'No API Key' };
}
// 调用 PostHog SDK 初始化
initPostHog();
logger.info('PostHog', 'Redux 初始化成功');
return { isInitialized: true };
} catch (error) {
logger.error('PostHog', '初始化失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 识别用户
*/
export const identifyUser = createAsyncThunk(
'posthog/identifyUser',
async ({ userId, userProperties }, { rejectWithValue }) => {
try {
posthogIdentifyUser(userId, userProperties);
logger.info('PostHog', '用户已识别', { userId });
return { userId, userProperties };
} catch (error) {
logger.error('PostHog', '用户识别失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 重置用户会话(登出)
*/
export const resetUser = createAsyncThunk(
'posthog/resetUser',
async (_, { rejectWithValue }) => {
try {
posthogResetUser();
logger.info('PostHog', '用户会话已重置');
return {};
} catch (error) {
logger.error('PostHog', '重置用户会话失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 追踪事件
*/
export const trackEvent = createAsyncThunk(
'posthog/trackEvent',
async ({ eventName, properties = {} }, { getState, rejectWithValue }) => {
try {
const { isInitialized } = getState().posthog;
if (!isInitialized) {
logger.warn('PostHog', 'PostHog 未初始化,事件将被缓存', { eventName });
return { eventName, properties, cached: true };
}
posthogTrackEvent(eventName, properties);
return {
eventName,
properties,
timestamp: new Date().toISOString(),
cached: false
};
} catch (error) {
logger.error('PostHog', '追踪事件失败', error, { eventName });
return rejectWithValue(error.message);
}
}
);
/**
* 获取所有 Feature Flags
*/
export const fetchFeatureFlags = createAsyncThunk(
'posthog/fetchFeatureFlags',
async (_, { rejectWithValue }) => {
try {
// PostHog SDK 会在初始化时自动获取 feature flags
// 这里只是读取缓存的值
const flags = {};
logger.info('PostHog', 'Feature Flags 已更新');
return flags;
} catch (error) {
logger.error('PostHog', '获取 Feature Flags 失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 刷新缓存的离线事件
*/
export const flushCachedEvents = createAsyncThunk(
'posthog/flushCachedEvents',
async (_, { getState, dispatch }) => {
try {
const { eventQueue, isInitialized } = getState().posthog;
if (!isInitialized || eventQueue.length === 0) {
return { flushed: 0 };
}
logger.info('PostHog', `刷新 ${eventQueue.length} 个缓存事件`);
// 批量发送缓存的事件
for (const { eventName, properties } of eventQueue) {
dispatch(trackEvent({ eventName, properties }));
}
return { flushed: eventQueue.length };
} catch (error) {
logger.error('PostHog', '刷新缓存事件失败', error);
return { flushed: 0, error: error.message };
}
}
);
// ==================== Slice ====================
const posthogSlice = createSlice({
name: 'posthog',
initialState,
reducers: {
// 设置 Feature Flag
setFeatureFlag: (state, action) => {
const { flagKey, value } = action.payload;
state.featureFlags[flagKey] = value;
},
// 清空事件队列
clearEventQueue: (state) => {
state.eventQueue = [];
},
// 更新配置
updateConfig: (state, action) => {
state.config = { ...state.config, ...action.payload };
},
// 用户 Opt-in
optIn: (state) => {
posthogOptIn();
logger.info('PostHog', '用户已选择加入追踪');
},
// 用户 Opt-out
optOut: (state) => {
posthogOptOut();
logger.info('PostHog', '用户已选择退出追踪');
},
},
extraReducers: (builder) => {
// 初始化
builder.addCase(initializePostHog.fulfilled, (state, action) => {
state.isInitialized = action.payload.isInitialized;
state.initError = null;
});
builder.addCase(initializePostHog.rejected, (state, action) => {
state.isInitialized = false;
state.initError = action.payload;
});
// 识别用户
builder.addCase(identifyUser.fulfilled, (state, action) => {
state.user = {
userId: action.payload.userId,
...action.payload.userProperties,
};
});
// 重置用户
builder.addCase(resetUser.fulfilled, (state) => {
state.user = null;
state.featureFlags = {};
});
// 追踪事件
builder.addCase(trackEvent.fulfilled, (state, action) => {
const { eventName, properties, timestamp, cached } = action.payload;
// 如果事件被缓存,添加到队列
if (cached) {
state.eventQueue.push({ eventName, properties, timestamp });
} else {
// 更新统计
state.stats.totalEvents += 1;
state.stats.lastEventTime = timestamp;
}
});
// 刷新缓存事件
builder.addCase(flushCachedEvents.fulfilled, (state, action) => {
if (action.payload.flushed > 0) {
state.eventQueue = [];
state.stats.totalEvents += action.payload.flushed;
}
});
// 获取 Feature Flags
builder.addCase(fetchFeatureFlags.fulfilled, (state, action) => {
state.featureFlags = action.payload;
});
},
});
// ==================== Actions ====================
export const {
setFeatureFlag,
clearEventQueue,
updateConfig,
optIn,
optOut,
} = posthogSlice.actions;
// ==================== Selectors ====================
export const selectPostHog = (state) => state.posthog;
export const selectIsInitialized = (state) => state.posthog.isInitialized;
export const selectUser = (state) => state.posthog.user;
export const selectFeatureFlags = (state) => state.posthog.featureFlags;
export const selectEventQueue = (state) => state.posthog.eventQueue;
export const selectStats = (state) => state.posthog.stats;
export const selectFeatureFlag = (flagKey) => (state) => {
return state.posthog.featureFlags[flagKey] || posthogGetFeatureFlag(flagKey);
};
export const selectIsOptedOut = () => posthogHasOptedOut();
// ==================== Export ====================
export default posthogSlice.reducer;