diff --git a/src/hooks/useNavigationEvents.js b/src/hooks/useNavigationEvents.js new file mode 100644 index 00000000..2ad99373 --- /dev/null +++ b/src/hooks/useNavigationEvents.js @@ -0,0 +1,293 @@ +// src/hooks/useNavigationEvents.js +// 导航和菜单事件追踪 Hook + +import { useCallback } from 'react'; +import { usePostHogTrack } from './usePostHogRedux'; +import { RETENTION_EVENTS } from '../lib/constants'; +import { logger } from '../utils/logger'; + +/** + * 导航事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {string} options.component - 组件名称 ('top_nav' | 'sidebar' | 'breadcrumb' | 'footer') + * @returns {Object} 事件追踪处理函数集合 + */ +export const useNavigationEvents = ({ component = 'navigation' } = {}) => { + const { track } = usePostHogTrack(); + + /** + * 追踪顶部导航点击 + * @param {string} itemName - 导航项名称 + * @param {string} path - 导航目标路径 + * @param {string} category - 导航分类 ('main' | 'user' | 'utility') + */ + const trackTopNavClicked = useCallback((itemName, path = '', category = 'main') => { + if (!itemName) { + logger.warn('useNavigationEvents', 'trackTopNavClicked: itemName is required'); + return; + } + + track(RETENTION_EVENTS.TOP_NAV_CLICKED, { + item_name: itemName, + path, + category, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🔝 Top Navigation Clicked', { + itemName, + path, + category, + }); + }, [track, component]); + + /** + * 追踪侧边栏菜单点击 + * @param {string} itemName - 菜单项名称 + * @param {string} path - 目标路径 + * @param {number} level - 菜单层级 (1=主菜单, 2=子菜单) + * @param {boolean} isExpanded - 是否展开状态 + */ + const trackSidebarMenuClicked = useCallback((itemName, path = '', level = 1, isExpanded = false) => { + if (!itemName) { + logger.warn('useNavigationEvents', 'trackSidebarMenuClicked: itemName is required'); + return; + } + + track(RETENTION_EVENTS.SIDEBAR_MENU_CLICKED, { + item_name: itemName, + path, + level, + is_expanded: isExpanded, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '📂 Sidebar Menu Clicked', { + itemName, + path, + level, + isExpanded, + }); + }, [track, component]); + + /** + * 追踪通用菜单项点击 + * @param {string} itemName - 菜单项名称 + * @param {string} menuType - 菜单类型 ('dropdown' | 'context' | 'tab') + * @param {string} path - 目标路径 + */ + const trackMenuItemClicked = useCallback((itemName, menuType = 'dropdown', path = '') => { + if (!itemName) { + logger.warn('useNavigationEvents', 'trackMenuItemClicked: itemName is required'); + return; + } + + track(RETENTION_EVENTS.MENU_ITEM_CLICKED, { + item_name: itemName, + menu_type: menuType, + path, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '📋 Menu Item Clicked', { + itemName, + menuType, + path, + }); + }, [track, component]); + + /** + * 追踪面包屑导航点击 + * @param {string} itemName - 面包屑项名称 + * @param {string} path - 目标路径 + * @param {number} position - 在面包屑中的位置 + * @param {number} totalItems - 面包屑总项数 + */ + const trackBreadcrumbClicked = useCallback((itemName, path = '', position = 0, totalItems = 0) => { + if (!itemName) { + logger.warn('useNavigationEvents', 'trackBreadcrumbClicked: itemName is required'); + return; + } + + track(RETENTION_EVENTS.BREADCRUMB_CLICKED, { + item_name: itemName, + path, + position, + total_items: totalItems, + is_last: position === totalItems - 1, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🍞 Breadcrumb Clicked', { + itemName, + position, + totalItems, + }); + }, [track, component]); + + /** + * 追踪Logo点击(返回首页) + */ + const trackLogoClicked = useCallback(() => { + track('Logo Clicked', { + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🏠 Logo Clicked'); + }, [track, component]); + + /** + * 追踪用户菜单展开 + * @param {Object} user - 用户对象 + * @param {number} menuItemCount - 菜单项数量 + */ + const trackUserMenuOpened = useCallback((user = {}, menuItemCount = 0) => { + track('User Menu Opened', { + user_id: user.id || null, + menu_item_count: menuItemCount, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '👤 User Menu Opened', { + userId: user.id, + menuItemCount, + }); + }, [track, component]); + + /** + * 追踪通知中心打开 + * @param {number} unreadCount - 未读通知数量 + */ + const trackNotificationCenterOpened = useCallback((unreadCount = 0) => { + track('Notification Center Opened', { + unread_count: unreadCount, + has_unread: unreadCount > 0, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🔔 Notification Center Opened', { + unreadCount, + }); + }, [track, component]); + + /** + * 追踪语言切换 + * @param {string} fromLanguage - 原语言 + * @param {string} toLanguage - 目标语言 + */ + const trackLanguageChanged = useCallback((fromLanguage, toLanguage) => { + if (!fromLanguage || !toLanguage) { + logger.warn('useNavigationEvents', 'trackLanguageChanged: both languages are required'); + return; + } + + track('Language Changed', { + from_language: fromLanguage, + to_language: toLanguage, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🌐 Language Changed', { + fromLanguage, + toLanguage, + }); + }, [track, component]); + + /** + * 追踪主题切换(深色/浅色模式) + * @param {string} fromTheme - 原主题 + * @param {string} toTheme - 目标主题 + */ + const trackThemeChanged = useCallback((fromTheme, toTheme) => { + if (!fromTheme || !toTheme) { + logger.warn('useNavigationEvents', 'trackThemeChanged: both themes are required'); + return; + } + + track('Theme Changed', { + from_theme: fromTheme, + to_theme: toTheme, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '🎨 Theme Changed', { + fromTheme, + toTheme, + }); + }, [track, component]); + + /** + * 追踪快捷键使用 + * @param {string} shortcut - 快捷键组合 (如 'Ctrl+K', 'Cmd+/') + * @param {string} action - 触发的动作 + */ + const trackShortcutUsed = useCallback((shortcut, action = '') => { + if (!shortcut) { + logger.warn('useNavigationEvents', 'trackShortcutUsed: shortcut is required'); + return; + } + + track('Keyboard Shortcut Used', { + shortcut, + action, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '⌨️ Keyboard Shortcut Used', { + shortcut, + action, + }); + }, [track, component]); + + /** + * 追踪返回按钮点击 + * @param {string} fromPage - 当前页面 + * @param {string} toPage - 返回到的页面 + */ + const trackBackButtonClicked = useCallback((fromPage = '', toPage = '') => { + track('Back Button Clicked', { + from_page: fromPage, + to_page: toPage, + component, + timestamp: new Date().toISOString(), + }); + + logger.debug('useNavigationEvents', '◀️ Back Button Clicked', { + fromPage, + toPage, + }); + }, [track, component]); + + return { + // 导航点击事件 + trackTopNavClicked, + trackSidebarMenuClicked, + trackMenuItemClicked, + trackBreadcrumbClicked, + trackLogoClicked, + + // 用户交互事件 + trackUserMenuOpened, + trackNotificationCenterOpened, + + // 设置变更事件 + trackLanguageChanged, + trackThemeChanged, + + // 其他交互 + trackShortcutUsed, + trackBackButtonClicked, + }; +}; + +export default useNavigationEvents; diff --git a/src/hooks/useProfileEvents.js b/src/hooks/useProfileEvents.js new file mode 100644 index 00000000..6b580b8e --- /dev/null +++ b/src/hooks/useProfileEvents.js @@ -0,0 +1,334 @@ +// src/hooks/useProfileEvents.js +// 个人资料和设置事件追踪 Hook + +import { useCallback } from 'react'; +import { usePostHogTrack } from './usePostHogRedux'; +import { RETENTION_EVENTS } from '../lib/constants'; +import { logger } from '../utils/logger'; + +/** + * 个人资料和设置事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {string} options.pageType - 页面类型 ('profile' | 'settings' | 'security') + * @returns {Object} 事件追踪处理函数集合 + */ +export const useProfileEvents = ({ pageType = 'profile' } = {}) => { + const { track } = usePostHogTrack(); + + /** + * 追踪个人资料字段编辑开始 + * @param {string} fieldName - 字段名称 ('nickname' | 'email' | 'phone' | 'avatar' | 'bio') + */ + const trackProfileFieldEditStarted = useCallback((fieldName) => { + if (!fieldName) { + logger.warn('useProfileEvents', 'trackProfileFieldEditStarted: fieldName is required'); + return; + } + + track('Profile Field Edit Started', { + field_name: fieldName, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '✏️ Profile Field Edit Started', { + fieldName, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪个人资料更新成功 + * @param {Array} updatedFields - 更新的字段列表 + * @param {Object} changes - 变更详情 + */ + const trackProfileUpdated = useCallback((updatedFields = [], changes = {}) => { + if (!updatedFields || updatedFields.length === 0) { + logger.warn('useProfileEvents', 'trackProfileUpdated: updatedFields array is required'); + return; + } + + track(RETENTION_EVENTS.PROFILE_UPDATED, { + updated_fields: updatedFields, + field_count: updatedFields.length, + changes: changes, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '✅ Profile Updated', { + updatedFields, + fieldCount: updatedFields.length, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪个人资料更新失败 + * @param {Array} attemptedFields - 尝试更新的字段 + * @param {string} errorMessage - 错误信息 + */ + const trackProfileUpdateFailed = useCallback((attemptedFields = [], errorMessage = '') => { + track('Profile Update Failed', { + attempted_fields: attemptedFields, + error_message: errorMessage, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '❌ Profile Update Failed', { + attemptedFields, + errorMessage, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪头像上传 + * @param {string} uploadMethod - 上传方式 ('file_upload' | 'url' | 'camera' | 'default_avatar') + * @param {number} fileSize - 文件大小(bytes) + */ + const trackAvatarUploaded = useCallback((uploadMethod = 'file_upload', fileSize = 0) => { + track('Avatar Uploaded', { + upload_method: uploadMethod, + file_size: fileSize, + file_size_mb: (fileSize / (1024 * 1024)).toFixed(2), + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '🖼️ Avatar Uploaded', { + uploadMethod, + fileSize, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪密码更改 + * @param {boolean} success - 是否成功 + * @param {string} errorReason - 失败原因 + */ + const trackPasswordChanged = useCallback((success = true, errorReason = '') => { + track('Password Changed', { + success, + error_reason: errorReason || null, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', success ? '🔒 Password Changed Successfully' : '❌ Password Change Failed', { + success, + errorReason, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪邮箱验证发起 + * @param {string} email - 邮箱地址 + */ + const trackEmailVerificationSent = useCallback((email = '') => { + track('Email Verification Sent', { + email_provided: Boolean(email), + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '📧 Email Verification Sent', { + emailProvided: Boolean(email), + pageType, + }); + }, [track, pageType]); + + /** + * 追踪手机号验证发起 + * @param {string} phone - 手机号 + */ + const trackPhoneVerificationSent = useCallback((phone = '') => { + track('Phone Verification Sent', { + phone_provided: Boolean(phone), + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '📱 Phone Verification Sent', { + phoneProvided: Boolean(phone), + pageType, + }); + }, [track, pageType]); + + /** + * 追踪账号绑定(微信、邮箱、手机等) + * @param {string} accountType - 账号类型 ('wechat' | 'email' | 'phone') + * @param {boolean} success - 是否成功 + */ + const trackAccountBound = useCallback((accountType, success = true) => { + if (!accountType) { + logger.warn('useProfileEvents', 'trackAccountBound: accountType is required'); + return; + } + + track('Account Bound', { + account_type: accountType, + success, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', success ? '🔗 Account Bound' : '❌ Account Bind Failed', { + accountType, + success, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪账号解绑 + * @param {string} accountType - 账号类型 + * @param {boolean} success - 是否成功 + */ + const trackAccountUnbound = useCallback((accountType, success = true) => { + if (!accountType) { + logger.warn('useProfileEvents', 'trackAccountUnbound: accountType is required'); + return; + } + + track('Account Unbound', { + account_type: accountType, + success, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', success ? '🔓 Account Unbound' : '❌ Account Unbind Failed', { + accountType, + success, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪设置项更改 + * @param {string} settingName - 设置名称 + * @param {any} oldValue - 旧值 + * @param {any} newValue - 新值 + * @param {string} category - 设置分类 ('notification' | 'privacy' | 'display' | 'advanced') + */ + const trackSettingChanged = useCallback((settingName, oldValue, newValue, category = 'general') => { + if (!settingName) { + logger.warn('useProfileEvents', 'trackSettingChanged: settingName is required'); + return; + } + + track(RETENTION_EVENTS.SETTINGS_CHANGED, { + setting_name: settingName, + old_value: String(oldValue), + new_value: String(newValue), + category, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '⚙️ Setting Changed', { + settingName, + oldValue, + newValue, + category, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪通知偏好更改 + * @param {Object} preferences - 通知偏好设置 + * @param {boolean} preferences.email - 邮件通知 + * @param {boolean} preferences.push - 推送通知 + * @param {boolean} preferences.sms - 短信通知 + */ + const trackNotificationPreferencesChanged = useCallback((preferences = {}) => { + track('Notification Preferences Changed', { + email_enabled: preferences.email || false, + push_enabled: preferences.push || false, + sms_enabled: preferences.sms || false, + total_enabled: Object.values(preferences).filter(Boolean).length, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '🔔 Notification Preferences Changed', { + preferences, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪隐私设置更改 + * @param {string} privacySetting - 隐私设置名称 + * @param {boolean} isPublic - 是否公开 + */ + const trackPrivacySettingChanged = useCallback((privacySetting, isPublic = false) => { + if (!privacySetting) { + logger.warn('useProfileEvents', 'trackPrivacySettingChanged: privacySetting is required'); + return; + } + + track('Privacy Setting Changed', { + privacy_setting: privacySetting, + is_public: isPublic, + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '🔐 Privacy Setting Changed', { + privacySetting, + isPublic, + pageType, + }); + }, [track, pageType]); + + /** + * 追踪账号删除请求 + * @param {string} reason - 删除原因 + */ + const trackAccountDeletionRequested = useCallback((reason = '') => { + track('Account Deletion Requested', { + reason, + has_reason: Boolean(reason), + page_type: pageType, + timestamp: new Date().toISOString(), + }); + + logger.debug('useProfileEvents', '🗑️ Account Deletion Requested', { + reason, + pageType, + }); + }, [track, pageType]); + + return { + // 个人资料编辑 + trackProfileFieldEditStarted, + trackProfileUpdated, + trackProfileUpdateFailed, + trackAvatarUploaded, + + // 安全和验证 + trackPasswordChanged, + trackEmailVerificationSent, + trackPhoneVerificationSent, + + // 账号绑定 + trackAccountBound, + trackAccountUnbound, + + // 设置更改 + trackSettingChanged, + trackNotificationPreferencesChanged, + trackPrivacySettingChanged, + + // 账号管理 + trackAccountDeletionRequested, + }; +}; + +export default useProfileEvents; diff --git a/src/hooks/useSearchEvents.js b/src/hooks/useSearchEvents.js new file mode 100644 index 00000000..bd120042 --- /dev/null +++ b/src/hooks/useSearchEvents.js @@ -0,0 +1,244 @@ +// src/hooks/useSearchEvents.js +// 全局搜索功能事件追踪 Hook + +import { useCallback } from 'react'; +import { usePostHogTrack } from './usePostHogRedux'; +import { RETENTION_EVENTS } from '../lib/constants'; +import { logger } from '../utils/logger'; + +/** + * 全局搜索事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {string} options.context - 搜索上下文 ('global' | 'stock' | 'news' | 'concept' | 'simulation') + * @returns {Object} 事件追踪处理函数集合 + */ +export const useSearchEvents = ({ context = 'global' } = {}) => { + const { track } = usePostHogTrack(); + + /** + * 追踪搜索开始(聚焦搜索框) + * @param {string} placeholder - 搜索框提示文本 + */ + const trackSearchInitiated = useCallback((placeholder = '') => { + track(RETENTION_EVENTS.SEARCH_INITIATED, { + context, + placeholder, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '🔍 Search Initiated', { + context, + placeholder, + }); + }, [track, context]); + + /** + * 追踪搜索查询提交 + * @param {string} query - 搜索查询词 + * @param {number} resultCount - 搜索结果数量 + * @param {Object} filters - 应用的筛选条件 + */ + const trackSearchQuerySubmitted = useCallback((query, resultCount = 0, filters = {}) => { + if (!query) { + logger.warn('useSearchEvents', 'trackSearchQuerySubmitted: query is required'); + return; + } + + track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, { + query, + query_length: query.length, + result_count: resultCount, + has_results: resultCount > 0, + context, + filters: filters, + filter_count: Object.keys(filters).length, + timestamp: new Date().toISOString(), + }); + + // 如果没有搜索结果,额外追踪 + if (resultCount === 0) { + track(RETENTION_EVENTS.SEARCH_NO_RESULTS, { + query, + context, + filters, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '❌ Search No Results', { + query, + context, + }); + } else { + logger.debug('useSearchEvents', '✅ Search Query Submitted', { + query, + resultCount, + context, + }); + } + }, [track, context]); + + /** + * 追踪搜索结果点击 + * @param {Object} result - 被点击的搜索结果 + * @param {string} result.type - 结果类型 ('stock' | 'news' | 'concept' | 'event') + * @param {string} result.id - 结果ID + * @param {string} result.title - 结果标题 + * @param {number} position - 在搜索结果中的位置 + * @param {string} query - 搜索查询词 + */ + const trackSearchResultClicked = useCallback((result, position = 0, query = '') => { + if (!result || !result.type) { + logger.warn('useSearchEvents', 'trackSearchResultClicked: result object with type is required'); + return; + } + + track(RETENTION_EVENTS.SEARCH_RESULT_CLICKED, { + result_type: result.type, + result_id: result.id || result.code || '', + result_title: result.title || result.name || '', + position, + query, + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '🎯 Search Result Clicked', { + type: result.type, + id: result.id || result.code, + position, + context, + }); + }, [track, context]); + + /** + * 追踪搜索筛选应用 + * @param {Object} filters - 应用的筛选条件 + * @param {string} filterType - 筛选类型 ('sort' | 'category' | 'date_range' | 'price_range') + * @param {any} filterValue - 筛选值 + */ + const trackSearchFilterApplied = useCallback((filterType, filterValue, filters = {}) => { + if (!filterType) { + logger.warn('useSearchEvents', 'trackSearchFilterApplied: filterType is required'); + return; + } + + track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, { + filter_type: filterType, + filter_value: String(filterValue), + all_filters: filters, + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '🔍 Search Filter Applied', { + filterType, + filterValue, + context, + }); + }, [track, context]); + + /** + * 追踪搜索建议点击(自动完成) + * @param {string} suggestion - 被点击的搜索建议 + * @param {number} position - 在建议列表中的位置 + * @param {string} source - 建议来源 ('history' | 'popular' | 'related') + */ + const trackSearchSuggestionClicked = useCallback((suggestion, position = 0, source = 'popular') => { + if (!suggestion) { + logger.warn('useSearchEvents', 'trackSearchSuggestionClicked: suggestion is required'); + return; + } + + track('Search Suggestion Clicked', { + suggestion, + position, + source, + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '💡 Search Suggestion Clicked', { + suggestion, + position, + source, + context, + }); + }, [track, context]); + + /** + * 追踪搜索历史查看 + * @param {number} historyCount - 历史记录数量 + */ + const trackSearchHistoryViewed = useCallback((historyCount = 0) => { + track('Search History Viewed', { + history_count: historyCount, + has_history: historyCount > 0, + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '📜 Search History Viewed', { + historyCount, + context, + }); + }, [track, context]); + + /** + * 追踪搜索历史清除 + */ + const trackSearchHistoryCleared = useCallback(() => { + track('Search History Cleared', { + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '🗑️ Search History Cleared', { + context, + }); + }, [track, context]); + + /** + * 追踪热门搜索词点击 + * @param {string} keyword - 被点击的热门关键词 + * @param {number} position - 在列表中的位置 + * @param {number} heatScore - 热度分数 + */ + const trackPopularKeywordClicked = useCallback((keyword, position = 0, heatScore = 0) => { + if (!keyword) { + logger.warn('useSearchEvents', 'trackPopularKeywordClicked: keyword is required'); + return; + } + + track('Popular Keyword Clicked', { + keyword, + position, + heat_score: heatScore, + context, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSearchEvents', '🔥 Popular Keyword Clicked', { + keyword, + position, + context, + }); + }, [track, context]); + + return { + // 搜索流程事件 + trackSearchInitiated, + trackSearchQuerySubmitted, + trackSearchResultClicked, + + // 筛选和建议 + trackSearchFilterApplied, + trackSearchSuggestionClicked, + + // 历史和热门 + trackSearchHistoryViewed, + trackSearchHistoryCleared, + trackPopularKeywordClicked, + }; +}; + +export default useSearchEvents; diff --git a/src/hooks/useSubscriptionEvents.js b/src/hooks/useSubscriptionEvents.js new file mode 100644 index 00000000..c6fdfe3f --- /dev/null +++ b/src/hooks/useSubscriptionEvents.js @@ -0,0 +1,394 @@ +// src/hooks/useSubscriptionEvents.js +// 订阅和支付事件追踪 Hook + +import { useCallback } from 'react'; +import { usePostHogTrack } from './usePostHogRedux'; +import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants'; +import { logger } from '../utils/logger'; + +/** + * 订阅和支付事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {Object} options.currentSubscription - 当前订阅信息 + * @returns {Object} 事件追踪处理函数集合 + */ +export const useSubscriptionEvents = ({ currentSubscription = null } = {}) => { + const { track } = usePostHogTrack(); + + /** + * 追踪付费墙展示 + * @param {string} feature - 被限制的功能名称 + * @param {string} requiredPlan - 需要的订阅计划 + * @param {string} triggerLocation - 触发位置 + */ + const trackPaywallShown = useCallback((feature, requiredPlan = 'pro', triggerLocation = '') => { + if (!feature) { + logger.warn('useSubscriptionEvents', 'trackPaywallShown: feature is required'); + return; + } + + track(REVENUE_EVENTS.PAYWALL_SHOWN, { + feature, + required_plan: requiredPlan, + current_plan: currentSubscription?.plan || 'free', + trigger_location: triggerLocation, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '🚧 Paywall Shown', { + feature, + requiredPlan, + triggerLocation, + }); + }, [track, currentSubscription]); + + /** + * 追踪付费墙关闭 + * @param {string} feature - 功能名称 + * @param {string} closeMethod - 关闭方式 ('dismiss' | 'upgrade_clicked' | 'back_button') + */ + const trackPaywallDismissed = useCallback((feature, closeMethod = 'dismiss') => { + if (!feature) { + logger.warn('useSubscriptionEvents', 'trackPaywallDismissed: feature is required'); + return; + } + + track(REVENUE_EVENTS.PAYWALL_DISMISSED, { + feature, + close_method: closeMethod, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '❌ Paywall Dismissed', { + feature, + closeMethod, + }); + }, [track, currentSubscription]); + + /** + * 追踪升级按钮点击 + * @param {string} targetPlan - 目标订阅计划 + * @param {string} source - 来源位置 + * @param {string} feature - 关联的功能(如果从付费墙点击) + */ + const trackUpgradePlanClicked = useCallback((targetPlan = 'pro', source = '', feature = '') => { + track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, { + current_plan: currentSubscription?.plan || 'free', + target_plan: targetPlan, + source, + feature: feature || null, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '⬆️ Upgrade Plan Clicked', { + currentPlan: currentSubscription?.plan, + targetPlan, + source, + feature, + }); + }, [track, currentSubscription]); + + /** + * 追踪订阅页面查看 + * @param {string} source - 来源 + */ + const trackSubscriptionPageViewed = useCallback((source = '') => { + track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, { + current_plan: currentSubscription?.plan || 'free', + subscription_status: currentSubscription?.status || 'unknown', + is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free', + source, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '💳 Subscription Page Viewed', { + currentPlan: currentSubscription?.plan, + source, + }); + }, [track, currentSubscription]); + + /** + * 追踪定价计划查看 + * @param {string} planName - 计划名称 ('free' | 'pro' | 'enterprise') + * @param {number} price - 价格 + */ + const trackPricingPlanViewed = useCallback((planName, price = 0) => { + if (!planName) { + logger.warn('useSubscriptionEvents', 'trackPricingPlanViewed: planName is required'); + return; + } + + track('Pricing Plan Viewed', { + plan_name: planName, + price, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '👀 Pricing Plan Viewed', { + planName, + price, + }); + }, [track, currentSubscription]); + + /** + * 追踪定价计划选择 + * @param {string} planName - 选择的计划名称 + * @param {string} billingCycle - 计费周期 ('monthly' | 'yearly') + * @param {number} price - 价格 + */ + const trackPricingPlanSelected = useCallback((planName, billingCycle = 'monthly', price = 0) => { + if (!planName) { + logger.warn('useSubscriptionEvents', 'trackPricingPlanSelected: planName is required'); + return; + } + + track('Pricing Plan Selected', { + plan_name: planName, + billing_cycle: billingCycle, + price, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '✅ Pricing Plan Selected', { + planName, + billingCycle, + price, + }); + }, [track, currentSubscription]); + + /** + * 追踪支付页面查看 + * @param {string} planName - 购买的计划 + * @param {number} amount - 支付金额 + */ + const trackPaymentPageViewed = useCallback((planName, amount = 0) => { + track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, { + plan_name: planName, + amount, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '💰 Payment Page Viewed', { + planName, + amount, + }); + }, [track, currentSubscription]); + + /** + * 追踪支付方式选择 + * @param {string} paymentMethod - 支付方式 ('wechat_pay' | 'alipay' | 'credit_card') + * @param {number} amount - 支付金额 + */ + const trackPaymentMethodSelected = useCallback((paymentMethod, amount = 0) => { + if (!paymentMethod) { + logger.warn('useSubscriptionEvents', 'trackPaymentMethodSelected: paymentMethod is required'); + return; + } + + track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, { + payment_method: paymentMethod, + amount, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '💳 Payment Method Selected', { + paymentMethod, + amount, + }); + }, [track, currentSubscription]); + + /** + * 追踪支付发起 + * @param {Object} paymentInfo - 支付信息 + * @param {string} paymentInfo.planName - 计划名称 + * @param {string} paymentInfo.paymentMethod - 支付方式 + * @param {number} paymentInfo.amount - 金额 + * @param {string} paymentInfo.billingCycle - 计费周期 + * @param {string} paymentInfo.orderId - 订单ID + */ + const trackPaymentInitiated = useCallback((paymentInfo = {}) => { + track(REVENUE_EVENTS.PAYMENT_INITIATED, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + billing_cycle: paymentInfo.billingCycle, + order_id: paymentInfo.orderId, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '🚀 Payment Initiated', { + planName: paymentInfo.planName, + amount: paymentInfo.amount, + paymentMethod: paymentInfo.paymentMethod, + }); + }, [track, currentSubscription]); + + /** + * 追踪支付成功 + * @param {Object} paymentInfo - 支付信息 + */ + const trackPaymentSuccessful = useCallback((paymentInfo = {}) => { + track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + billing_cycle: paymentInfo.billingCycle, + order_id: paymentInfo.orderId, + transaction_id: paymentInfo.transactionId, + previous_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '✅ Payment Successful', { + planName: paymentInfo.planName, + amount: paymentInfo.amount, + orderId: paymentInfo.orderId, + }); + }, [track, currentSubscription]); + + /** + * 追踪支付失败 + * @param {Object} paymentInfo - 支付信息 + * @param {string} errorReason - 失败原因 + */ + const trackPaymentFailed = useCallback((paymentInfo = {}, errorReason = '') => { + track(REVENUE_EVENTS.PAYMENT_FAILED, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + error_reason: errorReason, + order_id: paymentInfo.orderId, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '❌ Payment Failed', { + planName: paymentInfo.planName, + errorReason, + orderId: paymentInfo.orderId, + }); + }, [track, currentSubscription]); + + /** + * 追踪订阅创建成功 + * @param {Object} subscription - 订阅信息 + */ + const trackSubscriptionCreated = useCallback((subscription = {}) => { + track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, { + plan_name: subscription.plan, + billing_cycle: subscription.billingCycle, + amount: subscription.amount, + start_date: subscription.startDate, + end_date: subscription.endDate, + previous_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '🎉 Subscription Created', { + plan: subscription.plan, + billingCycle: subscription.billingCycle, + }); + }, [track, currentSubscription]); + + /** + * 追踪订阅续费 + * @param {Object} subscription - 订阅信息 + */ + const trackSubscriptionRenewed = useCallback((subscription = {}) => { + track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, { + plan_name: subscription.plan, + amount: subscription.amount, + previous_end_date: subscription.previousEndDate, + new_end_date: subscription.newEndDate, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '🔄 Subscription Renewed', { + plan: subscription.plan, + amount: subscription.amount, + }); + }, [track]); + + /** + * 追踪订阅取消 + * @param {string} reason - 取消原因 + * @param {boolean} cancelImmediately - 是否立即取消 + */ + const trackSubscriptionCancelled = useCallback((reason = '', cancelImmediately = false) => { + track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, { + plan_name: currentSubscription?.plan, + reason, + has_reason: Boolean(reason), + cancel_immediately: cancelImmediately, + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', '🚫 Subscription Cancelled', { + plan: currentSubscription?.plan, + reason, + cancelImmediately, + }); + }, [track, currentSubscription]); + + /** + * 追踪优惠券应用 + * @param {string} couponCode - 优惠券代码 + * @param {number} discountAmount - 折扣金额 + * @param {boolean} success - 是否成功 + */ + const trackCouponApplied = useCallback((couponCode, discountAmount = 0, success = true) => { + if (!couponCode) { + logger.warn('useSubscriptionEvents', 'trackCouponApplied: couponCode is required'); + return; + } + + track('Coupon Applied', { + coupon_code: couponCode, + discount_amount: discountAmount, + success, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + + logger.debug('useSubscriptionEvents', success ? '🎟️ Coupon Applied' : '❌ Coupon Failed', { + couponCode, + discountAmount, + success, + }); + }, [track, currentSubscription]); + + return { + // 付费墙事件 + trackPaywallShown, + trackPaywallDismissed, + trackUpgradePlanClicked, + + // 订阅页面事件 + trackSubscriptionPageViewed, + trackPricingPlanViewed, + trackPricingPlanSelected, + + // 支付流程事件 + trackPaymentPageViewed, + trackPaymentMethodSelected, + trackPaymentInitiated, + trackPaymentSuccessful, + trackPaymentFailed, + + // 订阅管理事件 + trackSubscriptionCreated, + trackSubscriptionRenewed, + trackSubscriptionCancelled, + + // 优惠券事件 + trackCouponApplied, + }; +}; + +export default useSubscriptionEvents;