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_CUT_URL = https://77.smartcut.py.qikongjian.com
|
||||
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
|
||||
# Google OAuth配置
|
||||
|
||||
@ -19,3 +19,6 @@ NEXT_PUBLIC_ERROR_CONFIG = 0.2
|
||||
# Google OAuth配置
|
||||
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
|
||||
|
||||
ENV NODE_ENV=production
|
||||
# Google Analytics 环境变量
|
||||
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=G-4BDXV6TWF4
|
||||
ENV NEXT_PUBLIC_GA_ENABLED=true
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { Providers } from '@/components/providers';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import CallbackModal from '@/components/common/CallbackModal';
|
||||
import { useAppStartupAnalytics } from '@/hooks/useAppStartupAnalytics';
|
||||
|
||||
// 创建上下文来传递弹窗控制方法
|
||||
const CallbackModalContext = createContext<{
|
||||
@ -26,6 +27,10 @@ export default function RootLayout({
|
||||
}) {
|
||||
const [showCallbackModal, setShowCallbackModal] = useState(false)
|
||||
const [paymentType, setPaymentType] = useState<'subscription' | 'token'>('subscription')
|
||||
|
||||
// 应用启动时设置用户GA属性
|
||||
useAppStartupAnalytics();
|
||||
|
||||
const openCallback = async function (ev: MessageEvent<any>) {
|
||||
if (ev.data.type === 'waiting-payment') {
|
||||
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="shortcut icon" type="image/x-icon" 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>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){window.dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-E6VBGZ4ER5');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{process.env.NEXT_PUBLIC_GA_ENABLED === 'true' && (
|
||||
<>
|
||||
<script async src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}`}></script>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
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>
|
||||
<body className="font-sans antialiased">
|
||||
<ConfigProvider
|
||||
|
||||
@ -13,8 +13,13 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
|
||||
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||||
import { trackEvent, trackPageView, trackPayment } from "@/utils/analytics";
|
||||
|
||||
export default function PricingPage() {
|
||||
// 页面访问跟踪
|
||||
useEffect(() => {
|
||||
trackPageView('/pricing', 'Pricing Plans');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
@ -91,6 +96,17 @@ function HomeModule5() {
|
||||
|
||||
const handleSubscribe = async (planName: string) => {
|
||||
setLoadingPlan(planName);
|
||||
|
||||
// 跟踪订阅按钮点击事件
|
||||
trackEvent('subscription_button_click', {
|
||||
event_category: 'subscription',
|
||||
event_label: planName,
|
||||
custom_parameters: {
|
||||
plan_name: planName,
|
||||
billing_type: billingType,
|
||||
},
|
||||
});
|
||||
|
||||
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
||||
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
||||
const win = window.open(url, '_blank');
|
||||
@ -148,7 +164,14 @@ function HomeModule5() {
|
||||
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
|
||||
>
|
||||
<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 ${
|
||||
billingType === "month"
|
||||
? "bg-white text-black"
|
||||
@ -164,7 +187,14 @@ function HomeModule5() {
|
||||
Monthly
|
||||
</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 ${
|
||||
billingType === "year"
|
||||
? "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>;
|
||||
dismiss: () => void;
|
||||
};
|
||||
// Google Analytics 类型声明
|
||||
gtag: (...args: any[]) => void;
|
||||
dataLayer: any[];
|
||||
|
||||
// Google GSI API类型声明
|
||||
google?: {
|
||||
accounts: {
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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 { message } from 'antd';
|
||||
import { errorHandle } from '@/api/errorHandle';
|
||||
import { setUserProperties } from '@/utils/analytics';
|
||||
|
||||
interface AuthGuardProps {
|
||||
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 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(() => {
|
||||
const verifyAuth = async () => {
|
||||
// 如果是公共页面,不需要鉴权
|
||||
@ -41,6 +72,9 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
||||
const user = await getUserProfile();
|
||||
if (user) {
|
||||
setIsAuthorized(true);
|
||||
|
||||
// 设置用户GA属性(页面首次加载时)
|
||||
setUserAnalyticsProperties(user);
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
@ -49,8 +83,16 @@ export default function AuthGuard({ children }: AuthGuardProps) {
|
||||
if(errorCode.message == 401||errorCode.message == 502){
|
||||
router.push('/login');
|
||||
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 {
|
||||
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,
|
||||
OAuthState
|
||||
} from '@/app/types/google-oauth';
|
||||
import { setUserProperties } from '@/utils/analytics';
|
||||
|
||||
// API配置
|
||||
//const JAVA_BASE_URL = 'http://192.168.120.36:8080';
|
||||
@ -109,6 +110,25 @@ export const getCurrentUser = () => {
|
||||
export const setUser = (user: any) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
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