feat: PostHog 集成\
1. ✅ 安装依赖: posthog-js@^1.280.1 2. ✅ 创建核心文件: - src/lib/posthog.js - PostHog SDK 封装(271 行) - src/lib/constants.js - 事件常量定义(AARRR 框架) - src/hooks/usePostHog.js - PostHog React Hook - src/hooks/usePageTracking.js - 页面追踪 Hook - src/components/PostHogProvider.js - Provider 组件 3. ✅ 集成到应用: - 修改 src/App.js,在最外层添加 <PostHogProvider> - 自动追踪所有页面浏览 4. ✅ 配置环境变量: - 在 .env 添加 PostHog 配置项 - REACT_APP_POSTHOG_KEY 留空,需要用户填写 5. ✅ 创建文档: POSTHOG_INTEGRATION.md 包含完整的使用说明
This commit is contained in:
@@ -43,6 +43,7 @@
|
|||||||
"match-sorter": "6.3.0",
|
"match-sorter": "6.3.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"nouislider": "15.0.0",
|
"nouislider": "15.0.0",
|
||||||
|
"posthog-js": "^1.281.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-apexcharts": "^1.3.9",
|
"react-apexcharts": "^1.3.9",
|
||||||
"react-big-calendar": "^0.33.2",
|
"react-big-calendar": "^0.33.2",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import NotificationContainer from "components/NotificationContainer";
|
|||||||
import ConnectionStatusBar from "components/ConnectionStatusBar";
|
import ConnectionStatusBar from "components/ConnectionStatusBar";
|
||||||
import NotificationTestTool from "components/NotificationTestTool";
|
import NotificationTestTool from "components/NotificationTestTool";
|
||||||
import ScrollToTop from "components/ScrollToTop";
|
import ScrollToTop from "components/ScrollToTop";
|
||||||
|
import PostHogProvider from "components/PostHogProvider";
|
||||||
import { logger } from "utils/logger";
|
import { logger } from "utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,6 +296,7 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PostHogProvider>
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ChakraProvider
|
<ChakraProvider
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -322,5 +324,6 @@ export default function App() {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
|
</PostHogProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
83
src/components/PostHogProvider.js
Normal file
83
src/components/PostHogProvider.js
Normal file
@@ -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:
|
||||||
|
* <PostHogProvider>
|
||||||
|
* <App />
|
||||||
|
* </PostHogProvider>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
55
src/hooks/usePageTracking.js
Normal file
55
src/hooks/usePageTracking.js
Normal file
@@ -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;
|
||||||
101
src/hooks/usePostHog.js
Normal file
101
src/hooks/usePostHog.js
Normal file
@@ -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;
|
||||||
351
src/lib/constants.js
Normal file
351
src/lib/constants.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
271
src/lib/posthog.js
Normal file
271
src/lib/posthog.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user