新增 埋点上报

This commit is contained in:
moux1024 2025-09-28 18:11:17 +08:00
parent bc0269983d
commit 0420836109
11 changed files with 728 additions and 15 deletions

View File

@ -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配置

View File

@ -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

View File

@ -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

View File

@ -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 async src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}`}></script>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);} function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-E6VBGZ4ER5'); 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

View File

@ -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"

View File

@ -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: {

View File

@ -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,6 +83,14 @@ 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 {

194
hooks/useAnalytics.ts Normal file
View 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,
};
};

View 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);
}, []);
};

View File

@ -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
View 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);
}
};