From fb76e442f79a0bfaa29e510ce31e3ffc6df4a9a6 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 28 Oct 2025 20:51:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BB=8E=20React=20Context=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=20Redux=EF=BC=8C=E5=AE=9E=E7=8E=B0=E4=BA=86?= =?UTF-8?q?=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. ✅ 集中式状态管理 - PostHog 状态与应用状态统一管理 2. ✅ 自动追踪机制 - Middleware 自动拦截 Redux actions 进行追踪 3. ✅ Redux DevTools 支持 - 可视化调试所有 PostHog 事件 4. ✅ 离线事件缓存 - 网络恢复时自动刷新缓存事件 5. ✅ 性能优化 Hooks - 提供轻量级 Hook 避免不必要的重渲染 --- src/App.js | 68 ++--- src/hooks/usePostHogRedux.js | 272 ++++++++++++++++++++ src/store/index.js | 13 +- src/store/middleware/posthogMiddleware.js | 281 ++++++++++++++++++++ src/store/slices/posthogSlice.js | 299 ++++++++++++++++++++++ 5 files changed, 900 insertions(+), 33 deletions(-) create mode 100644 src/hooks/usePostHogRedux.js create mode 100644 src/store/middleware/posthogMiddleware.js create mode 100644 src/store/slices/posthogSlice.js diff --git a/src/App.js b/src/App.js index 362942f4..33695b9c 100755 --- a/src/App.js +++ b/src/App.js @@ -59,9 +59,12 @@ import NotificationContainer from "components/NotificationContainer"; import ConnectionStatusBar from "components/ConnectionStatusBar"; import NotificationTestTool from "components/NotificationTestTool"; import ScrollToTop from "components/ScrollToTop"; -import PostHogProvider from "components/PostHogProvider"; import { logger } from "utils/logger"; +// PostHog Redux 集成 +import { useDispatch } from 'react-redux'; +import { initializePostHog } from "store/slices/posthogSlice"; + /** * ConnectionStatusBar 包装组件 * 需要在 NotificationProvider 内部使用,所以单独提取 @@ -109,6 +112,13 @@ function ConnectionStatusBarWrapper() { function AppContent() { const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + + // 🎯 PostHog Redux 初始化 + useEffect(() => { + dispatch(initializePostHog()); + logger.info('App', 'PostHog Redux 初始化已触发'); + }, [dispatch]); return ( @@ -296,34 +306,32 @@ export default function App() { }, []); return ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/src/hooks/usePostHogRedux.js b/src/hooks/usePostHogRedux.js new file mode 100644 index 00000000..d2c69cd2 --- /dev/null +++ b/src/hooks/usePostHogRedux.js @@ -0,0 +1,272 @@ +// src/hooks/usePostHogRedux.js +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + trackEvent, + identifyUser, + resetUser, + optIn, + optOut, + selectPostHog, + selectIsInitialized, + selectUser, + selectFeatureFlags, + selectFeatureFlag, + selectIsOptedOut, + selectStats, + flushCachedEvents, +} from '../store/slices/posthogSlice'; +import { trackPageView } from '../lib/posthog'; + +/** + * PostHog Redux Hook + * 提供便捷的 PostHog 功能访问接口 + * + * 用法示例: + * + * ```jsx + * import { usePostHogRedux } from 'hooks/usePostHogRedux'; + * import { RETENTION_EVENTS } from 'lib/constants'; + * + * function MyComponent() { + * const { track, identify, user, isInitialized } = usePostHogRedux(); + * + * const handleClick = () => { + * track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, { + * article_id: '123', + * article_title: '标题', + * }); + * }; + * + * if (!isInitialized) { + * return
正在加载...
; + * } + * + * return ( + *
+ * + * {user &&

当前用户: {user.userId}

} + *
+ * ); + * } + * ``` + */ +export const usePostHogRedux = () => { + const dispatch = useDispatch(); + + // Selectors + const posthog = useSelector(selectPostHog); + const isInitialized = useSelector(selectIsInitialized); + const user = useSelector(selectUser); + const featureFlags = useSelector(selectFeatureFlags); + const stats = useSelector(selectStats); + + // ==================== 追踪事件 ==================== + + /** + * 追踪自定义事件 + * @param {string} eventName - 事件名称(建议使用 constants.js 中的常量) + * @param {object} properties - 事件属性 + */ + const track = useCallback( + (eventName, properties = {}) => { + dispatch(trackEvent({ eventName, properties })); + }, + [dispatch] + ); + + /** + * 追踪页面浏览 + * @param {string} pagePath - 页面路径 + * @param {object} properties - 页面属性 + */ + const trackPage = useCallback( + (pagePath, properties = {}) => { + trackPageView(pagePath, properties); + }, + [] + ); + + // ==================== 用户管理 ==================== + + /** + * 识别用户(登录后调用) + * @param {string} userId - 用户 ID + * @param {object} userProperties - 用户属性 + */ + const identify = useCallback( + (userId, userProperties = {}) => { + dispatch(identifyUser({ userId, userProperties })); + }, + [dispatch] + ); + + /** + * 重置用户会话(登出时调用) + */ + const reset = useCallback(() => { + dispatch(resetUser()); + }, [dispatch]); + + // ==================== 隐私控制 ==================== + + /** + * 用户选择退出追踪 + */ + const optOutTracking = useCallback(() => { + dispatch(optOut()); + }, [dispatch]); + + /** + * 用户选择加入追踪 + */ + const optInTracking = useCallback(() => { + dispatch(optIn()); + }, [dispatch]); + + /** + * 检查用户是否已退出追踪 + */ + const isOptedOut = selectIsOptedOut(); + + // ==================== Feature Flags ==================== + + /** + * 获取特定 Feature Flag 的值 + * @param {string} flagKey - Flag 键名 + * @returns {any} Flag 值 + */ + const getFlag = useCallback( + (flagKey) => { + return selectFeatureFlag(flagKey)({ posthog }); + }, + [posthog] + ); + + /** + * 检查 Feature Flag 是否启用 + * @param {string} flagKey - Flag 键名 + * @returns {boolean} + */ + const isEnabled = useCallback( + (flagKey) => { + const value = getFlag(flagKey); + return Boolean(value); + }, + [getFlag] + ); + + // ==================== 离线事件管理 ==================== + + /** + * 刷新缓存的离线事件 + */ + const flushEvents = useCallback(() => { + dispatch(flushCachedEvents()); + }, [dispatch]); + + // ==================== 返回接口 ==================== + + return { + // 状态 + isInitialized, + user, + featureFlags, + stats, + posthog, // 完整的 PostHog 状态 + + // 追踪方法 + track, + trackPage, + + // 用户管理 + identify, + reset, + + // 隐私控制 + optOut: optOutTracking, + optIn: optInTracking, + isOptedOut, + + // Feature Flags + getFlag, + isEnabled, + + // 离线事件 + flushEvents, + }; +}; + +// ==================== 便捷 Hooks ==================== + +/** + * 仅获取追踪功能的 Hook(性能优化) + */ +export const usePostHogTrack = () => { + const dispatch = useDispatch(); + + const track = useCallback( + (eventName, properties = {}) => { + dispatch(trackEvent({ eventName, properties })); + }, + [dispatch] + ); + + return { track }; +}; + +/** + * 仅获取 Feature Flags 的 Hook(性能优化) + */ +export const usePostHogFlags = () => { + const featureFlags = useSelector(selectFeatureFlags); + const posthog = useSelector(selectPostHog); + + const getFlag = useCallback( + (flagKey) => { + return selectFeatureFlag(flagKey)({ posthog }); + }, + [posthog] + ); + + const isEnabled = useCallback( + (flagKey) => { + const value = getFlag(flagKey); + return Boolean(value); + }, + [getFlag] + ); + + return { + featureFlags, + getFlag, + isEnabled, + }; +}; + +/** + * 获取用户信息的 Hook(性能优化) + */ +export const usePostHogUser = () => { + const user = useSelector(selectUser); + const dispatch = useDispatch(); + + const identify = useCallback( + (userId, userProperties = {}) => { + dispatch(identifyUser({ userId, userProperties })); + }, + [dispatch] + ); + + const reset = useCallback(() => { + dispatch(resetUser()); + }, [dispatch]); + + return { + user, + identify, + reset, + }; +}; + +export default usePostHogRedux; diff --git a/src/store/index.js b/src/store/index.js index 52a809b0..33789148 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,18 +1,25 @@ // src/store/index.js import { configureStore } from '@reduxjs/toolkit'; import communityDataReducer from './slices/communityDataSlice'; +import posthogReducer from './slices/posthogSlice'; +import posthogMiddleware from './middleware/posthogMiddleware'; export const store = configureStore({ reducer: { - communityData: communityDataReducer + communityData: communityDataReducer, + posthog: posthogReducer, // ✅ PostHog Redux 状态管理 }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { // 忽略这些 action types 的序列化检查 - ignoredActions: ['communityData/fetchPopularKeywords/fulfilled', 'communityData/fetchHotEvents/fulfilled'], + ignoredActions: [ + 'communityData/fetchPopularKeywords/fulfilled', + 'communityData/fetchHotEvents/fulfilled', + 'posthog/trackEvent/fulfilled', // ✅ PostHog 事件追踪 + ], }, - }), + }).concat(posthogMiddleware), // ✅ PostHog 自动追踪中间件 }); export default store; diff --git a/src/store/middleware/posthogMiddleware.js b/src/store/middleware/posthogMiddleware.js new file mode 100644 index 00000000..feec5813 --- /dev/null +++ b/src/store/middleware/posthogMiddleware.js @@ -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; diff --git a/src/store/slices/posthogSlice.js b/src/store/slices/posthogSlice.js new file mode 100644 index 00000000..8b5a4287 --- /dev/null +++ b/src/store/slices/posthogSlice.js @@ -0,0 +1,299 @@ +// 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;