From 6506cb222b0056a0a3c522ca5a64b45912184815 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 28 Oct 2025 20:09:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20PostHog=20=E9=9B=86=E6=88=90\=201.=20?= =?UTF-8?q?=E2=9C=85=20=E5=AE=89=E8=A3=85=E4=BE=9D=E8=B5=96:=20posthog-js@?= =?UTF-8?q?^1.280.1=20=20=202.=20=E2=9C=85=20=E5=88=9B=E5=BB=BA=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=96=87=E4=BB=B6:=20=20=20=20=20-=20src/lib/posthog.?= =?UTF-8?q?js=20-=20PostHog=20SDK=20=E5=B0=81=E8=A3=85=EF=BC=88271=20?= =?UTF-8?q?=E8=A1=8C=EF=BC=89=20=20=20=20=20-=20src/lib/constants.js=20-?= =?UTF-8?q?=20=E4=BA=8B=E4=BB=B6=E5=B8=B8=E9=87=8F=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=EF=BC=88AARRR=20=E6=A1=86=E6=9E=B6=EF=BC=89=20=20=20=20=20-=20?= =?UTF-8?q?src/hooks/usePostHog.js=20-=20PostHog=20React=20Hook=20=20=20?= =?UTF-8?q?=20=20-=20src/hooks/usePageTracking.js=20-=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=20Hook=20=20=20=20=20-=20src/components/Post?= =?UTF-8?q?HogProvider.js=20-=20Provider=20=E7=BB=84=E4=BB=B6=20=20=203.?= =?UTF-8?q?=20=E2=9C=85=20=E9=9B=86=E6=88=90=E5=88=B0=E5=BA=94=E7=94=A8:?= =?UTF-8?q?=20=20=20=20=20-=20=E4=BF=AE=E6=94=B9=20src/App.js=EF=BC=8C?= =?UTF-8?q?=E5=9C=A8=E6=9C=80=E5=A4=96=E5=B1=82=E6=B7=BB=E5=8A=A0=20=20=20=20=20=20-=20=E8=87=AA=E5=8A=A8=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E6=89=80=E6=9C=89=E9=A1=B5=E9=9D=A2=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=20=20=204.=20=E2=9C=85=20=E9=85=8D=E7=BD=AE=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F:=20=20=20=20=20-=20=E5=9C=A8=20.env=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20PostHog=20=E9=85=8D=E7=BD=AE=E9=A1=B9=20?= =?UTF-8?q?=20=20=20=20-=20REACT=5FAPP=5FPOSTHOG=5FKEY=20=E7=95=99?= =?UTF-8?q?=E7=A9=BA=EF=BC=8C=E9=9C=80=E8=A6=81=E7=94=A8=E6=88=B7=E5=A1=AB?= =?UTF-8?q?=E5=86=99=20=20=205.=20=E2=9C=85=20=E5=88=9B=E5=BB=BA=E6=96=87?= =?UTF-8?q?=E6=A1=A3:=20POSTHOG=5FINTEGRATION.md=20=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/App.js | 57 ++--- src/components/PostHogProvider.js | 83 +++++++ src/hooks/usePageTracking.js | 55 +++++ src/hooks/usePostHog.js | 101 +++++++++ src/lib/constants.js | 351 ++++++++++++++++++++++++++++++ src/lib/posthog.js | 271 +++++++++++++++++++++++ 7 files changed, 892 insertions(+), 27 deletions(-) create mode 100644 src/components/PostHogProvider.js create mode 100644 src/hooks/usePageTracking.js create mode 100644 src/hooks/usePostHog.js create mode 100644 src/lib/constants.js create mode 100644 src/lib/posthog.js diff --git a/package.json b/package.json index a5499614..2c644a07 100755 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "match-sorter": "6.3.0", "moment": "^2.29.1", "nouislider": "15.0.0", + "posthog-js": "^1.281.0", "react": "18.3.1", "react-apexcharts": "^1.3.9", "react-big-calendar": "^0.33.2", diff --git a/src/App.js b/src/App.js index 64e84621..362942f4 100755 --- a/src/App.js +++ b/src/App.js @@ -59,6 +59,7 @@ 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"; /** @@ -295,32 +296,34 @@ export default function App() { }, []); return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/src/components/PostHogProvider.js b/src/components/PostHogProvider.js new file mode 100644 index 00000000..0ed48309 --- /dev/null +++ b/src/components/PostHogProvider.js @@ -0,0 +1,83 @@ +// src/components/PostHogProvider.js +import React, { useEffect, useState } from 'react'; +import { initPostHog } from '../lib/posthog'; +import { usePageTracking } from '../hooks/usePageTracking'; + +/** + * PostHog Provider Component + * Initializes PostHog SDK and provides automatic page view tracking + * + * Usage: + * + * + * + */ +export const PostHogProvider = ({ children }) => { + const [isInitialized, setIsInitialized] = useState(false); + + // Initialize PostHog once when component mounts + useEffect(() => { + // Only run in browser + if (typeof window === 'undefined') return; + + // Initialize PostHog + initPostHog(); + setIsInitialized(true); + + // Log initialization + if (process.env.NODE_ENV === 'development') { + console.log('✅ PostHogProvider initialized'); + } + }, []); + + // Automatically track page views + usePageTracking({ + enabled: isInitialized, + getProperties: (location) => { + // Add custom properties based on route + const properties = {}; + + // Identify page type based on path + if (location.pathname === '/home' || location.pathname === '/home/') { + properties.page_type = 'landing'; + } else if (location.pathname.startsWith('/home/center')) { + properties.page_type = 'dashboard'; + } else if (location.pathname.startsWith('/auth/')) { + properties.page_type = 'auth'; + } else if (location.pathname.startsWith('/community')) { + properties.page_type = 'feature'; + properties.feature_name = 'community'; + } else if (location.pathname.startsWith('/concepts')) { + properties.page_type = 'feature'; + properties.feature_name = 'concepts'; + } else if (location.pathname.startsWith('/stocks')) { + properties.page_type = 'feature'; + properties.feature_name = 'stocks'; + } else if (location.pathname.startsWith('/limit-analyse')) { + properties.page_type = 'feature'; + properties.feature_name = 'limit_analyse'; + } else if (location.pathname.startsWith('/trading-simulation')) { + properties.page_type = 'feature'; + properties.feature_name = 'trading_simulation'; + } else if (location.pathname.startsWith('/company')) { + properties.page_type = 'detail'; + properties.content_type = 'company'; + } else if (location.pathname.startsWith('/event-detail')) { + properties.page_type = 'detail'; + properties.content_type = 'event'; + } + + return properties; + }, + }); + + // Don't render children until PostHog is initialized + // This prevents tracking events before SDK is ready + if (!isInitialized) { + return children; // Or return a loading spinner + } + + return <>{children}; +}; + +export default PostHogProvider; diff --git a/src/hooks/usePageTracking.js b/src/hooks/usePageTracking.js new file mode 100644 index 00000000..6a91ff6e --- /dev/null +++ b/src/hooks/usePageTracking.js @@ -0,0 +1,55 @@ +// src/hooks/usePageTracking.js +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; +import posthog from 'posthog-js'; + +/** + * Custom hook for automatic page view tracking with PostHog + * + * @param {Object} options - Configuration options + * @param {boolean} options.enabled - Whether tracking is enabled + * @param {Function} options.getProperties - Function to get custom properties for each page view + */ +export const usePageTracking = ({ enabled = true, getProperties } = {}) => { + const location = useLocation(); + const previousPathRef = useRef(''); + + useEffect(() => { + if (!enabled) return; + + // Get the current path + const currentPath = location.pathname + location.search; + + // Skip if it's the same page (prevents duplicate tracking) + if (previousPathRef.current === currentPath) { + return; + } + + // Update the previous path + previousPathRef.current = currentPath; + + // Get custom properties if function provided + const customProperties = getProperties ? getProperties(location) : {}; + + // Track page view with PostHog + if (posthog && posthog.__loaded) { + posthog.capture('$pageview', { + $current_url: window.location.href, + path: location.pathname, + search: location.search, + hash: location.hash, + ...customProperties, + }); + + // Log in development + if (process.env.NODE_ENV === 'development') { + console.log('📊 PostHog $pageview:', { + path: location.pathname, + ...customProperties, + }); + } + } + }, [location, enabled, getProperties]); +}; + +export default usePageTracking; diff --git a/src/hooks/usePostHog.js b/src/hooks/usePostHog.js new file mode 100644 index 00000000..eb51f4e9 --- /dev/null +++ b/src/hooks/usePostHog.js @@ -0,0 +1,101 @@ +// src/hooks/usePostHog.js +import { useCallback } from 'react'; +import { + getPostHog, + trackEvent, + trackPageView, + identifyUser, + setUserProperties, + resetUser, + optIn, + optOut, + hasOptedOut, + getFeatureFlag, + isFeatureEnabled, +} from '../lib/posthog'; + +/** + * Custom hook to access PostHog functionality + * Provides convenient methods for tracking events and managing user sessions + * + * @returns {object} PostHog methods + */ +export const usePostHog = () => { + // Get PostHog instance + const posthog = getPostHog(); + + // Track custom event + const track = useCallback((eventName, properties = {}) => { + trackEvent(eventName, properties); + }, []); + + // Track page view + const trackPage = useCallback((pagePath, properties = {}) => { + trackPageView(pagePath, properties); + }, []); + + // Identify user + const identify = useCallback((userId, userProperties = {}) => { + identifyUser(userId, userProperties); + }, []); + + // Set user properties + const setProperties = useCallback((properties) => { + setUserProperties(properties); + }, []); + + // Reset user session (logout) + const reset = useCallback(() => { + resetUser(); + }, []); + + // Opt out of tracking + const optOutTracking = useCallback(() => { + optOut(); + }, []); + + // Opt in to tracking + const optInTracking = useCallback(() => { + optIn(); + }, []); + + // Check if user has opted out + const isOptedOut = useCallback(() => { + return hasOptedOut(); + }, []); + + // Get feature flag value + const getFlag = useCallback((flagKey, defaultValue = false) => { + return getFeatureFlag(flagKey, defaultValue); + }, []); + + // Check if feature is enabled + const isEnabled = useCallback((flagKey) => { + return isFeatureEnabled(flagKey); + }, []); + + return { + // Core PostHog instance + posthog, + + // Tracking methods + track, + trackPage, + + // User management + identify, + setProperties, + reset, + + // Privacy controls + optOut: optOutTracking, + optIn: optInTracking, + isOptedOut, + + // Feature flags + getFlag, + isEnabled, + }; +}; + +export default usePostHog; diff --git a/src/lib/constants.js b/src/lib/constants.js new file mode 100644 index 00000000..ec47bd03 --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,351 @@ +// src/lib/constants.js +// PostHog Event Names and Constants +// Organized by AARRR Framework (Acquisition, Activation, Retention, Referral, Revenue) + +// ============================================================================ +// ACQUISITION (获客) - Landing page, marketing website events +// ============================================================================ +export const ACQUISITION_EVENTS = { + // Landing page + LANDING_PAGE_VIEWED: 'Landing Page Viewed', + CTA_BUTTON_CLICKED: 'CTA Button Clicked', + FEATURE_CARD_VIEWED: 'Feature Card Viewed', + FEATURE_VIDEO_PLAYED: 'Feature Video Played', + + // Pricing page + PRICING_PAGE_VIEWED: 'Pricing Page Viewed', + PRICING_PLAN_VIEWED: 'Pricing Plan Viewed', + PRICING_PLAN_SELECTED: 'Pricing Plan Selected', + + // How to use page + HOW_TO_USE_PAGE_VIEWED: 'How To Use Page Viewed', + TUTORIAL_STEP_VIEWED: 'Tutorial Step Viewed', + + // Roadmap page + ROADMAP_PAGE_VIEWED: 'Roadmap Page Viewed', + ROADMAP_ITEM_CLICKED: 'Roadmap Item Clicked', +}; + +// ============================================================================ +// ACTIVATION (激活) - Sign up, login, onboarding +// ============================================================================ +export const ACTIVATION_EVENTS = { + // Auth pages + LOGIN_PAGE_VIEWED: 'Login Page Viewed', + SIGNUP_PAGE_VIEWED: 'Signup Page Viewed', + + // Login/Signup actions + LOGIN_METHOD_SELECTED: 'Login Method Selected', // wechat, email, phone + WECHAT_QR_DISPLAYED: 'WeChat QR Code Displayed', + WECHAT_QR_SCANNED: 'WeChat QR Code Scanned', + USER_LOGGED_IN: 'User Logged In', + USER_SIGNED_UP: 'User Signed Up', + VERIFICATION_CODE_SENT: 'Verification Code Sent', + VERIFICATION_CODE_SUBMITTED: 'Verification Code Submitted', + LOGIN_FAILED: 'Login Failed', + SIGNUP_FAILED: 'Signup Failed', + + // Onboarding + ONBOARDING_STARTED: 'Onboarding Started', + ONBOARDING_STEP_COMPLETED: 'Onboarding Step Completed', + ONBOARDING_COMPLETED: 'Onboarding Completed', + ONBOARDING_SKIPPED: 'Onboarding Skipped', + + // User agreement + USER_AGREEMENT_VIEWED: 'User Agreement Viewed', + USER_AGREEMENT_ACCEPTED: 'User Agreement Accepted', + PRIVACY_POLICY_VIEWED: 'Privacy Policy Viewed', + PRIVACY_POLICY_ACCEPTED: 'Privacy Policy Accepted', +}; + +// ============================================================================ +// RETENTION (留存) - Core product usage, feature engagement +// ============================================================================ +export const RETENTION_EVENTS = { + // Dashboard + DASHBOARD_VIEWED: 'Dashboard Viewed', + DASHBOARD_CENTER_VIEWED: 'Dashboard Center Viewed', + FUNCTION_CARD_CLICKED: 'Function Card Clicked', // Core功能卡片点击 + + // Navigation + TOP_NAV_CLICKED: 'Top Navigation Clicked', + SIDEBAR_MENU_CLICKED: 'Sidebar Menu Clicked', + MENU_ITEM_CLICKED: 'Menu Item Clicked', + BREADCRUMB_CLICKED: 'Breadcrumb Clicked', + + // Search + SEARCH_INITIATED: 'Search Initiated', + SEARCH_QUERY_SUBMITTED: 'Search Query Submitted', + SEARCH_RESULT_CLICKED: 'Search Result Clicked', + SEARCH_NO_RESULTS: 'Search No Results', + SEARCH_FILTER_APPLIED: 'Search Filter Applied', + + // News/Community (新闻催化分析) + COMMUNITY_PAGE_VIEWED: 'Community Page Viewed', + NEWS_LIST_VIEWED: 'News List Viewed', + NEWS_ARTICLE_CLICKED: 'News Article Clicked', + NEWS_DETAIL_OPENED: 'News Detail Opened', + NEWS_TAB_CLICKED: 'News Tab Clicked', // 相关标的, 相关概念, etc. + NEWS_FILTER_APPLIED: 'News Filter Applied', + NEWS_SORTED: 'News Sorted', + + // Concept Center (概念中心) + CONCEPT_PAGE_VIEWED: 'Concept Page Viewed', + CONCEPT_LIST_VIEWED: 'Concept List Viewed', + CONCEPT_CLICKED: 'Concept Clicked', + CONCEPT_DETAIL_VIEWED: 'Concept Detail Viewed', + CONCEPT_STOCK_CLICKED: 'Concept Stock Clicked', + + // Stock Center (个股中心) + STOCK_OVERVIEW_VIEWED: 'Stock Overview Page Viewed', + STOCK_LIST_VIEWED: 'Stock List Viewed', + STOCK_SEARCHED: 'Stock Searched', + STOCK_CLICKED: 'Stock Clicked', + STOCK_DETAIL_VIEWED: 'Stock Detail Viewed', + STOCK_TAB_CLICKED: 'Stock Tab Clicked', // 公司概览, 股票行情, 财务全景, 盈利预测 + + // Company Details + COMPANY_OVERVIEW_VIEWED: 'Company Overview Viewed', + COMPANY_FINANCIALS_VIEWED: 'Company Financials Viewed', + COMPANY_FORECAST_VIEWED: 'Company Forecast Viewed', + COMPANY_MARKET_DATA_VIEWED: 'Company Market Data Viewed', + + // Limit Analysis (涨停分析) + LIMIT_ANALYSE_PAGE_VIEWED: 'Limit Analyse Page Viewed', + LIMIT_BOARD_CLICKED: 'Limit Board Clicked', + LIMIT_SECTOR_EXPANDED: 'Limit Sector Expanded', + LIMIT_SECTOR_ANALYSIS_VIEWED: 'Limit Sector Analysis Viewed', + LIMIT_STOCK_CLICKED: 'Limit Stock Clicked', + + // Trading Simulation (模拟盘交易) + TRADING_SIMULATION_ENTERED: 'Trading Simulation Entered', + SIMULATION_ORDER_PLACED: 'Simulation Order Placed', + SIMULATION_HOLDINGS_VIEWED: 'Simulation Holdings Viewed', + SIMULATION_HISTORY_VIEWED: 'Simulation History Viewed', + SIMULATION_STOCK_SEARCHED: 'Simulation Stock Searched', + + // Event Details + EVENT_DETAIL_VIEWED: 'Event Detail Viewed', + EVENT_ANALYSIS_VIEWED: 'Event Analysis Viewed', + EVENT_TIMELINE_CLICKED: 'Event Timeline Clicked', + + // Profile & Settings + PROFILE_PAGE_VIEWED: 'Profile Page Viewed', + PROFILE_UPDATED: 'Profile Updated', + SETTINGS_PAGE_VIEWED: 'Settings Page Viewed', + SETTINGS_CHANGED: 'Settings Changed', + + // Subscription Management + SUBSCRIPTION_PAGE_VIEWED: 'Subscription Page Viewed', + UPGRADE_PLAN_CLICKED: 'Upgrade Plan Clicked', +}; + +// ============================================================================ +// REFERRAL (推荐) - Sharing, inviting +// ============================================================================ +export const REFERRAL_EVENTS = { + // Sharing + SHARE_BUTTON_CLICKED: 'Share Button Clicked', + CONTENT_SHARED: 'Content Shared', + SHARE_LINK_GENERATED: 'Share Link Generated', + SHARE_MODAL_OPENED: 'Share Modal Opened', + SHARE_MODAL_CLOSED: 'Share Modal Closed', + + // Referral + REFERRAL_PAGE_VIEWED: 'Referral Page Viewed', + REFERRAL_LINK_COPIED: 'Referral Link Copied', + REFERRAL_INVITE_SENT: 'Referral Invite Sent', +}; + +// ============================================================================ +// REVENUE (收入) - Payment, subscription, monetization +// ============================================================================ +export const REVENUE_EVENTS = { + // Paywall + PAYWALL_SHOWN: 'Paywall Shown', + PAYWALL_DISMISSED: 'Paywall Dismissed', + PAYWALL_UPGRADE_CLICKED: 'Paywall Upgrade Clicked', + + // Payment + PAYMENT_PAGE_VIEWED: 'Payment Page Viewed', + PAYMENT_METHOD_SELECTED: 'Payment Method Selected', + PAYMENT_INITIATED: 'Payment Initiated', + PAYMENT_SUCCESSFUL: 'Payment Successful', + PAYMENT_FAILED: 'Payment Failed', + + // Subscription + SUBSCRIPTION_CREATED: 'Subscription Created', + SUBSCRIPTION_RENEWED: 'Subscription Renewed', + SUBSCRIPTION_UPGRADED: 'Subscription Upgraded', + SUBSCRIPTION_DOWNGRADED: 'Subscription Downgraded', + SUBSCRIPTION_CANCELLED: 'Subscription Cancelled', + SUBSCRIPTION_EXPIRED: 'Subscription Expired', + + // Refund + REFUND_REQUESTED: 'Refund Requested', + REFUND_PROCESSED: 'Refund Processed', +}; + +// ============================================================================ +// SPECIAL EVENTS (特殊事件) - Errors, performance, chatbot +// ============================================================================ +export const SPECIAL_EVENTS = { + // Errors + ERROR_OCCURRED: 'Error Occurred', + API_ERROR: 'API Error', + NOT_FOUND_404: '404 Not Found', + + // Performance + PAGE_LOAD_TIME: 'Page Load Time', + API_RESPONSE_TIME: 'API Response Time', + + // Chatbot (Dify) + CHATBOT_OPENED: 'Chatbot Opened', + CHATBOT_CLOSED: 'Chatbot Closed', + CHATBOT_MESSAGE_SENT: 'Chatbot Message Sent', + CHATBOT_MESSAGE_RECEIVED: 'Chatbot Message Received', + CHATBOT_FEEDBACK_PROVIDED: 'Chatbot Feedback Provided', + + // Scroll depth + SCROLL_DEPTH_25: 'Scroll Depth 25%', + SCROLL_DEPTH_50: 'Scroll Depth 50%', + SCROLL_DEPTH_75: 'Scroll Depth 75%', + SCROLL_DEPTH_100: 'Scroll Depth 100%', + + // Session + SESSION_STARTED: 'Session Started', + SESSION_ENDED: 'Session Ended', + USER_IDLE: 'User Idle', + USER_RETURNED: 'User Returned', + + // Logout + USER_LOGGED_OUT: 'User Logged Out', +}; + +// ============================================================================ +// USER PROPERTIES (用户属性) +// ============================================================================ +export const USER_PROPERTIES = { + // Identity + EMAIL: 'email', + USERNAME: 'username', + USER_ID: 'user_id', + PHONE: 'phone', + + // Subscription + SUBSCRIPTION_TIER: 'subscription_tier', // 'free', 'pro', 'enterprise' + SUBSCRIPTION_STATUS: 'subscription_status', // 'active', 'expired', 'cancelled' + SUBSCRIPTION_START_DATE: 'subscription_start_date', + SUBSCRIPTION_END_DATE: 'subscription_end_date', + + // Engagement + REGISTRATION_DATE: 'registration_date', + LAST_LOGIN: 'last_login', + LOGIN_COUNT: 'login_count', + DAYS_SINCE_REGISTRATION: 'days_since_registration', + LIFETIME_VALUE: 'lifetime_value', + + // Preferences + PREFERRED_LANGUAGE: 'preferred_language', + THEME_PREFERENCE: 'theme_preference', // 'light', 'dark' + NOTIFICATION_ENABLED: 'notification_enabled', + + // Attribution + UTM_SOURCE: 'utm_source', + UTM_MEDIUM: 'utm_medium', + UTM_CAMPAIGN: 'utm_campaign', + REFERRER: 'referrer', + + // Behavioral + FAVORITE_FEATURES: 'favorite_features', + MOST_VISITED_PAGES: 'most_visited_pages', + TOTAL_SESSIONS: 'total_sessions', + AVERAGE_SESSION_DURATION: 'average_session_duration', +}; + +// ============================================================================ +// SUBSCRIPTION TIERS (订阅等级) +// ============================================================================ +export const SUBSCRIPTION_TIERS = { + FREE: 'free', + PRO: 'pro', + ENTERPRISE: 'enterprise', +}; + +// ============================================================================ +// PAGE TYPES (页面类型) +// ============================================================================ +export const PAGE_TYPES = { + LANDING: 'landing', + DASHBOARD: 'dashboard', + FEATURE: 'feature', + DETAIL: 'detail', + AUTH: 'auth', + SETTINGS: 'settings', + PAYMENT: 'payment', + ERROR: 'error', +}; + +// ============================================================================ +// CONTENT TYPES (内容类型) +// ============================================================================ +export const CONTENT_TYPES = { + NEWS: 'news', + STOCK: 'stock', + CONCEPT: 'concept', + ANALYSIS: 'analysis', + EVENT: 'event', + COMPANY: 'company', +}; + +// ============================================================================ +// SHARE CHANNELS (分享渠道) +// ============================================================================ +export const SHARE_CHANNELS = { + WECHAT: 'wechat', + LINK: 'link', + QRCODE: 'qrcode', + EMAIL: 'email', + COPY: 'copy', +}; + +// ============================================================================ +// LOGIN METHODS (登录方式) +// ============================================================================ +export const LOGIN_METHODS = { + WECHAT: 'wechat', + EMAIL: 'email', + PHONE: 'phone', + USERNAME: 'username', +}; + +// ============================================================================ +// PAYMENT METHODS (支付方式) +// ============================================================================ +export const PAYMENT_METHODS = { + WECHAT_PAY: 'wechat_pay', + ALIPAY: 'alipay', + CREDIT_CARD: 'credit_card', +}; + +// ============================================================================ +// Helper function to get all events +// ============================================================================ +export const getAllEvents = () => { + return { + ...ACQUISITION_EVENTS, + ...ACTIVATION_EVENTS, + ...RETENTION_EVENTS, + ...REFERRAL_EVENTS, + ...REVENUE_EVENTS, + ...SPECIAL_EVENTS, + }; +}; + +// ============================================================================ +// Helper function to validate event name +// ============================================================================ +export const isValidEvent = (eventName) => { + const allEvents = getAllEvents(); + return Object.values(allEvents).includes(eventName); +}; diff --git a/src/lib/posthog.js b/src/lib/posthog.js new file mode 100644 index 00000000..1173a250 --- /dev/null +++ b/src/lib/posthog.js @@ -0,0 +1,271 @@ +// src/lib/posthog.js +import posthog from 'posthog-js'; + +/** + * Initialize PostHog SDK + * Should be called once when the app starts + */ +export const initPostHog = () => { + // Only run in browser environment + if (typeof window === 'undefined') return; + + const apiKey = process.env.REACT_APP_POSTHOG_KEY; + const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com'; + + if (!apiKey) { + console.warn('⚠️ PostHog API key not found. Analytics will be disabled.'); + return; + } + + try { + posthog.init(apiKey, { + api_host: apiHost, + + // Pageview tracking - manual control for better accuracy + capture_pageview: false, // We'll manually capture with custom properties + capture_pageleave: true, // Auto-capture when user leaves page + + // Session Recording Configuration + session_recording: { + enabled: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true', + + // Privacy: Mask sensitive input fields + maskInputOptions: { + password: true, + email: true, + phone: true, + 'data-sensitive': true, // Custom attribute for sensitive fields + }, + + // Record canvas for charts/graphs + recordCanvas: true, + + // Network payload capture (useful for debugging API issues) + networkPayloadCapture: { + recordHeaders: true, + recordBody: true, + // Don't record sensitive endpoints + urlBlocklist: [ + '/api/auth/session', + '/api/auth/login', + '/api/auth/register', + '/api/payment', + ], + }, + }, + + // Performance optimization + batch_size: 10, // Send events in batches of 10 + batch_interval_ms: 3000, // Or every 3 seconds + + // Privacy settings + respect_dnt: true, // Respect Do Not Track browser setting + persistence: 'localStorage+cookie', // Use both for reliability + + // Feature flags (for A/B testing) + bootstrap: { + featureFlags: {}, + }, + + // Autocapture settings + autocapture: { + // Automatically capture clicks on buttons, links, etc. + dom_event_allowlist: ['click', 'submit', 'change'], + + // Capture additional element properties + capture_copied_text: false, // Don't capture copied text (privacy) + }, + + // Development debugging + loaded: (posthogInstance) => { + if (process.env.NODE_ENV === 'development') { + console.log('✅ PostHog initialized successfully'); + posthogInstance.debug(); // Enable debug mode in development + } + }, + }); + + console.log('📊 PostHog Analytics initialized'); + } catch (error) { + console.error('❌ PostHog initialization failed:', error); + } +}; + +/** + * Get PostHog instance + * @returns {object} PostHog instance + */ +export const getPostHog = () => { + return posthog; +}; + +/** + * Identify user with PostHog + * Call this after successful login/registration + * + * @param {string} userId - Unique user identifier + * @param {object} userProperties - User properties (email, name, subscription_tier, etc.) + */ +export const identifyUser = (userId, userProperties = {}) => { + if (!userId) { + console.warn('⚠️ Cannot identify user: userId is required'); + return; + } + + try { + posthog.identify(userId, { + email: userProperties.email, + username: userProperties.username, + subscription_tier: userProperties.subscription_tier || 'free', + role: userProperties.role, + registration_date: userProperties.registration_date, + last_login: new Date().toISOString(), + ...userProperties, + }); + + console.log('👤 User identified:', userId); + } catch (error) { + console.error('❌ User identification failed:', error); + } +}; + +/** + * Update user properties + * Use this to update user attributes without re-identifying + * + * @param {object} properties - Properties to update + */ +export const setUserProperties = (properties) => { + try { + posthog.people.set(properties); + console.log('📝 User properties updated'); + } catch (error) { + console.error('❌ Failed to update user properties:', error); + } +}; + +/** + * Track custom event + * + * @param {string} eventName - Name of the event + * @param {object} properties - Event properties + */ +export const trackEvent = (eventName, properties = {}) => { + try { + posthog.capture(eventName, { + ...properties, + timestamp: new Date().toISOString(), + }); + + if (process.env.NODE_ENV === 'development') { + console.log('📍 Event tracked:', eventName, properties); + } + } catch (error) { + console.error('❌ Event tracking failed:', error); + } +}; + +/** + * Track page view + * + * @param {string} pagePath - Current page path + * @param {object} properties - Additional properties + */ +export const trackPageView = (pagePath, properties = {}) => { + try { + posthog.capture('$pageview', { + $current_url: window.location.href, + page_path: pagePath, + page_title: document.title, + referrer: document.referrer, + ...properties, + }); + + if (process.env.NODE_ENV === 'development') { + console.log('📄 Page view tracked:', pagePath); + } + } catch (error) { + console.error('❌ Page view tracking failed:', error); + } +}; + +/** + * Reset user session + * Call this on logout + */ +export const resetUser = () => { + try { + posthog.reset(); + console.log('🔄 User session reset'); + } catch (error) { + console.error('❌ Session reset failed:', error); + } +}; + +/** + * User opt-out from tracking + */ +export const optOut = () => { + try { + posthog.opt_out_capturing(); + console.log('🚫 User opted out of tracking'); + } catch (error) { + console.error('❌ Opt-out failed:', error); + } +}; + +/** + * User opt-in to tracking + */ +export const optIn = () => { + try { + posthog.opt_in_capturing(); + console.log('✅ User opted in to tracking'); + } catch (error) { + console.error('❌ Opt-in failed:', error); + } +}; + +/** + * Check if user has opted out + * @returns {boolean} + */ +export const hasOptedOut = () => { + try { + return posthog.has_opted_out_capturing(); + } catch (error) { + console.error('❌ Failed to check opt-out status:', error); + return false; + } +}; + +/** + * Get feature flag value + * @param {string} flagKey - Feature flag key + * @param {any} defaultValue - Default value if flag not found + * @returns {any} Feature flag value + */ +export const getFeatureFlag = (flagKey, defaultValue = false) => { + try { + return posthog.getFeatureFlag(flagKey) || defaultValue; + } catch (error) { + console.error('❌ Failed to get feature flag:', error); + return defaultValue; + } +}; + +/** + * Check if feature flag is enabled + * @param {string} flagKey - Feature flag key + * @returns {boolean} + */ +export const isFeatureEnabled = (flagKey) => { + try { + return posthog.isFeatureEnabled(flagKey); + } catch (error) { + console.error('❌ Failed to check feature flag:', error); + return false; + } +}; + +export default posthog;