forked from 77media/video-flow
新增 埋点上报
This commit is contained in:
parent
bc0269983d
commit
0420836109
@ -3,6 +3,9 @@ NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai
|
|||||||
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||||
NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com
|
NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com
|
||||||
NEXT_PUBLIC_CUT_URL_TO = https://smartcut.huiying.video
|
NEXT_PUBLIC_CUT_URL_TO = https://smartcut.huiying.video
|
||||||
|
# google analysis
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID = G-BHBXC1B1JL
|
||||||
|
NEXT_PUBLIC_GA_ENABLED = true
|
||||||
# 失败率
|
# 失败率
|
||||||
NEXT_PUBLIC_ERROR_CONFIG = 0.5
|
NEXT_PUBLIC_ERROR_CONFIG = 0.5
|
||||||
# Google OAuth配置
|
# Google OAuth配置
|
||||||
|
|||||||
@ -19,3 +19,6 @@ NEXT_PUBLIC_ERROR_CONFIG = 0.2
|
|||||||
# Google OAuth配置
|
# Google OAuth配置
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com
|
||||||
|
|
||||||
|
# google analysis
|
||||||
|
NEXT_PUBLIC_GA_MEASUREMENT_ID = G-E6VBGZ4ER5
|
||||||
|
NEXT_PUBLIC_GA_ENABLED = true
|
||||||
@ -8,6 +8,9 @@ COPY package.json package-lock.json* ./
|
|||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
# Google Analytics 环境变量
|
||||||
|
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=G-4BDXV6TWF4
|
||||||
|
ENV NEXT_PUBLIC_GA_ENABLED=true
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
import { Providers } from '@/components/providers';
|
import { Providers } from '@/components/providers';
|
||||||
import { ConfigProvider, theme } from 'antd';
|
import { ConfigProvider, theme } from 'antd';
|
||||||
import CallbackModal from '@/components/common/CallbackModal';
|
import CallbackModal from '@/components/common/CallbackModal';
|
||||||
|
import { useAppStartupAnalytics } from '@/hooks/useAppStartupAnalytics';
|
||||||
|
|
||||||
// 创建上下文来传递弹窗控制方法
|
// 创建上下文来传递弹窗控制方法
|
||||||
const CallbackModalContext = createContext<{
|
const CallbackModalContext = createContext<{
|
||||||
@ -26,6 +27,10 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
const [showCallbackModal, setShowCallbackModal] = useState(false)
|
const [showCallbackModal, setShowCallbackModal] = useState(false)
|
||||||
const [paymentType, setPaymentType] = useState<'subscription' | 'token'>('subscription')
|
const [paymentType, setPaymentType] = useState<'subscription' | 'token'>('subscription')
|
||||||
|
|
||||||
|
// 应用启动时设置用户GA属性
|
||||||
|
useAppStartupAnalytics();
|
||||||
|
|
||||||
const openCallback = async function (ev: MessageEvent<any>) {
|
const openCallback = async function (ev: MessageEvent<any>) {
|
||||||
if (ev.data.type === 'waiting-payment') {
|
if (ev.data.type === 'waiting-payment') {
|
||||||
setPaymentType(ev.data.paymentType || 'subscription')
|
setPaymentType(ev.data.paymentType || 'subscription')
|
||||||
@ -48,17 +53,25 @@ export default function RootLayout({
|
|||||||
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
|
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E6VBGZ4ER5"></script>
|
{process.env.NEXT_PUBLIC_GA_ENABLED === 'true' && (
|
||||||
<script
|
<>
|
||||||
dangerouslySetInnerHTML={{
|
<script async src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}`}></script>
|
||||||
__html: `
|
<script
|
||||||
window.dataLayer = window.dataLayer || [];
|
dangerouslySetInnerHTML={{
|
||||||
function gtag(){window.dataLayer.push(arguments);}
|
__html: `
|
||||||
gtag('js', new Date());
|
window.dataLayer = window.dataLayer || [];
|
||||||
gtag('config', 'G-E6VBGZ4ER5');
|
function gtag(){window.dataLayer.push(arguments);}
|
||||||
`,
|
gtag('js', new Date());
|
||||||
}}
|
gtag('config', '${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}', {
|
||||||
/>
|
page_title: document.title,
|
||||||
|
page_location: window.location.href,
|
||||||
|
send_page_view: true
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</head>
|
</head>
|
||||||
<body className="font-sans antialiased">
|
<body className="font-sans antialiased">
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
|
|||||||
@ -13,8 +13,13 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
|
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
|
||||||
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||||
|
import { trackEvent, trackPageView, trackPayment } from "@/utils/analytics";
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
|
// 页面访问跟踪
|
||||||
|
useEffect(() => {
|
||||||
|
trackPageView('/pricing', 'Pricing Plans');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
@ -91,6 +96,17 @@ function HomeModule5() {
|
|||||||
|
|
||||||
const handleSubscribe = async (planName: string) => {
|
const handleSubscribe = async (planName: string) => {
|
||||||
setLoadingPlan(planName);
|
setLoadingPlan(planName);
|
||||||
|
|
||||||
|
// 跟踪订阅按钮点击事件
|
||||||
|
trackEvent('subscription_button_click', {
|
||||||
|
event_category: 'subscription',
|
||||||
|
event_label: planName,
|
||||||
|
custom_parameters: {
|
||||||
|
plan_name: planName,
|
||||||
|
billing_type: billingType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
||||||
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
||||||
const win = window.open(url, '_blank');
|
const win = window.open(url, '_blank');
|
||||||
@ -148,7 +164,14 @@ function HomeModule5() {
|
|||||||
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
|
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setBillingType("month")}
|
onClick={() => {
|
||||||
|
setBillingType("month");
|
||||||
|
trackEvent('billing_toggle', {
|
||||||
|
event_category: 'subscription',
|
||||||
|
event_label: 'month',
|
||||||
|
custom_parameters: { billing_type: 'month' },
|
||||||
|
});
|
||||||
|
}}
|
||||||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||||||
billingType === "month"
|
billingType === "month"
|
||||||
? "bg-white text-black"
|
? "bg-white text-black"
|
||||||
@ -164,7 +187,14 @@ function HomeModule5() {
|
|||||||
Monthly
|
Monthly
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setBillingType("year")}
|
onClick={() => {
|
||||||
|
setBillingType("year");
|
||||||
|
trackEvent('billing_toggle', {
|
||||||
|
event_category: 'subscription',
|
||||||
|
event_label: 'year',
|
||||||
|
custom_parameters: { billing_type: 'year' },
|
||||||
|
});
|
||||||
|
}}
|
||||||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||||||
billingType === "year"
|
billingType === "year"
|
||||||
? "bg-white text-black"
|
? "bg-white text-black"
|
||||||
|
|||||||
4
app/types/global.d.ts
vendored
4
app/types/global.d.ts
vendored
@ -12,6 +12,10 @@ declare global {
|
|||||||
loading: (message: string) => ReturnType<typeof toast.promise>;
|
loading: (message: string) => ReturnType<typeof toast.promise>;
|
||||||
dismiss: () => void;
|
dismiss: () => void;
|
||||||
};
|
};
|
||||||
|
// Google Analytics 类型声明
|
||||||
|
gtag: (...args: any[]) => void;
|
||||||
|
dataLayer: any[];
|
||||||
|
|
||||||
// Google GSI API类型声明
|
// Google GSI API类型声明
|
||||||
google?: {
|
google?: {
|
||||||
accounts: {
|
accounts: {
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { clearAuthData, getUserProfile, isAuthenticated } from '@/lib/auth';
|
import { clearAuthData, getUserProfile, isAuthenticated, getCurrentUser } from '@/lib/auth';
|
||||||
import GlobalLoad from '../common/GlobalLoad';
|
import GlobalLoad from '../common/GlobalLoad';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { errorHandle } from '@/api/errorHandle';
|
import { errorHandle } from '@/api/errorHandle';
|
||||||
|
import { setUserProperties } from '@/utils/analytics';
|
||||||
|
|
||||||
interface AuthGuardProps {
|
interface AuthGuardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -21,6 +22,36 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
|||||||
const publicPaths = ['/','/login', '/signup', '/forgot-password', '/Terms', '/Privacy', '/activate', '/users/oauth/callback'];
|
const publicPaths = ['/','/login', '/signup', '/forgot-password', '/Terms', '/Privacy', '/activate', '/users/oauth/callback'];
|
||||||
const isPublicPath = publicPaths.includes(pathname);
|
const isPublicPath = publicPaths.includes(pathname);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户GA属性
|
||||||
|
* 基于实际userData结构: {"id":"8f61686734d340d0820f0fe980368629","userId":"8f61686734d340d0820f0fe980368629","username":"Orange","email":"moviflow68@test.com","isActive":1,"authType":"LOCAL","lastLogin":"2025-09-28T16:28:51.870731"}
|
||||||
|
*/
|
||||||
|
const setUserAnalyticsProperties = (userData: any) => {
|
||||||
|
if (!userData || !userData.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUserProperties(userData.id, {
|
||||||
|
// 基础用户信息
|
||||||
|
user_id: userData.id,
|
||||||
|
email: userData.email,
|
||||||
|
username: userData.username,
|
||||||
|
// 认证信息
|
||||||
|
auth_type: userData.authType || 'LOCAL',
|
||||||
|
is_active: userData.isActive || 1,
|
||||||
|
// 登录信息
|
||||||
|
last_login: userData.lastLogin || new Date().toISOString(),
|
||||||
|
// 页面信息
|
||||||
|
current_page: pathname,
|
||||||
|
// 用户状态
|
||||||
|
user_status: userData.isActive === 1 ? 'active' : 'inactive',
|
||||||
|
// 会话信息
|
||||||
|
session_id: `${userData.id}_${Date.now()}`
|
||||||
|
});
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const verifyAuth = async () => {
|
const verifyAuth = async () => {
|
||||||
// 如果是公共页面,不需要鉴权
|
// 如果是公共页面,不需要鉴权
|
||||||
@ -41,6 +72,9 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
|||||||
const user = await getUserProfile();
|
const user = await getUserProfile();
|
||||||
if (user) {
|
if (user) {
|
||||||
setIsAuthorized(true);
|
setIsAuthorized(true);
|
||||||
|
|
||||||
|
// 设置用户GA属性(页面首次加载时)
|
||||||
|
setUserAnalyticsProperties(user);
|
||||||
} else {
|
} else {
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
}
|
}
|
||||||
@ -49,8 +83,16 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
|||||||
if(errorCode.message == 401||errorCode.message == 502){
|
if(errorCode.message == 401||errorCode.message == 502){
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
|
} else {
|
||||||
|
// 如果API调用失败但不是认证错误,尝试使用本地存储的用户数据
|
||||||
|
const localUser = getCurrentUser();
|
||||||
|
if (localUser && localUser.id) {
|
||||||
|
console.log('API调用失败,使用本地用户数据设置GA属性');
|
||||||
|
setUserAnalyticsProperties(localUser);
|
||||||
|
setIsAuthorized(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
errorHandle(errorCode.message)
|
errorHandle(errorCode.message)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
194
hooks/useAnalytics.ts
Normal file
194
hooks/useAnalytics.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Google Analytics 自定义Hook
|
||||||
|
* 提供便捷的GA事件跟踪功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { trackPageView, trackEvent, isGAAvailable, setUserProperties } from '@/utils/analytics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面访问跟踪Hook
|
||||||
|
* @param pageTitle - 页面标题
|
||||||
|
* @param customParams - 自定义参数
|
||||||
|
*/
|
||||||
|
export const usePageTracking = (
|
||||||
|
pageTitle?: string,
|
||||||
|
customParams?: Record<string, any>
|
||||||
|
) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isGAAvailable()) {
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
trackPageView(currentPath, pageTitle, { custom_parameters: customParams });
|
||||||
|
}
|
||||||
|
}, [router, pageTitle, customParams]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户行为跟踪Hook
|
||||||
|
*/
|
||||||
|
export const useEventTracking = () => {
|
||||||
|
const trackUserAction = (
|
||||||
|
action: string,
|
||||||
|
category: string = 'user',
|
||||||
|
label?: string,
|
||||||
|
value?: number,
|
||||||
|
customParams?: Record<string, any>
|
||||||
|
) => {
|
||||||
|
trackEvent(action, {
|
||||||
|
event_category: category,
|
||||||
|
event_label: label,
|
||||||
|
value: value,
|
||||||
|
custom_parameters: customParams,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackButtonClick = (buttonName: string, location?: string) => {
|
||||||
|
trackUserAction('button_click', 'interaction', buttonName, undefined, {
|
||||||
|
button_name: buttonName,
|
||||||
|
location: location,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackFormSubmit = (formName: string, success: boolean = true) => {
|
||||||
|
trackUserAction('form_submit', 'form', formName, undefined, {
|
||||||
|
form_name: formName,
|
||||||
|
success: success,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackNavigation = (from: string, to: string) => {
|
||||||
|
trackUserAction('navigation', 'user', `${from} -> ${to}`, undefined, {
|
||||||
|
from_page: from,
|
||||||
|
to_page: to,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackUserAction,
|
||||||
|
trackButtonClick,
|
||||||
|
trackFormSubmit,
|
||||||
|
trackNavigation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频相关事件跟踪Hook
|
||||||
|
*/
|
||||||
|
export const useVideoTracking = () => {
|
||||||
|
const trackVideoCreation = (templateType: string, aspectRatio?: string) => {
|
||||||
|
trackEvent('video_creation_start', {
|
||||||
|
event_category: 'video',
|
||||||
|
event_label: templateType,
|
||||||
|
custom_parameters: {
|
||||||
|
template_type: templateType,
|
||||||
|
aspect_ratio: aspectRatio,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackVideoGeneration = (duration: number, templateType: string) => {
|
||||||
|
trackEvent('video_generation_complete', {
|
||||||
|
event_category: 'video',
|
||||||
|
value: duration,
|
||||||
|
custom_parameters: {
|
||||||
|
template_type: templateType,
|
||||||
|
video_duration: duration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackVideoDownload = (videoId: string, format: string) => {
|
||||||
|
trackEvent('video_download', {
|
||||||
|
event_category: 'video',
|
||||||
|
event_label: format,
|
||||||
|
custom_parameters: {
|
||||||
|
video_id: videoId,
|
||||||
|
format: format,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackVideoShare = (videoId: string, platform: string) => {
|
||||||
|
trackEvent('video_share', {
|
||||||
|
event_category: 'video',
|
||||||
|
event_label: platform,
|
||||||
|
custom_parameters: {
|
||||||
|
video_id: videoId,
|
||||||
|
platform: platform,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackVideoCreation,
|
||||||
|
trackVideoGeneration,
|
||||||
|
trackVideoDownload,
|
||||||
|
trackVideoShare,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付相关事件跟踪Hook
|
||||||
|
*/
|
||||||
|
export const usePaymentTracking = () => {
|
||||||
|
const trackPaymentStart = (paymentType: string, amount: number, currency: string = 'USD') => {
|
||||||
|
trackEvent('payment_start', {
|
||||||
|
event_category: 'ecommerce',
|
||||||
|
value: amount,
|
||||||
|
custom_parameters: {
|
||||||
|
payment_type: paymentType,
|
||||||
|
currency: currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackPaymentComplete = (paymentType: string, amount: number, currency: string = 'USD') => {
|
||||||
|
trackEvent('purchase', {
|
||||||
|
event_category: 'ecommerce',
|
||||||
|
value: amount,
|
||||||
|
custom_parameters: {
|
||||||
|
payment_type: paymentType,
|
||||||
|
currency: currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackPaymentFailed = (paymentType: string, errorReason: string) => {
|
||||||
|
trackEvent('payment_failed', {
|
||||||
|
event_category: 'ecommerce',
|
||||||
|
event_label: errorReason,
|
||||||
|
custom_parameters: {
|
||||||
|
payment_type: paymentType,
|
||||||
|
error_reason: errorReason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackPaymentStart,
|
||||||
|
trackPaymentComplete,
|
||||||
|
trackPaymentFailed,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户属性管理Hook
|
||||||
|
*/
|
||||||
|
export const useUserProperties = () => {
|
||||||
|
const setUserAnalyticsProperties = (
|
||||||
|
userId: string,
|
||||||
|
userProperties: Record<string, any>
|
||||||
|
) => {
|
||||||
|
setUserProperties(userId, userProperties);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
setUserAnalyticsProperties,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
67
hooks/useAppStartupAnalytics.ts
Normal file
67
hooks/useAppStartupAnalytics.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 应用启动时用户属性设置Hook
|
||||||
|
* 确保在应用启动时为用户设置GA属性
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { getCurrentUser, isAuthenticated } from '@/lib/auth';
|
||||||
|
import { setUserProperties } from '@/utils/analytics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用启动时设置用户GA属性
|
||||||
|
*/
|
||||||
|
export const useAppStartupAnalytics = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeUserAnalytics = () => {
|
||||||
|
// 检查用户是否已认证
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
console.log('用户未认证,跳过GA属性设置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地存储的用户数据
|
||||||
|
const currentUser = getCurrentUser();
|
||||||
|
if (!currentUser || !currentUser.id) {
|
||||||
|
console.log('本地用户数据不存在,跳过GA属性设置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置应用启动时的用户属性
|
||||||
|
// 基于实际userData结构: {"id":"8f61686734d340d0820f0fe980368629","userId":"8f61686734d340d0820f0fe980368629","username":"Orange","email":"moviflow68@test.com","isActive":1,"authType":"LOCAL","lastLogin":"2025-09-28T16:28:51.870731"}
|
||||||
|
setUserProperties(currentUser.id, {
|
||||||
|
// 基础用户信息
|
||||||
|
user_id: currentUser.id,
|
||||||
|
email: currentUser.email,
|
||||||
|
username: currentUser.username,
|
||||||
|
|
||||||
|
// 认证信息
|
||||||
|
auth_type: currentUser.authType || 'LOCAL',
|
||||||
|
is_active: currentUser.isActive || 1,
|
||||||
|
|
||||||
|
// 应用启动信息
|
||||||
|
last_login: currentUser.lastLogin || new Date().toISOString(),
|
||||||
|
|
||||||
|
// 用户状态
|
||||||
|
user_status: currentUser.isActive === 1 ? 'active' : 'inactive',
|
||||||
|
|
||||||
|
// 设备信息
|
||||||
|
user_agent: navigator.userAgent,
|
||||||
|
screen_resolution: `${screen.width}x${screen.height}`,
|
||||||
|
language: navigator.language,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
|
||||||
|
// 会话信息
|
||||||
|
session_id: `${currentUser.id}_${Date.now()}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 应用启动时GA用户属性设置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟执行,确保应用完全加载
|
||||||
|
const timer = setTimeout(initializeUserAnalytics, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
20
lib/auth.ts
20
lib/auth.ts
@ -7,6 +7,7 @@ import type {
|
|||||||
EmailConflictData,
|
EmailConflictData,
|
||||||
OAuthState
|
OAuthState
|
||||||
} from '@/app/types/google-oauth';
|
} from '@/app/types/google-oauth';
|
||||||
|
import { setUserProperties } from '@/utils/analytics';
|
||||||
|
|
||||||
// API配置
|
// API配置
|
||||||
//const JAVA_BASE_URL = 'http://192.168.120.36:8080';
|
//const JAVA_BASE_URL = 'http://192.168.120.36:8080';
|
||||||
@ -109,6 +110,25 @@ export const getCurrentUser = () => {
|
|||||||
export const setUser = (user: any) => {
|
export const setUser = (user: any) => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||||
|
|
||||||
|
// 设置详细的GA用户属性
|
||||||
|
if (user && user.id) {
|
||||||
|
setUserProperties(user.id, {
|
||||||
|
// 基础用户信息
|
||||||
|
user_id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
// 认证信息
|
||||||
|
auth_type: user.authType || 'LOCAL',
|
||||||
|
is_active: user.isActive || 1,
|
||||||
|
// 用户状态
|
||||||
|
user_status: user.isActive === 1 ? 'active' : 'inactive',
|
||||||
|
// 登录信息
|
||||||
|
last_login: user.lastLogin || new Date().toISOString(),
|
||||||
|
// 会话信息
|
||||||
|
session_id: `${user.id}_${Date.now()}`
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
334
utils/analytics.ts
Normal file
334
utils/analytics.ts
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* Google Analytics 4 工具函数
|
||||||
|
* 提供标准化的事件跟踪和页面访问监控
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 扩展全局Window接口
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
gtag: (...args: any[]) => void;
|
||||||
|
dataLayer: any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GA4事件参数类型定义
|
||||||
|
*/
|
||||||
|
export interface GAEventParameters {
|
||||||
|
event_category?: string;
|
||||||
|
event_label?: string;
|
||||||
|
value?: number;
|
||||||
|
custom_parameters?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面访问参数类型定义
|
||||||
|
*/
|
||||||
|
export interface GAPageViewParameters {
|
||||||
|
page_title?: string;
|
||||||
|
page_location?: string;
|
||||||
|
custom_parameters?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化/序列化事件参数,避免 [object Object]
|
||||||
|
*/
|
||||||
|
const normalizeEventParams = (
|
||||||
|
params: Record<string, any>
|
||||||
|
): Record<string, string | number | boolean> => {
|
||||||
|
const result: Record<string, string | number | boolean> = {};
|
||||||
|
|
||||||
|
const assignPrimitive = (key: string, value: any) => {
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
typeof value === 'boolean'
|
||||||
|
) {
|
||||||
|
result[key] = value;
|
||||||
|
} else if (value === null) {
|
||||||
|
result[key] = 'null';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
result[key] = JSON.stringify(value);
|
||||||
|
} catch (_) {
|
||||||
|
result[key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params || {})) {
|
||||||
|
if (key === 'custom_parameters' && value && typeof value === 'object') {
|
||||||
|
for (const [ck, cv] of Object.entries(value)) {
|
||||||
|
assignPrimitive(ck, cv);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assignPrimitive(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查GA是否可用
|
||||||
|
*/
|
||||||
|
export const isGAAvailable = (): boolean => {
|
||||||
|
return typeof window !== 'undefined' &&
|
||||||
|
typeof window.gtag === 'function' &&
|
||||||
|
process.env.NEXT_PUBLIC_GA_ENABLED === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取GA测量ID
|
||||||
|
*/
|
||||||
|
export const getGAMeasurementId = (): string => {
|
||||||
|
return process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-4BDXV6TWF4';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪自定义事件
|
||||||
|
* @param eventName - 事件名称
|
||||||
|
* @param parameters - 事件参数
|
||||||
|
*/
|
||||||
|
export const trackEvent = (
|
||||||
|
eventName: string,
|
||||||
|
parameters?: GAEventParameters
|
||||||
|
): void => {
|
||||||
|
if (!isGAAvailable()) {
|
||||||
|
console.warn('Google Analytics not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventParamsRaw = {
|
||||||
|
event_category: parameters?.event_category || 'general',
|
||||||
|
event_label: parameters?.event_label,
|
||||||
|
value: parameters?.value,
|
||||||
|
...parameters?.custom_parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventParams = normalizeEventParams(eventParamsRaw);
|
||||||
|
|
||||||
|
window.gtag('event', eventName, eventParams);
|
||||||
|
|
||||||
|
// 开发环境下打印日志
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('GA Event:', eventName, eventParams);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error tracking GA event:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪页面访问
|
||||||
|
* @param pagePath - 页面路径
|
||||||
|
* @param pageTitle - 页面标题
|
||||||
|
* @param parameters - 额外参数
|
||||||
|
*/
|
||||||
|
export const trackPageView = (
|
||||||
|
pagePath: string,
|
||||||
|
pageTitle?: string,
|
||||||
|
parameters?: GAPageViewParameters
|
||||||
|
): void => {
|
||||||
|
if (!isGAAvailable()) {
|
||||||
|
console.warn('Google Analytics not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pageParamsRaw = {
|
||||||
|
page_path: pagePath,
|
||||||
|
page_title: pageTitle,
|
||||||
|
page_location: parameters?.page_location || window.location.href,
|
||||||
|
...parameters?.custom_parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageParams = normalizeEventParams(pageParamsRaw);
|
||||||
|
|
||||||
|
window.gtag('config', getGAMeasurementId(), pageParams);
|
||||||
|
|
||||||
|
// 开发环境下打印日志
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('GA Page View:', pagePath, pageParams);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error tracking GA page view:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪用户注册事件
|
||||||
|
* @param method - 注册方式 (email, google, etc.)
|
||||||
|
*/
|
||||||
|
export const trackUserRegistration = (method: string): void => {
|
||||||
|
trackEvent('user_registration', {
|
||||||
|
event_category: 'user',
|
||||||
|
event_label: method,
|
||||||
|
custom_parameters: {
|
||||||
|
registration_method: method,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪用户登录事件
|
||||||
|
* @param method - 登录方式 (email, google, etc.)
|
||||||
|
*/
|
||||||
|
export const trackUserLogin = (method: string): void => {
|
||||||
|
trackEvent('user_login', {
|
||||||
|
event_category: 'user',
|
||||||
|
event_label: method,
|
||||||
|
custom_parameters: {
|
||||||
|
login_method: method,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪视频创建开始事件
|
||||||
|
* @param templateType - 模板类型
|
||||||
|
* @param aspectRatio - 视频比例
|
||||||
|
*/
|
||||||
|
export const trackVideoCreationStart = (
|
||||||
|
templateType: string,
|
||||||
|
aspectRatio?: string
|
||||||
|
): void => {
|
||||||
|
trackEvent('video_creation_start', {
|
||||||
|
event_category: 'video',
|
||||||
|
event_label: templateType,
|
||||||
|
custom_parameters: {
|
||||||
|
template_type: templateType,
|
||||||
|
aspect_ratio: aspectRatio,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪视频生成完成事件
|
||||||
|
* @param duration - 视频时长
|
||||||
|
* @param templateType - 模板类型
|
||||||
|
*/
|
||||||
|
export const trackVideoGenerationComplete = (
|
||||||
|
duration: number,
|
||||||
|
templateType: string
|
||||||
|
): void => {
|
||||||
|
trackEvent('video_generation_complete', {
|
||||||
|
event_category: 'video',
|
||||||
|
value: duration,
|
||||||
|
custom_parameters: {
|
||||||
|
template_type: templateType,
|
||||||
|
video_duration: duration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪支付事件
|
||||||
|
* @param paymentType - 支付类型 (subscription, token)
|
||||||
|
* @param amount - 支付金额
|
||||||
|
* @param currency - 货币类型
|
||||||
|
*/
|
||||||
|
export const trackPayment = (
|
||||||
|
paymentType: string,
|
||||||
|
amount: number,
|
||||||
|
currency: string = 'USD'
|
||||||
|
): void => {
|
||||||
|
trackEvent('purchase', {
|
||||||
|
event_category: 'ecommerce',
|
||||||
|
value: amount,
|
||||||
|
custom_parameters: {
|
||||||
|
payment_type: paymentType,
|
||||||
|
currency: currency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪模板选择事件
|
||||||
|
* @param templateId - 模板ID
|
||||||
|
* @param templateName - 模板名称
|
||||||
|
*/
|
||||||
|
export const trackTemplateSelection = (
|
||||||
|
templateId: string,
|
||||||
|
templateName: string
|
||||||
|
): void => {
|
||||||
|
trackEvent('template_selection', {
|
||||||
|
event_category: 'template',
|
||||||
|
event_label: templateName,
|
||||||
|
custom_parameters: {
|
||||||
|
template_id: templateId,
|
||||||
|
template_name: templateName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪功能使用事件
|
||||||
|
* @param featureName - 功能名称
|
||||||
|
* @param action - 操作类型
|
||||||
|
*/
|
||||||
|
export const trackFeatureUsage = (
|
||||||
|
featureName: string,
|
||||||
|
action: string
|
||||||
|
): void => {
|
||||||
|
trackEvent('feature_usage', {
|
||||||
|
event_category: 'feature',
|
||||||
|
event_label: featureName,
|
||||||
|
custom_parameters: {
|
||||||
|
feature_name: featureName,
|
||||||
|
action: action,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪错误事件
|
||||||
|
* @param errorType - 错误类型
|
||||||
|
* @param errorMessage - 错误信息
|
||||||
|
* @param errorLocation - 错误位置
|
||||||
|
*/
|
||||||
|
export const trackError = (
|
||||||
|
errorType: string,
|
||||||
|
errorMessage: string,
|
||||||
|
errorLocation?: string
|
||||||
|
): void => {
|
||||||
|
trackEvent('error', {
|
||||||
|
event_category: 'error',
|
||||||
|
event_label: errorType,
|
||||||
|
custom_parameters: {
|
||||||
|
error_type: errorType,
|
||||||
|
error_message: errorMessage,
|
||||||
|
error_location: errorLocation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪用户属性
|
||||||
|
* @param userId - 用户ID
|
||||||
|
* @param userProperties - 用户属性
|
||||||
|
*/
|
||||||
|
export const setUserProperties = (
|
||||||
|
userId: string,
|
||||||
|
userProperties: Record<string, any>
|
||||||
|
): void => {
|
||||||
|
if (!isGAAvailable()) {
|
||||||
|
console.warn('Google Analytics not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// GA4 推荐:通过 config 设置 user_id,通过 set user_properties 设置用户属性
|
||||||
|
window.gtag('config', getGAMeasurementId(), {
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
window.gtag('set', 'user_properties', normalizeEventParams(userProperties));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting user properties:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user