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